You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NSTextView中提取行号与光标字符位置?

实现文本编辑器的光标行号与字符偏移量展示功能

开发带有文本编辑器的应用时,向用户展示光标所在的行号和字符偏移量会很实用。以下是实现该功能的示例方法:

/// 获取光标所在的行号与列偏移
/// 查找indexStarts中第一个大于光标位置的行起始索引,计算光标与上一行起始位置的距离
func setColumnAndRow() {
    // 将NSRange转换为Range<String.Index>
    let selectionRange = Range(selectedRange, in: string)!

    // 找到第一个大于光标位置的行起始索引
    if let nextIndex = indexStarts.firstIndex(
                                where: {$0 > selectionRange.lowerBound}
    ) {
        // 光标所在行是该索引的前一行
        let lineIndex = nextIndex - 1
        // 使用<String.Index>.distance计算列偏移
        let distance = string.distance(from: indexStarts[lineIndex]
                                       , to: selectionRange.lowerBound
                           )
        print("column: \(distance), row: \(lineIndex)")
    } else {
        print("column: 0, row: \(indexStarts.count-1)")
    }
}

经调研,Apple并未提供此功能的官方API,甚至Xcode编辑器本身也没有该功能。因此我们需要构建一个存储每行起始字符位置的数组(如上述代码中的indexStarts),且该数组需在NSTextField内容发生任何变化时更新,所以生成该数组的方法必须高效快速。

我整理了四种生成行起始位置数组的方法:

方法一

使用glyphs数量与lineFragmentRect——目前最慢的实现

func lineStartsWithLayout() -> [Int] {
        // 比后续方法慢约100倍
        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] {
    // 比方法一快约100倍
    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——原本预期速度很快,但实际比方法二慢近一倍,不过代码风格更符合Swift特性。

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] {
    /*
     // 将Range<String.Index>转换为NSRange:
     let range   = s[s.startIndex..<s.endIndex]
     let nsRange = NSRange(range, in: s)
     
     // 将NSRange转换为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. lineStartsWithLayout1.452 s
2. lineStartsWithParagraphs0.020 s
3. lineStartsByEnumerating0.065 s
4. indexStartsByLineRange0.019 s

我更倾向于使用indexStartsByLineRange方法,也欢迎其他意见。在Objective-C中,建议采用lineStartsWithParagraphs的算法,只需调整部分调用即可。

内容的提问来源于stack exchange,提问作者Lego Esprit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 14:07:03