title: Replacing NSAttributedString in NSTextStorage Moves NSTextView Cursor
body: <p>I followed <a href="https://www.raywenderlich.com/5960-text-kit-tutorial-getting-started" rel="nofollow noreferrer">this tutorial</a> and created a Mac version of it. It works pretty well, except there's a bug I can't figure out. The cursor jumps to the end of the string if you try editing anything in the middle of the string. Like this:</p>
  
  <p><a href="https://i.stack.imgur.com/tKnrn.gif" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/tKnrn.gif" alt="NSTextView Cursor Jumps to End"></a></p>
  
  <p>Here is a <a href="https://d.pr/f/7nGAjp" rel="nofollow noreferrer">sample project</a>, or you can just create a new macOS project and put this in the default <strong>ViewController.swift</strong>:</p>
  
  <pre><code>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)
      ])
    }
  
  }
  </code></pre>
  
  <p>And then create a <strong>FancyTextStorage</strong> class that subclasses <code>NSTextStorage</code> with this:</p>
  
  <pre><code>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 &lt; 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?) -&gt; [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")
    }
  }
  </code></pre>
  
  <p>It seems that when the string is rewritten, it doesn't preserve the cursor position, but this same code on iOS (from the above-mentioned tutorial) doesn't have this problem.</p>
  
  <p>Any ideas?</p>