构建一个显示文本编辑器的应用程序,将光标位置作为行和偏移量传达给用户会很好。这是执行此操作的示例方法。
/// Find row and column of cursor position
/// Checks indexStarts for the index of the start larger than the selected position and
/// calculates the distance between cursor position and the previous line start.
func setColumnAndRow() {
// Convert NSRange to Range<String.Index>
let selectionRange = Range(selectedRange, in: string)!
// Retrieve the first start line greater than the cursor position
if let nextIndex = indexStarts.firstIndex(
where: {$0 > selectionRange.lowerBound}
) {
// The line with the cursor was one before that
let lineIndex = nextIndex - 1
// Use the <String.Index>.distance to determine the column position
let distance = string.distance(from: indexStarts[lineIndex]
, to: selectionRange.lowerBound
)
print("column: \(distance), row: \(lineIndex)")
} else {
print("column: 0, row: \(indexStarts.count-1)")
}
}
根据我的研究,苹果并没有为此提供任何API,事实上这甚至不是Xcode编辑器的一个功能。最后,我需要为上面使用的每行开始建立一个字符位置数组。每当NSTextField中发生任何更改时,都必须更新此数组。因此,该列表的生成必须非常有效和快速。
我发现/组装了四种方法来生成行开始数组:
第一种方法
使用glyphs和lineFragmentRect的数量-这是目前最慢的实现
func lineStartsWithLayout() -> [Int] {
// about 100 times slower than below
let start = ProcessInfo.processInfo.systemUptime
var lineStarts:[Int] = []
let layoutManager = layoutManager!
let numberOfGlyphs = layoutManager.numberOfGlyphs
var lineRange: NSRange = NSRange()
var indexOfGlyph: Int = 0
lineStarts.append(indexOfGlyph)
while indexOfGlyph < numberOfGlyphs {
layoutManager.lineFragmentRect(
forGlyphAt: indexOfGlyph
, effectiveRange: &lineRange
, withoutAdditionalLayout: false
)
indexOfGlyph = NSMaxRange(lineRange)
lineStarts.append(indexOfGlyph)
}
lineStarts.append(Int.max)
Logger.write("\(ProcessInfo.processInfo.systemUptime-start) s")
return lineStarts
}
第二种方法
使用paragraphs数组作为单独的行长度-根据Apple的说法,可能不建议使用,因为它可能会产生大量的对象。这里很可能不是这种情况,因为我们只是阅读段落数组,我们没有对它进行任何修改。实际上几乎与最快的实现一样快。如果你使用Objective-C。
func lineStartsWithParagraphs() -> [Int] {
// about 100 times faster than above
let start = ProcessInfo.processInfo.systemUptime;
var lineStarts:[Int] = []
var lineStart = 0
lineStarts = []
lineStarts.append(lineStart)
for p in textStorage?.paragraphs ?? [] {
lineStart += p.length
lineStarts.append(lineStart)
}
lineStarts.append(Int.max)
Logger.write("\(ProcessInfo.processInfo.systemUptime-start) s")
return lineStarts
}
第三种方法
使用enumerateLines-预期非常快,但实际上比lineStartsWithParagraphs慢近两倍,但相当迅速。
func lineStartsByEnumerating() -> [Int] {
let start = ProcessInfo.processInfo.systemUptime;
var lineStarts:[Int] = []
var lineStart = 0
lineStarts = []
lineStarts.append(lineStart)
string.enumerateLines {
line, stop in
lineStart += line.count
lineStarts.append(lineStart)
}
lineStarts.append(Int.max)
Logger.write("\(ProcessInfo.processInfo.systemUptime-start) s")
return lineStarts
}
第四种方法
- 使用Swift中的lineRange-Swift中最快也可能是最好的实现。不能在Objective-C中使用。使用起来有点复杂,例如NSTextView.selectedRange返回一个NSRange,因此必须转换为Range<String.Index>。*
func indexStartsByLineRange() -> [String.Index] {
/*
// Convert Range<String.Index> to NSRange:
let range = s[s.startIndex..<s.endIndex]
let nsRange = NSRange(range, in: s)
// Convert NSRange to Range<String.Index>:
let nsRange = NSMakeRange(0, 4)
let range = Range(nsRange, in: s)
*/
let start = ProcessInfo.processInfo.systemUptime;
var indexStarts:[String.Index] = []
var index = string.startIndex
indexStarts.append(index)
while index != string.endIndex {
let range = string.lineRange(for: index..<index)
index = range.upperBound
indexStarts.append(index)
}
Logger.write("\(ProcessInfo.processInfo.systemUptime-start) s")
return indexStarts
}
基准:
| 方法|使用Ventura的M2上的32000行NSTextView的时间|
| - -----|- -----|
| 1. lineStartsWithLayout| 1.452秒|
| 2. lineStartsWithParagrams| 0.020秒|
| 3. lineStartsBy枚举|0.065秒|
| 4. indexStartsByLineRange| 0.019秒|
我更喜欢indexStartsByLineRange,但我有兴趣听听其他的意见在Objective-C中,我会坚持lineStartsWithParagraphs中的算法,考虑到一些调用必须适应。
1条答案
按热度按时间pb3s4cty1#
由Willeke的评论触发,我检查了不同的行号和光标位置计算的可能性,结果令我惊讶。
请注意这些方法之间的微小差异,但这是获得相同结果的唯一方法,除了使用reduce的方法有时会对某些文本产生太小的行号。奇怪的是,使用RegularExpression是最快的。对于32000行的文本,它总是在10 ms以下。
请注意不要使用.newline表示“\n”,因为这样会使“\a”和“\n”的行数增加一倍。
| 方法|基准|评论|
| - -----|- -----|- -----|
| RegEx| 0.006秒||
| 扫描器|0.036秒||
| 组件|0.038秒||
| 列举|0.028秒||
| 减少|0.132秒|故障|
所以对我来说,答案是,使用正则表达式,这是如此之快,以至于可能不需要保持一个行开始数组。