Text with line numbers in TextKit 2

What is the recommended approach to rendering text with line numbers in TextKit 2?

I don't know if this is recommended, however since nobody picked up this questions, this is what I do:

I assume you use NSRulerView as an NSScrollView.verticalRulerView property. What I do, is override drawHashMarksAndLabels(in:) and use CoreText to draw numbers in the positions from NSTextLayoutManager

class LineNumberRulerView: NSRulerView {
    private weak var textView: NSTextView?

    init(textView: NSTextView) {
        self.textView = textView
        super.init(scrollView: textView.enclosingScrollView!, orientation: .verticalRuler)
        clientView = textView.enclosingScrollView!.documentView

        NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: textView, queue: nil) { [weak self] _ in
            self?.needsDisplay = true
        }

        NotificationCenter.default.addObserver(forName: NSText.didChangeNotification, object: textView, queue: nil) { [weak self] _ in
            self?.needsDisplay = true
        }
    }
   
    public override func drawHashMarksAndLabels(in rect: NSRect) {
        guard let context = NSGraphicsContext.current?.cgContext,
              let textView = textView,
              let textLayoutManager = textView.textLayoutManager
        else {
            return
        }

        let relativePoint = self.convert(NSZeroPoint, from: textView)

        context.saveGState()
        context.textMatrix = CGAffineTransform(scaleX: 1, y: isFlipped ? -1 : 1)

        let attributes: [NSAttributedString.Key: Any] = [
            .font: textView.font!,
            .foregroundColor: NSColor.secondaryLabelColor
        ]

        var lineNum = 1
        textLayoutManager.enumerateTextLayoutFragments(from: nil, options: .ensuresLayout) { fragment in
            let fragmentFrame = fragment.layoutFragmentFrame

            for (subLineIdx, textLineFragment) in fragment.textLineFragments.enumerated() where subLineIdx == 0 {
                let locationForFirstCharacter = textLineFragment.locationForCharacter(at: 0)
                let ctline = CTLineCreateWithAttributedString(CFAttributedStringCreate(nil, "\(lineNum)" as CFString, attributes as CFDictionary))
                context.textPosition = fragmentFrame.origin.applying(.init(translationX: 4, y: locationForFirstCharacter.y + relativePoint.y))
                CTLineDraw(ctline, context)
            }

            lineNum += 1
            return true
        }

        context.restoreGState()
    }
}

Hi krzyzanowskim,

thanks for the answer, It worked fine for me. It encounters correctly line wraps due to the width of the view and scrolling works as well.

Thanks very much!

Hi krzyzanowskim,

Thanks for posting this. I was struggling to make the right associations between the notions of text storage vs. layout fragments, redraw notifications, etc., and also hadn't thought of using an NSRulerView for the numbers. Your approach here is much more concise and easy to implement than the one I was pursuing, and provided a lot of clarity.

Text with line numbers in TextKit 2
 
 
Q