krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgEditorAppKit/LineNumbers.swift

b98a6509c5ae75e5172dd333d1b6105fd6ceee0e
orgstar/Sources/OrgEditorAppKit/LineNumbers.swift history · blame · raw

50 lines · 2631 bytes

2 symbols in this file
 1#if os(macOS)
 2import AppKit
 3
 4/// Line numbers in a margin left of the text, as `display-line-numbers-mode` shows them: one
 5/// per line of the file (folded lines skipped, wrapped lines numbered once), the caret's line
 6/// bright. Drawn by the text view itself: an `NSRulerView` stops TextKit 2 from drawing text.
 7extension OrgTextView {
 8    /// Width of the margin for the largest line number, or 0 when numbers are off.
 9    func updateGutter() {
10        var width: CGFloat = 0
11        if showsLineNumbers {
12            let lines = lineIndex?.lineCount ?? 1
13            let digits = max(2, String(lines).count)
14            width = ceil(("0" as NSString).size(withAttributes: [.font: lineNumberFont]).width * CGFloat(digits) + 16)
15        }
16        if textContainerInset.width != width {
17            textContainerInset = NSSize(width: width, height: textContainerInset.height)
18            needsDisplay = true
19        }
20    }
21
22    func drawLineNumbers(in rect: NSRect) {
23        guard showsLineNumbers, let layout = textLayoutManager, let content = layout.textContentManager else { return }
24        let gutter = textContainerInset.width
25        guard rect.minX < gutter else { return }
26        let text = string as NSString
27        let caretLine = text.lineRange(for: NSRange(location: min(selectedRange().location, text.length), length: 0)).location
28        let origin = textContainerOrigin
29        let documentStart = content.documentRange.location
30        guard let start = layout.textLayoutFragment(for: CGPoint(x: 0, y: max(0, rect.minY - origin.y)))?.rangeInElement.location
31            ?? layout.textViewportLayoutController.viewportRange?.location else { return }
32
33        layout.enumerateTextLayoutFragments(from: start, options: []) { fragment in
34            let frame = fragment.layoutFragmentFrame
35            if frame.minY + origin.y > rect.maxY { return false }
36            let offset = content.offset(from: documentStart, to: fragment.rangeInElement.location)
37            guard offset <= text.length else { return false }
38            let line = lineIndex?.line(of: offset) ?? 1
39            let label = NSAttributedString(string: "\(line)", attributes: [
40                .font: lineNumberFont,
41                .foregroundColor: offset == caretLine ? currentLineNumberColor : lineNumberColor,
42            ])
43            let lineHeight = fragment.textLineFragments.first?.typographicBounds.height ?? frame.height
44            let size = label.size()
45            label.draw(at: NSPoint(x: gutter - size.width - 8, y: frame.minY + origin.y + (lineHeight - size.height) / 2))
46            return true
47        }
48    }
49}
50#endif