子类化NSTextStorage中断列表编辑

Mar*_*ark 4 cocoa nstextview nstextstorage textkit

我有一个标准的基本Mac应用程序NSTextView.我正在尝试实现并使用子类NSTextStorage,但即使是非常基本的实现也会破坏列表编辑行为:

  1. 我添加了一个带有两个项目的项目符号列表
  2. 我将该列表进一步复制并粘贴到文档中
  3. 在粘贴列表中按Enter键会中断最后一个列表项的格式.

这是一个快速视频:

NSTextStorage列表问题

两个问题:

  1. 粘贴列表的项目符号点使用较小的字体大小
  2. 在第二个列表项打破第三个项后按Enter键

当我不替换文本存储时,这工作正常.

这是我的代码:

ViewController.swift

@IBOutlet var textView:NSTextView!

override func viewDidLoad() {
   [...]
   textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
Run Code Online (Sandbox Code Playgroud)

TestTextStorage.swift

class TestTextStorage: NSTextStorage {

    let backingStore = NSMutableAttributedString()

    override var string: String {
        return backingStore.string
    }

    override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key:Any] {
        return backingStore.attributes(at: location, effectiveRange: range)
    }

    override func replaceCharacters(in range: NSRange, with str: String) {
        beginEditing()
        backingStore.replaceCharacters(in: range, with:str)
        edited(.editedCharacters, range: range,
               changeInLength: (str as NSString).length - range.length)
        endEditing()
    }

    override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
        beginEditing()
        backingStore.setAttributes(attrs, range: range)
        edited(.editedAttributes, range: range, changeInLength: 0)
        endEditing()
    }
}
Run Code Online (Sandbox Code Playgroud)

CRD*_*CRD 5

你在Swift中发现了一个错误(也许不仅仅是在Swift库中,也许是更基本的东西).

那么发生了什么?

如果您创建编号列表而不是项目符号列表,您将能够更好地看到这一点.您无需进行任何复制和粘贴,只需:

  1. 输入"aa",点击返回,输入"bb"
  2. 选择全部并格式化为编号列表
  3. 将光标放在"aa"的末尾并点击返回...

你看到的是一团糟,但你可以看到两个原始数字仍在那里,你通过点击返回开始的新中间列表项目就是所有混乱的地方.

当您点击返回时,文本系统必须重新编号列表项,因为您刚刚插入了一个新项.首先,事实证明它执行"重新编号"即使它是一个项目符号列表,这就是为什么你看到你的例子中的混乱.其次,它通过从列表的开头开始重新编号并重新编号每个列表项并为刚刚创建的项插入新编号来进行重新编号.

Objective-C中的过程

如果您将Swift代码转换为等效的Objective-C并进行跟踪,则可以观察该过程.从...开始:

1) aa
2) bb
Run Code Online (Sandbox Code Playgroud)

内部缓冲区是这样的:

\t1)\taa\n\t2)\tbb
Run Code Online (Sandbox Code Playgroud)

首先插入返回:

\t1)\taa\n\n\t2)\tbb
Run Code Online (Sandbox Code Playgroud)

然后_reformListAtIndex:调用一个内部例程,它开始"重新编号".首先,它取代\t1)\t\t1)-数量并没有改变.然后它\t2)\t在两个新行之间插入,因为此时我们有:

\t1)\taa\n\t2)\t\n\t2)\tbb
Run Code Online (Sandbox Code Playgroud)

然后它取代了原有\t2)\t\t3)\t,并提供:

\t1)\taa\n\t2)\t\n\t3)\tbb
Run Code Online (Sandbox Code Playgroud)

它的工作已经完成.所有这些替换都基于指定要替换的字符范围,插入使用长度为0的范围,并通过:

- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
Run Code Online (Sandbox Code Playgroud)

在Swift中被替换为:

override func replaceCharacters(in range: NSRange, with str: String)
Run Code Online (Sandbox Code Playgroud)

斯威夫特的过程

在Objective-C字符串中有引用语义,更改字符串和代码的所有部分,并引用字符串查看更改.在Swift中,字符串具有值语义,并且在传递给函数等时会复制字符串(至少在概念上).如果在被调用函数中更改了副本,则调用者将不会在其副本中看到该更改.

文本系统是用(或用于)Objective-C编写的,可以合理地假设它可以利用引用语义.当你用Swift替换它的部分代码时,Swift代码必须做一点跳舞,在列表重新编号阶段,当replaceCharacters()调用堆栈时,它看起来像:

#0  0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1  0x0000000100003a00 in @objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2  0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3  0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4  0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()
Run Code Online (Sandbox Code Playgroud)

第4帧是在命中返回时调用的Objective-C代码,在插入新行之后调用内部例程_reformListAtIndex:#3帧进行重新编号.这将调用帧#2中的另一个Objective-C例程,它反过来调用帧#1,它认为是Objective-C方法replaceCharactersInRange:withString:,但实际上是Swift替换.这个替换做了一点点舞蹈,将Objective-C引用语义字符串转换为Swift值语义字符串,然后调用#0帧,即Swift replaceCharacters().

跳舞很难

如果您跟踪过,就像你做的Objective-C的转换时,重新编号得到改变原来的阶段,你的银行代码\t2)\t\t3)\t你会看到一个失误,原始给定的范围\t2)\t是什么,是在之前的新\t2)\t插在上一步(即它被4个位置关闭)...然后你最后得到了一个混乱和一些舞步,之后代码崩溃了一个字符串引用错误,因为索引都是错误的.

这表明Objective-C代码依赖于引用语义,而Swift舞蹈的编排者将引用转换为值并返回引用语义已经无法满足Objective-C代码的期望:所以无论是Objective-C代码,或者一些已经替换它的Swift代码,计算\t2)\t它在字符串上执行的原始范围,该字符串没有被先前插入的新内容所改变\t2)\t.

困惑?好舞会有时让你头晕;-)

固定?

编写NSTextStorageObjective-C中的子类,然后转到bugreport.apple.com并报告错误.

HTH(超过它让你晕眩)