Sources/OrgCore/Commands/VisibleText.swift
112 lines · 5540 bytes
1import Foundation
2
3/// How wide each character of a line is as Emacs displays Org text: bracket links show only
4/// their description (or path), and with the options, emphasis markers are hidden and entities
5/// and sub/superscripts show as their characters. Emacs's `current-column` counts only what is
6/// visible, so tag alignment, filling and table widths all depend on this.
7struct VisibleLine {
8 /// Display width of the character starting at each UTF-16 offset; -1 for a tab.
9 private var widths: [Int]
10 /// Whether a character starts at each offset (false for the second half of a pair).
11 private var starts: [Bool]
12 private var hidden: [Bool]
13
14 /// The line at `range` (UTF-16) inside `context`, which holds the lines around it so that
15 /// links spanning lines are seen whole.
16 init(_ context: String, line range: Range<Int>, options: EditingOptions, settings: OrgSettings = .default) {
17 let whole = VisibleLine(context, options: options, settings: settings)
18 widths = Array(whole.widths[range])
19 starts = Array(whole.starts[range])
20 hidden = Array(whole.hidden[range])
21 }
22
23 init(_ line: String, options: EditingOptions, settings: OrgSettings = .default) {
24 let count = line.utf16.count
25 widths = [Int](repeating: 0, count: count)
26 starts = [Bool](repeating: false, count: count)
27 hidden = [Bool](repeating: false, count: count)
28 var index = 0
29 for character in line {
30 widths[index] = character == "\t" ? -1 : displayWidth(of: character)
31 starts[index] = true
32 index += String(character).utf16.count
33 }
34 let needsLinks = line.contains("[[")
35 let needsEmphasis = options.hideEmphasisMarkers && line.contains(where: { "*/_+=~".contains($0) })
36 if needsLinks || needsEmphasis {
37 let tree = OrgParser.parse(line + "\n", defaults: settings)
38 for node in tree.root.descendants() {
39 switch node.kind {
40 case .link where node.text.hasPrefix("[["):
41 if let description = node.firstChild(.linkDescription) {
42 hide(node.range.lowerBound..<description.range.lowerBound)
43 hide(description.range.upperBound..<node.range.upperBound)
44 } else {
45 hide(node.range.lowerBound..<(node.range.lowerBound + 2))
46 hide((node.range.upperBound - 2)..<node.range.upperBound)
47 }
48 case .bold, .italic, .underline, .strikeThrough, .verbatim, .code:
49 guard options.hideEmphasisMarkers else { break }
50 hide(node.range.lowerBound..<(node.range.lowerBound + 1))
51 hide((node.range.upperBound - 1)..<node.range.upperBound)
52 default:
53 break
54 }
55 }
56 }
57 if options.prettyEntities, line.contains(where: { $0 == "\\" || $0 == "_" || $0 == "^" }) {
58 let ns = line as NSString
59 let all = NSRange(location: 0, length: ns.length)
60 // `org-fontify-entities`: a single-character entity is composed into one glyph.
61 // Positions inside it stay at its starting column; its width counts at its end.
62 let entity = try! NSRegularExpression(pattern: "\\\\(there4|sup[123]|frac[13][24]|[a-zA-Z]+)($|\\{\\}|[^\\p{L}\\n])")
63 for match in entity.matches(in: line, range: all) {
64 guard let display = OrgEntities.display[ns.substring(with: match.range(at: 1))], display.count == 1 else { continue }
65 let end = ns.substring(with: match.range(at: 2)) == "{}" ? NSMaxRange(match.range(at: 2)) : NSMaxRange(match.range(at: 1))
66 for i in match.range.location..<end where i < widths.count { widths[i] = 0 }
67 widths[end - 1] = displayWidth(display)
68 }
69 let pattern = options.subSuperscriptsNeedBraces
70 ? "\\S([_^])(\\{)[^{}\\n]*(\\})"
71 : "\\S([_^])(?:(\\{)[^{}\\n]*(\\})|[*]|[-+]?[^-+*!@#$%^_ \\t\\r\\n,:\"?<>~;./{}=()]+)"
72 for match in try! NSRegularExpression(pattern: pattern).matches(in: line, range: all) {
73 for group in 1...3 where match.range(at: group).location != NSNotFound {
74 hide(match.range(at: group).location..<NSMaxRange(match.range(at: group)))
75 }
76 }
77 }
78 }
79
80 private mutating func hide(_ range: Range<Int>) {
81 for i in range where i >= 0 && i < hidden.count { hidden[i] = true }
82 }
83
84 private func advance(_ column: Int, _ i: Int) -> Int {
85 widths[i] < 0 ? (column / 8 + 1) * 8 : column + widths[i]
86 }
87
88 /// Display column at UTF-16 `offset`.
89 func column(at offset: Int) -> Int {
90 var column = 0
91 for i in 0..<min(offset, widths.count) where starts[i] && !hidden[i] {
92 column = advance(column, i)
93 }
94 return column
95 }
96
97 var width: Int { column(at: widths.count) }
98
99 /// `move-to-column`: hidden text is skipped before the goal test, so point ends past
100 /// markup that sits at the goal column.
101 func offset(ofColumn target: Int) -> Int {
102 var column = 0
103 var i = 0
104 while true {
105 while i < widths.count, hidden[i] || !starts[i] { i += 1 }
106 if column >= target || i >= widths.count { break }
107 column = advance(column, i)
108 i += 1
109 }
110 return i
111 }
112}