替换 NSTextStorage 中的 NSAttributedString 移动 NSTextView 光标

Replacing NSAttributedString in NSTextStorage Moves NSTextView Cursor

我关注 this tutorial 并创建了它的 Mac 版本。它工作得很好,除了有一个我无法弄清楚的错误。如果您尝试编辑字符串中间的任何内容,光标会跳到字符串的末尾。像这样:

这是一个sample project,或者你可以只创建一个新的macOS项目并将其放入默认ViewController.swift:

import Cocoa

class ViewController: NSViewController, NSTextViewDelegate {
  var textView: NSTextView!
  var textStorage: FancyTextStorage!

  override func viewDidLoad() {
    super.viewDidLoad()

    createTextView()
  }

  func createTextView() {
    // 1
    let attrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13)]
    let attrString = NSAttributedString(string: "This is a *cool* sample.", attributes: attrs)
    textStorage = FancyTextStorage()
    textStorage.append(attrString)

    let newTextViewRect = view.bounds

    // 2
    let layoutManager = NSLayoutManager()

    // 3
    let containerSize = CGSize(width: newTextViewRect.width, height: .greatestFiniteMagnitude)
    let container = NSTextContainer(size: containerSize)
    container.widthTracksTextView = true
    layoutManager.addTextContainer(container)
    textStorage.addLayoutManager(layoutManager)

    // 4
    textView = NSTextView(frame: newTextViewRect, textContainer: container)
    textView.delegate = self
    view.addSubview(textView)

    // 5
    textView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
      textView.topAnchor.constraint(equalTo: view.topAnchor),
      textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
    ])
  }

}

然后创建一个 FancyTextStorage class 子classes NSTextStorage 与此:

class FancyTextStorage: NSTextStorage{
  let backingStore = NSMutableAttributedString()
  private var replacements: [String: [NSAttributedString.Key: Any]] = [:]

  override var string: String {
    return backingStore.string
  }
  override init() {
    super.init()
    createHighlightPatterns()
  }

  func createHighlightPatterns() {
    let boldAttributes = [NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 13)]
    replacements = ["(\*\w+(\s\w+)*\*)": boldAttributes]
  }

  func applyStylesToRange(searchRange: NSRange) {
    let normalAttrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13, weight: .regular), NSAttributedString.Key.foregroundColor: NSColor.init(calibratedRed: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)]

    addAttributes(normalAttrs, range: searchRange)

    // iterate over each replacement
    for (pattern, attributes) in replacements {
      do {
        let regex = try NSRegularExpression(pattern: pattern)
        regex.enumerateMatches(in: backingStore.string, range: searchRange) {
          match, flags, stop in
          // apply the style
          if let matchRange = match?.range(at: 1) {
            print("Matched pattern: \(pattern)")
            addAttributes(attributes, range: matchRange)

            // reset the style to the original
            let maxRange = matchRange.location + matchRange.length
            if maxRange + 1 < length {
              addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
            }
          }
        }
      }
      catch {
        print("An error occurred attempting to locate pattern: " +
              "\(error.localizedDescription)")
      }
    }
  }

  func performReplacementsForRange(changedRange: NSRange) {
    var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(changedRange.location, 0)))
    extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(NSMaxRange(changedRange), 0)))
    beginEditing()
    applyStylesToRange(searchRange: extendedRange)
    endEditing()
  }

  override func processEditing() {
    performReplacementsForRange(changedRange: editedRange)
    super.processEditing()
  }

  override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
    return backingStore.attributes(at: location, effectiveRange: range)
  }

  override func replaceCharacters(in range: NSRange, with str: String) {
    print("replaceCharactersInRange:\(range) withString:\(str)")

    backingStore.replaceCharacters(in: range, with:str)
    edited(.editedCharacters, range: range,
          changeInLength: (str as NSString).length - range.length)
  }

  override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
    //print("setAttributes:\(String(describing: attrs)) range:\(range)")
    backingStore.setAttributes(attrs, range: range)
    edited(.editedAttributes, range: range, changeInLength: 0)
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
    fatalError("init(pasteboardPropertyList:ofType:) has not been implemented")
  }
}

似乎在重写字符串时,它不会保留光标位置,但是 iOS 上的相同代码(来自上述教程)没有这个问题。

有什么想法吗?

我想我(希望)在阅读这篇文章后明白了:https://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/

在我的 ViewController.swift 中,我添加了 textDidChange 委托方法和用于更新样式的可重用函数:

func textDidChange(_ notification: Notification) {
  updateStyles()
}

func updateStyles(){
  guard let fancyTextStorage = textView.textStorage as? FancyTextStorage else { return }

  fancyTextStorage.beginEditing()
  fancyTextStorage.applyStylesToRange(searchRange: fancyTextStorage.extendedRange)
  fancyTextStorage.endEditing()
}

然后在 FancyTextStorage 中,我必须从 processEditing() 删除 performReplacementsForRange 因为它调用 applyStylesToRange() 而上述文章的要点是 你不能在 TextStorageprocessEditing() 函数中应用样式 否则世界将会爆炸(并且光标会移动到末尾)。

我希望这对其他人有帮助!