Sources/OrgCore/Commands/TableShrink.swift
153 lines · 7285 bytes
1import Foundation
2
3/// Narrowed table columns (`org-table-shrink`, `org-table-toggle-column-width`, Org 9.8.7):
4/// where org puts its overlays, as ranges of text shown as other text. The editor hides each
5/// range and draws its display instead.
6public enum TableShrink {
7 public struct Segment: Sendable, Equatable {
8 public let range: Range<Int>
9 /// What shows instead of the range's text.
10 public let display: String
11 }
12
13 static let indicator = "…"
14
15 /// The table holding `offset`: its start and end.
16 public static func table(at offset: Int, in text: String) -> Range<Int>? {
17 let buffer = EmacsBuffer(text, point: offset)
18 guard buffer.atTable else { return nil }
19 return buffer.tableBegin()..<buffer.tableEnd()
20 }
21
22 /// The column at `offset`, or nil before the first or after the last.
23 public static func column(at offset: Int, in text: String) -> Int? {
24 let buffer = EmacsBuffer(text, point: offset)
25 let outside = buffer.saveExcursion { () -> Bool in
26 buffer.skipBackwardNot([124, 10])
27 return buffer.bolp || buffer.looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil
28 }
29 return outside ? nil : buffer.tableCurrentColumn()
30 }
31
32 /// The columns with a width cookie, as `org-table-shrink` finds them.
33 public static func cookieColumns(_ table: Range<Int>, in text: String) -> [Int] {
34 let buffer = EmacsBuffer(text, point: table.lowerBound)
35 var columns: [Int] = []
36 while let match = buffer.searchForward("\\|[ \\t]*<[lrc]?[0-9]+>[ \\t]*(\\||$)", bound: table.upperBound, caseInsensitive: false) {
37 buffer.point = match[1]!.lowerBound
38 let column = buffer.tableCurrentColumn()
39 if !columns.contains(column) { columns.append(column) }
40 }
41 return columns.sorted()
42 }
43
44 /// `org-table--read-column-selection`.
45 public static func columns(_ selection: String, max: Int) -> [Int] {
46 var result: Set<Int> = []
47 for part in selection.split(whereSeparator: \.isWhitespace) {
48 if let m = part.wholeMatch(of: /([0-9]+)?-([0-9]+)?/) {
49 let low = m.1.flatMap { Int($0) } ?? 1
50 let high = m.2.flatMap { Int($0) } ?? max
51 if low <= high { result.formUnion(low...high) }
52 } else if let n = Int(part) {
53 result.insert(n)
54 }
55 }
56 return result.sorted()
57 }
58
59 /// `org-table--shrink-columns` for one column.
60 public static func segments(_ table: Range<Int>, column c: Int, in text: String) -> [Segment] {
61 let buffer = EmacsBuffer(text, point: table.lowerBound)
62 var align: String?
63 var width: Int?
64 var fields: [(start: Int, end: Int, contents: String?)] = []
65 while buffer.point < table.upperBound {
66 let lineEnd = buffer.lineEnd(buffer.point)
67 let hline = buffer.lookingAt("[ \\t]*\\|-") != nil
68 let separator: unichar = hline ? 43 : 124
69 var ok = buffer.searchForward("\\|", bound: lineEnd, caseInsensitive: false) != nil
70 var remaining = c - 1
71 while ok, remaining > 0 {
72 guard let next = (buffer.point..<lineEnd).first(where: { buffer.character(at: $0) == separator }) else { ok = false; break }
73 buffer.point = next + 1
74 remaining -= 1
75 }
76 if ok {
77 let start = buffer.point
78 var end = start
79 while end < lineEnd, let ch = buffer.character(at: end), ch != 124, ch != separator { end += 1 }
80 let contents: String? = hline ? nil : buffer.substring(start, end).trimmingCharacters(in: CharacterSet(charactersIn: " \t\n\r"))
81 fields.append((start, end, contents))
82 if let contents, let m = contents.wholeMatch(of: /<([lrc])?([0-9]+)>/) {
83 if align == nil { align = m.1.map(String.init) }
84 if width == nil { width = Int(m.2) }
85 }
86 }
87 if buffer.forwardLine(1) != 0 { break }
88 }
89 return fields.flatMap { shrinkField(width: width ?? 0, align: align ?? "l", start: $0.start, end: $0.end, contents: $0.contents, buffer: buffer) }
90 }
91
92 /// `org-table--shrink-field`; nil contents for a rule.
93 static func shrinkField(width: Int, align: String, start: Int, end: Int, contents: String?, buffer: EmacsBuffer) -> [Segment] {
94 func overlay(_ s: Int, _ e: Int, _ display: String, pre: Bool = false) -> Segment {
95 Segment(range: s..<e, display: pre ? " " + display : display + indicator)
96 }
97 func stringWidth(_ s: Int, _ e: Int) -> Int { buffer.cellWidth(buffer.substring(s, e)) }
98 guard start != end else { return [] }
99 if width == 0 { return [overlay(start, end, "")] }
100 guard let contents else { return [overlay(start, end, String(repeating: "-", count: width + 1))] }
101 if contents.isEmpty {
102 let w = stringWidth(start, end)
103 let full = width + 2
104 return w <= full ? [overlay(end - 1, end, String(repeating: " ", count: full - w))] : [overlay(end - (w - full) - 1, end, "")]
105 }
106 var lead = 0
107 while start + lead < end, buffer.character(at: start + lead) == 32 { lead += 1 }
108 var trail = 0
109 while end - trail > start + lead, buffer.character(at: end - trail - 1) == 32 { trail += 1 }
110 let contentsWidth = stringWidth(start + lead, end - trail)
111 if width <= contentsWidth {
112 let pre = lead > 0 ? overlay(start, start + lead, "", pre: true) : nil
113 let begin = start + lead
114 var lower = begin
115 var upper = end - 1
116 let target = lead == 0 ? width + 1 : width
117 var cut = upper
118 while upper - lower > 1 {
119 let mean = (lower >> 1) + (upper >> 1) + (lower & upper & 1)
120 let w = stringWidth(begin, mean)
121 if w == target {
122 cut = mean
123 break
124 } else if w > target {
125 upper = mean
126 } else {
127 lower = mean
128 }
129 cut = upper
130 }
131 return [pre, overlay(cut, end, "")].compactMap { $0 }
132 }
133 let required = width - contentsWidth
134 let before: Int = lead == 0 ? -1 : align == "r" ? required : align == "c" ? required / 2 : 0
135 let after = required - before
136 var result: [Segment] = []
137 if lead != 0, lead - 1 != before {
138 if lead - 1 > before {
139 result.append(overlay(start, start + (lead - before), "", pre: true))
140 } else {
141 result.append(overlay(start, start + 1, String(repeating: " ", count: before - (lead - 1)), pre: true))
142 }
143 }
144 if trail - 1 == after {
145 result.append(overlay(end - 1, end, ""))
146 } else if trail - 1 > after {
147 result.append(overlay(after + (end - trail), end, ""))
148 } else {
149 result.append(overlay(end - 1, end, String(repeating: " ", count: after - (trail - 1))))
150 }
151 return result
152 }
153}