NSTextStorageDelegate 的 textStorage(_,willProcessEditing:,range:,changeInLength:) 移动选择

NSTextStorageDelegate's textStorage(_,willProcessEditing:,range:,changeInLength:) moves selection

我正在尝试实现一个语法着色文本编辑器,它还可以为您在新行的开头插入空格,或者用文本附件替换文本。

在先前的实现出现撤消问题后再次仔细阅读文档后,似乎推荐的瓶颈是 NSTextStorageDelegate 的 textStorage(_,willProcessEditing:,range:,changeInLength:) 方法(声明 Delegates can change the characters or attributes.,而 didProcessEditing 说我只能 更改属性)。这很好用, 除了 每当我实际更改属性或文本时,文本插入标记都会移动到我修改的任何文本范围的末尾(所以如果我更改整行的样式, 光标移动到行尾)。

有人知道我遗漏了哪些额外的电话告诉 NSTextStorage/NSTextView 不要搞砸插入标记吗?此外,一旦我插入文本,我可能必须告诉它移动插入标记以说明我插入的文本。

注意:我见过 Modifying NSTextStorage causes insertion point to move to the end of the line,但假设我是 subclassing NSTextStorage,所以我不能使用该解决方案那里(并且宁愿不使用 subclass NSTextStorage,因为它是一个半抽象的 subclass,如果我对它进行 subclass 编辑,我会失去 Apple class 的某些行为).

我找到了问题的根源。

并且是唯一能够基于 Cocoa 框架固有的原因而不仅仅是变通办法的可靠解决方案。 (请注意,可能至少还有一种基于 ton 快速修复的其他亚稳态方法会产生类似的结果,但随着亚稳态替代方案的出现,这将非常脆弱,需要大量的维护工作。)

  • TL;DR 问题:NSTextStorage 收集 edited 调用并组合范围,从用户编辑的更改(例如插入)开始,然后在突出显示期间添加来自 addAttributes(_:range:) 调用的所有范围。

  • TL;DR 解决方案:仅从 textDidChange(_:) 执行突出显示。

详情

这仅适用于单个 processEditing() 运行,在 NSTextStorage 子类和 NSTextStorageDelegate 回调中。

我发现执行突出显示的唯一安全方法是挂钩 NSText.didChangeNotification 或实现 NSTextDelegate.textDidChange(_:).

根据@Willeke 对 OP 问题的评论,这是布局传递后执行更改的最佳位置。但与评论线程相反,设置 NSText.selectedRange 是不够的。您不会注意到 post 的问题 - 在插入符号移开后修复选择,直到

  • 您突出显示了整个文本块,
  • 跨越多行,并且
  • 超出滚动视图的可见(NSClipView)边界。

在这种罕见的情况下,大多数击键都会使滚动视图摇晃或弹跳。但是没有针对此的额外快速修复。我试过。既不能阻止从 NSLayoutManager 中的私有 API 发送滚动命令,也不能通过覆盖 NSTextView 子类中包含 "scroll" 的所有方法来避免滚动。当然,您可以完全停止滚动到插入点,但是没有这样的运气得到一个可靠的算法,它只在您执行突出显示时才滚动。

didChangeNotification 方法在我和我的应用程序的测试人员能够想到的所有情况下都能可靠地工作(包括像滚动文本一样奇怪的崩溃情况,然后在动画期间替换字符串用更短的东西——是的,试着从崩溃日志中找出报告无效字形生成的那种东西……)。

这种方法之所以有效,是因为它进行了 2 次字形生成:

  1. 编辑范围的一次通过,在输入长度为1的NSRange的每个击键的情况下,发送带有两个[.editedCharacters, .editedAttributes]edited通知,前者负责移动插入符;
  2. 对受语法高亮影响的任何范围的另一个传递,仅发送带有 [.editedAttributes]edited 通知,因此 不会 完全影响插入符号的位置.

更多细节

如果您想更多地了解问题的根源,我将更多的研究、不同的方法和解决方案的详细信息放在更长的博客中 post 以供参考。不过,这就是解决方案本身。 http://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/

上面通知中心接受的答案对我有用,但在编辑文本时我不得不再添加一件事。 (可能与选择不同)。

NSTextStorageeditedRange 在通知中心回调后被重击了。因此,我通过覆盖 processEditing 函数并在稍后获得回调时使用该值来自己跟踪最后一个已知值。

override func processEditing() {
        // Hack.. the editedRange property when reading from the notification center callback is weird
        lastEditedRange = editedRange
        super.processEditing()
    }