Modifying NSTextStorage causes insertion point to

2020-03-12 04:54发布

问题:

I've got an NSTextView subclass acting as its NSTextStorage delegate. I'm trying to do 2 things:

  1. Highlight the text in some ways
  2. Evaluate the text and then append the answer to the textview.

I'm doing this in two different methods, both invoked by the - (void)textStorageWillProcessEditing:(NSNotification *)notification delegate callback.

I can do the syntax highlighting just fine, but when it comes to appending my answer, the insertion point jumps to the end of the line and I don't really know why. My evaluate method looks like the following:

NSString *result = ..;
NSRange lineRange = [[textStorage string] lineRangeForRange:[self selectedRange]];
NSString *line = [[textStorage string] substringWithRange:lineRange];
line = [self appendResult:result toLine:line]; // appends the answer

[textStorage replaceCharactersInRange:lineRange withString:line];

Doing that will append my result just fine, but the problem is, as mentioned, the insertion point jumps to the end.

I've tried:

  1. Wrapping those above calls up in [textStorage beginEditing] and -endEditing.
  2. Saving the selection range (i.e., the insertion point) before changing the text storage so I can reset it afterwards, but no dice.

Am I doing this right? I'm trying to do this the least hackish way, and I'm also unsure if this is the ideal place to be doing my parsing/highlighting. The docs lead me to believe this, but maybe it's wrong.

回答1:

I know that this question has been long since answered, however I had exactly the same issue. In my NSTextStorage subclass I was doing the following:

- (void)processEditing {
    //Process self.editedRange and apply styles first
    [super processEditing];
}

However, the correct thing to do is this:

- (void)processEditing {
    [super processEditing];
    //Process self.editedRange and apply styles after calling superclass method
}


回答2:

Reason for the insertion point to move

Suprisingly, I never found an actual explanation to why these suggestion do (or do not) work.

Digging into it, the reason for the insertion point to move is: .editedCharacters (NSTextStorageEditedCharacters in ObjC)affects the position of the insertion point from NSLayoutManager.processEditing(from:editedMask:...).

If only .editedAttributes/NSTextStorageEditedAttributes is sent, the insertion point will not be touched. This is what you will want to achieve when you highlight: change attributes only.

Why highlighting affects the insertion point

The problem with highlighting here is that NSTextStorage collects all edited calls during a single processing run and combines the ranges, starting with the user-edited change (e.g. the insertion when typing), then forming a union of this and all ranges reported by addAttributes(_:range:). This results in one single NSLayoutManager.processEditing(from:editedMask:...) call -- with an editedMask of both [.editedCharacters, .editedAttributes].

So you want to send .editedAttributes for the highlighted ranges but end up forming a union with .editedCharacters instead. That union moves the insertion point waaaaaaaay beyond where it should go.

Changing the order in processEditing to call super first works because the layout manager will be notified of a finished edit. But this approach will still break for some edge cases, resulting in invalid layout or jiggling scroll views while you type in very large paragraphs.

This is true for hooking into NSTextStorageDelegate, too, by the way.

Hook into callbacks after layout has truly finished to trigger highlighting instead of processEditing

The only solution that will work robustly based on reasons inherent to the Cocoa framework is to perform highlighting from textDidChange(_:) exclusively, i.e. after the layout processing really has been finished. Subscribing to NSTextDidChangeNotification work just as well.

Downside: you have to trigger highlighting passes for programmatic changes to the underlying string as these will not invoke the textDidChange(_:) callback.


In case you want to know more about the source of the problem, I put more my research, different approaches, and details of the solution in a much longer blog post for reference. This post is still a self-contained solution in itself: http://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/



回答3:

It's simple! I ended up breaking this problem into 2 parts. I still do my syntax highlighting as a result of the textStorage delegate callback, but now I do my evaluation and appending elsewhere.

I ended up overriding both -insertText: and -deleteBackwards: (I might also want to do the same for -deleteForwards:, too). Both overrides look like the following:

- (void)insertText:(id)insertString {
    [super insertText:insertString];
    NSRange selectedRange = [self selectedRange];
    [self doEvaluationAndAppendResult];
    [self setSelectedRange:selectedRange];
}

I ended up having to reset the insertion point manually here. I'd still love to figure out why that's necessary, but at least this feels like less of a hack.