Sources/OrgCore/Commands/VisibleText.swift
104 lines · 4707 bytes
5 symbols in this file
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 {
58 // `org-fontify-entities`: a single-character entity is composed into one glyph.
59 // Positions inside it stay at its starting column; its width counts at its end.
60 for run in PrettyEntities.runs(in: line, subSuperscriptsNeedBraces: options.subSuperscriptsNeedBraces) {
61 switch run {
62 case .entity(let range, let display):
63 for i in range where i < widths.count { widths[i] = 0 }
64 widths[range.upperBound - 1] = displayWidth(display)
65 case .script(let markers, _, _):
66 for marker in markers { hide(marker) }
67 }
68 }
69 }
70 }
71
72 private mutating func hide(_ range: Range<Int>) {
73 for i in range where i >= 0 && i < hidden.count { hidden[i] = true }
74 }
75
76 private func advance(_ column: Int, _ i: Int) -> Int {
77 widths[i] < 0 ? (column / 8 + 1) * 8 : column + widths[i]
78 }
79
80 /// Display column at UTF-16 `offset`.
81 func column(at offset: Int) -> Int {
82 var column = 0
83 for i in 0..<min(offset, widths.count) where starts[i] && !hidden[i] {
84 column = advance(column, i)
85 }
86 return column
87 }
88
89 var width: Int { column(at: widths.count) }
90
91 /// `move-to-column`: hidden text is skipped before the goal test, so point ends past
92 /// markup that sits at the goal column.
93 func offset(ofColumn target: Int) -> Int {
94 var column = 0
95 var i = 0
96 while true {
97 while i < widths.count, hidden[i] || !starts[i] { i += 1 }
98 if column >= target || i >= widths.count { break }
99 column = advance(column, i)
100 i += 1
101 }
102 return i
103 }
104}