子类化 NSTextStorage 中断列表编辑
Subclassing NSTextStorage breaks list editing
我有一个带有标准 NSTextView
的基本 Mac 应用程序。我正在尝试实现和使用 NSTextStorage
的子类,但即使是非常基本的实现也会破坏列表编辑行为:
- 我添加了一个包含两项的项目符号列表
- 我将该列表进一步复制并粘贴到文档中
- 在粘贴的列表中按 Enter 键会破坏最后一个列表项的格式设置。
这是一个简短的视频:
两期:
- 粘贴列表的项目符号使用较小的字体
- 在第二个列表项中断第三个项目后按 Enter
当我不替换文本存储时,这工作正常。
这是我的代码:
ViewController.swift
@IBOutlet var textView:NSTextView!
override func viewDidLoad() {
[...]
textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
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()
}
}
您在 Swift 中发现了一个错误(可能不只是在 Swift 库中,可能在更基础的东西中)。
这是怎么回事?
如果您创建编号列表而不是项目符号列表,您将能够更好地看到这一点。您不需要做任何复制和粘贴,只需:
- 键入 "aa",点击 return,键入 "bb"
- 全部执行 select 并格式化为编号列表
- 将光标放在 "aa" 的末尾并点击 return...
你看到的是一团糟,但你可以看到原来的两个数字仍然存在,而你通过点击 return 开始的新的中间列表项就是所有乱七八糟的地方。
当您点击 return 时,文本系统必须对列表项重新编号,因为您刚刚插入了一个新项。首先,事实证明它执行此 "renumbering" 即使它是一个项目符号列表,这就是为什么您在示例中看到混乱的原因。其次,它通过从列表的开头开始重新编号 每个 列表项并为刚刚创建的项目插入一个新编号来进行重新编号。
Objective-C
中的进程
如果您将 Swift 代码翻译成等效的 Objective-C 并进行跟踪,您可以观察到该过程。开始于:
1) aa
2) bb
内部缓冲区类似于:
\t1)\taa\n\t2)\tbb
首先插入 return:
\t1)\taa\n\n\t2)\tbb
然后调用内部例程 _reformListAtIndex:
并启动 "renumbering"。首先,它将 \t1)\t
替换为 \t1)
- 数字没有改变。然后它在两个新行之间插入 \t2)\t
,此时我们有:
\t1)\taa\n\t2)\t\n\t2)\tbb
然后用 \t3)\t
替换原来的 \t2)\t
给出:
\t1)\taa\n\t2)\t\n\t3)\tbb
它的工作完成了。所有这些替换都是基于指定替换字符的范围,插入使用长度为0的范围,经过:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
其中 Swift 被替换为:
override func replaceCharacters(in range: NSRange, with str: String)
Swift
中的进程
在 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:] ()
Frame #4 是在 return 被命中时调用的 Objective-C 代码,插入换行符后它调用内部例程 _reformListAtIndex:
,frame #3,进行重新编号。这会调用框架 #2 中的另一个 Objective-C 例程,该例程又调用框架 #1,它认为是 Objective-C 方法 replaceCharactersInRange:withString:
,但实际上是 Swift替换。这个替换做了一点舞蹈,将 Objective-C 引用语义字符串转换为 Swift 值语义字符串,然后调用帧 #0,Swift replaceCharacters()
.
跳舞很难
如果当重新编号进入将原始 \t2)\t
更改为 \t3)\t
的阶段时,您像执行 Objective-C 翻译一样跟踪您的 Swift 代码,您将看到一个失误,为原始 \t2)\t
给出的范围是 在 之前的范围,新的 \t2)\t
在上一步中插入(即它关闭了4个位置)......你最终会一团糟,然后再跳几个舞步代码崩溃并出现字符串引用错误,因为索引都是错误的。
这表明Objective-C代码依赖于引用语义,而Swift舞蹈的编舞者将引用转换为值并返回引用语义未能满足Objective-C 代码的期望:因此,当 Objective-C 代码或替换它的某些 Swift 代码计算原始 \t2)\t
的范围时,它是在尚未计算的字符串上这样做的由之前插入的新 \t2)\t
.
改变
迷茫?嗯,跳舞有时会让你头晕;-)
修复?
在 Objective-C 中编写 NSTextStorage
的子类,然后转到 bugreport.apple.com
并报告错误。
HTH(不止让你头晕)
我有一个带有标准 NSTextView
的基本 Mac 应用程序。我正在尝试实现和使用 NSTextStorage
的子类,但即使是非常基本的实现也会破坏列表编辑行为:
- 我添加了一个包含两项的项目符号列表
- 我将该列表进一步复制并粘贴到文档中
- 在粘贴的列表中按 Enter 键会破坏最后一个列表项的格式设置。
这是一个简短的视频:
两期:
- 粘贴列表的项目符号使用较小的字体
- 在第二个列表项中断第三个项目后按 Enter
当我不替换文本存储时,这工作正常。
这是我的代码:
ViewController.swift
@IBOutlet var textView:NSTextView!
override func viewDidLoad() {
[...]
textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
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()
}
}
您在 Swift 中发现了一个错误(可能不只是在 Swift 库中,可能在更基础的东西中)。
这是怎么回事?
如果您创建编号列表而不是项目符号列表,您将能够更好地看到这一点。您不需要做任何复制和粘贴,只需:
- 键入 "aa",点击 return,键入 "bb"
- 全部执行 select 并格式化为编号列表
- 将光标放在 "aa" 的末尾并点击 return...
你看到的是一团糟,但你可以看到原来的两个数字仍然存在,而你通过点击 return 开始的新的中间列表项就是所有乱七八糟的地方。
当您点击 return 时,文本系统必须对列表项重新编号,因为您刚刚插入了一个新项。首先,事实证明它执行此 "renumbering" 即使它是一个项目符号列表,这就是为什么您在示例中看到混乱的原因。其次,它通过从列表的开头开始重新编号 每个 列表项并为刚刚创建的项目插入一个新编号来进行重新编号。
Objective-C
中的进程如果您将 Swift 代码翻译成等效的 Objective-C 并进行跟踪,您可以观察到该过程。开始于:
1) aa
2) bb
内部缓冲区类似于:
\t1)\taa\n\t2)\tbb
首先插入 return:
\t1)\taa\n\n\t2)\tbb
然后调用内部例程 _reformListAtIndex:
并启动 "renumbering"。首先,它将 \t1)\t
替换为 \t1)
- 数字没有改变。然后它在两个新行之间插入 \t2)\t
,此时我们有:
\t1)\taa\n\t2)\t\n\t2)\tbb
然后用 \t3)\t
替换原来的 \t2)\t
给出:
\t1)\taa\n\t2)\t\n\t3)\tbb
它的工作完成了。所有这些替换都是基于指定替换字符的范围,插入使用长度为0的范围,经过:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
其中 Swift 被替换为:
override func replaceCharacters(in range: NSRange, with str: String)
Swift
中的进程在 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:] ()
Frame #4 是在 return 被命中时调用的 Objective-C 代码,插入换行符后它调用内部例程 _reformListAtIndex:
,frame #3,进行重新编号。这会调用框架 #2 中的另一个 Objective-C 例程,该例程又调用框架 #1,它认为是 Objective-C 方法 replaceCharactersInRange:withString:
,但实际上是 Swift替换。这个替换做了一点舞蹈,将 Objective-C 引用语义字符串转换为 Swift 值语义字符串,然后调用帧 #0,Swift replaceCharacters()
.
跳舞很难
如果当重新编号进入将原始 \t2)\t
更改为 \t3)\t
的阶段时,您像执行 Objective-C 翻译一样跟踪您的 Swift 代码,您将看到一个失误,为原始 \t2)\t
给出的范围是 在 之前的范围,新的 \t2)\t
在上一步中插入(即它关闭了4个位置)......你最终会一团糟,然后再跳几个舞步代码崩溃并出现字符串引用错误,因为索引都是错误的。
这表明Objective-C代码依赖于引用语义,而Swift舞蹈的编舞者将引用转换为值并返回引用语义未能满足Objective-C 代码的期望:因此,当 Objective-C 代码或替换它的某些 Swift 代码计算原始 \t2)\t
的范围时,它是在尚未计算的字符串上这样做的由之前插入的新 \t2)\t
.
迷茫?嗯,跳舞有时会让你头晕;-)
修复?
在 Objective-C 中编写 NSTextStorage
的子类,然后转到 bugreport.apple.com
并报告错误。
HTH(不止让你头晕)