Text with line numbers in TextKit 2

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

Replies

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!