5

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

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

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

注意:我已经看到修改 NSTextStorage 会导致插入点移动到行尾,但这假设我是 NSTextStorage 的子类,所以我不能在那里使用该解决方案(并且宁愿不子类 NSTextStorage,因为它是一个半-抽象子类,如果我将其子类化,我会失去苹果类的某些行为)。

4

2 回答 2

2

我找到了问题的根源。

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

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

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

细节

当您键入和更改样式时会发生什么

这仅适用于单次processEditing()运行,无论是在NSTextStorage子类中还是在NSTextStorageDelegate回调中。

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

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

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

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

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

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

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

更多细节

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

于 2017-11-29T20:25:17.367 回答
0

通知中心接受的上述答案对我有用,但在编辑文本时我必须再包括一件事。(这可能与选择不同)。

editedRange通知NSTextStorage中心回调后被重击。processEditing因此,我通过重写函数并在稍后收到回调时使用该值来自己跟踪最后一个已知值。

override func processEditing() {
        // Hack.. the editedRange property when reading from the notification center callback is weird
        lastEditedRange = editedRange
        super.processEditing()
    }
于 2022-02-18T19:36:05.920 回答