Sources/OrgEditorAppKit/LineNumbers.swift
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