I've got an NSTextView
subclass acting as its NSTextStorage
delegate. I'm trying to do 2 things:
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:
[textStorage beginEditing]
and -endEditing
.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.
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.
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.
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With