krz/orgstar

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

Commit ec4eda648b

ec4eda648b86ce715ab9a31029e3c6e199796d56

parent: d71e190221

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-05 05:43 UTC

Port org tables and visible widths

Layout: unified · split

Sources/OrgCore/Commands/Command.swift +15 −3
@@ -15,10 +15,17 @@ public struct EditingOptions: Sendable, Equatable, Codable {
15 public var logRepeat: Bool 15 public var logRepeat: Bool
16 /// `fill-column` for M-q. 16 /// `fill-column` for M-q.
17 public var fillColumn: Int 17 public var fillColumn: Int
18 /// `org-hide-emphasis-markers`: table alignment measures emphasis without its markers.
19 public var hideEmphasisMarkers: Bool
20 /// `org-pretty-entities`: table alignment measures `\alpha` as `α`.
21 public var prettyEntities: Bool
22 /// `org-use-sub-superscripts` `{}`: only `_{…}` and `^{…}` are sub- and superscripts.
23 public var subSuperscriptsNeedBraces: Bool
18 24
19 public init( 25 public init(
20 tagsColumn: Int = -77, insertHeadingRespectContent: Bool = false, metaReturnMaySplitLine: Bool = true, 26 tagsColumn: Int = -77, insertHeadingRespectContent: Bool = false, metaReturnMaySplitLine: Bool = true,
21 listAllowAlphabetical: Bool = false, logRepeat: Bool = true, fillColumn: Int = 70 27 listAllowAlphabetical: Bool = false, logRepeat: Bool = true, fillColumn: Int = 70,
28 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false
22 ) { 29 ) {
23 self.tagsColumn = tagsColumn 30 self.tagsColumn = tagsColumn
24 self.insertHeadingRespectContent = insertHeadingRespectContent 31 self.insertHeadingRespectContent = insertHeadingRespectContent
@@ -26,6 +33,9 @@ public struct EditingOptions: Sendable, Equatable, Codable {
26 self.listAllowAlphabetical = listAllowAlphabetical 33 self.listAllowAlphabetical = listAllowAlphabetical
27 self.logRepeat = logRepeat 34 self.logRepeat = logRepeat
28 self.fillColumn = fillColumn 35 self.fillColumn = fillColumn
36 self.hideEmphasisMarkers = hideEmphasisMarkers
37 self.prettyEntities = prettyEntities
38 self.subSuperscriptsNeedBraces = subSuperscriptsNeedBraces
29 } 39 }
30 40
31 /// Org's own defaults. 41 /// Org's own defaults.
@@ -151,11 +161,13 @@ public enum Commands {
151 InsertItem(), InsertCheckboxItem(), IndentItem(), OutdentItem(), IndentItemTree(), OutdentItemTree(), 161 InsertItem(), InsertCheckboxItem(), IndentItem(), OutdentItem(), IndentItemTree(), OutdentItemTree(),
152 MoveItemUp(), MoveItemDown(), ToggleCheckbox(), 162 MoveItemUp(), MoveItemDown(), ToggleCheckbox(),
153 InsertTimestamp(), InsertTimestamp(inactive: true), SetPlanning(deadline: false), SetPlanning(deadline: true), 163 InsertTimestamp(), InsertTimestamp(inactive: true), SetPlanning(deadline: false), SetPlanning(deadline: true),
154 RemovePlanning(deadline: false), RemovePlanning(deadline: true), FillParagraph(), 164 RemovePlanning(deadline: false), RemovePlanning(deadline: true), FillParagraph(), CreateTable(),
155 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false), 165 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false),
156 ] 166 ]
157 167
168 public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) }
169
158 public static func command(_ id: String) -> (any OrgCommand)? { 170 public static func command(_ id: String) -> (any OrgCommand)? {
159 all.first { $0.id == id } 171 everything.first { $0.id == id }
160 } 172 }
161} 173}
Sources/OrgCore/Commands/EmacsBuffer.swift +30 −5
@@ -25,6 +25,7 @@ final class EmacsBuffer {
25 var point: Int 25 var point: Int
26 private var markers: [Marker] = [] 26 private var markers: [Marker] = []
27 let settings: OrgSettings 27 let settings: OrgSettings
28 private var visibleCache: [String: VisibleLine] = [:]
28 /// Groups of the last successful match, as Emacs's match data. 29 /// Groups of the last successful match, as Emacs's match data.
29 var lastMatch: [Range<Int>?] = [] 30 var lastMatch: [Range<Int>?] = []
30 /// The clock and calendar commands see. 31 /// The clock and calendar commands see.
@@ -206,14 +207,38 @@ final class EmacsBuffer {
206 static let blanks: Set<unichar> = [32, 9] 207 static let blanks: Set<unichar> = [32, 9]
207 static let whitespace: Set<unichar> = [32, 9, 10, 13] 208 static let whitespace: Set<unichar> = [32, 9, 10, 13]
208 209
209 var currentColumn: Int { column(of: substring(lineStart(point), point)) } 210 /// Display column of point, counting only what Emacs shows.
211 var currentColumn: Int {
212 let start = lineStart(point)
213 return visibleLine(start).column(at: point - start)
214 }
215
216 /// The display widths of the line starting at `start`, seen with three lines on either
217 /// side so that links spanning lines are whole. Cached by that text.
218 func visibleLine(_ start: Int) -> VisibleLine {
219 let (from, to) = saveExcursion { () -> (Int, Int) in
220 point = start
221 forwardLine(-3)
222 let from = point
223 point = start
224 forwardLine(3)
225 return (from, lineEnd(point))
226 }
227 let context = substring(from, to)
228 let range = (start - from)..<(lineEnd(start) - from)
229 let key = context + "\u{0}\(range.lowerBound)"
230 if let cached = visibleCache[key] { return cached }
231 let line = VisibleLine(context, line: range, options: options, settings: settings)
232 if visibleCache.count > 256 { visibleCache.removeAll() }
233 visibleCache[key] = line
234 return line
235 }
210 236
211 /// `move-to-column`: as far as `target` on this line, or the line's end. With `force`, a 237 /// `move-to-column`: as far as `target` on this line, or the line's end. With `force`, a
212 /// tab spanning `target` becomes spaces (`indent-tabs-mode` nil) so point lands on it. 238 /// tab spanning `target` becomes spaces (`indent-tabs-mode` nil) so point lands on it.
213 func moveToColumn(_ target: Int, force: Bool = false) { 239 func moveToColumn(_ target: Int, force: Bool = false) {
214 let start = lineStart(point) 240 let start = lineStart(point)
215 let line = LineBuffer(substring(start, lineEnd(point)), caret: nil) 241 point = start + visibleLine(start).offset(ofColumn: target)
216 point = start + line.offset(ofColumn: target)
217 guard force, currentColumn > target, character(at: point - 1) == 9 else { return } 242 guard force, currentColumn > target, character(at: point - 1) == 9 else { return }
218 let tabEnd = currentColumn 243 let tabEnd = currentColumn
219 point -= 1 244 point -= 1
@@ -415,14 +440,14 @@ final class EmacsBuffer {
415 let end = lineEnd(offset) 440 let end = lineEnd(offset)
416 let line = substring(start, end) 441 let line = substring(start, end)
417 var plain = LineBuffer(line, caret: nil) 442 var plain = LineBuffer(line, caret: nil)
418 OrgCore.alignTags(&plain, tagsColumn: options.tagsColumn) 443 OrgCore.alignTags(&plain, options: options)
419 guard plain.string != line else { return } 444 guard plain.string != line else { return }
420 func map(_ position: Int) -> Int { 445 func map(_ position: Int) -> Int {
421 guard position >= start, position <= end else { 446 guard position >= start, position <= end else {
422 return position > end ? position + (plain.string as NSString).length - (line as NSString).length : position 447 return position > end ? position + (plain.string as NSString).length - (line as NSString).length : position
423 } 448 }
424 var buffer = LineBuffer(line, caret: position - start) 449 var buffer = LineBuffer(line, caret: position - start)
425 OrgCore.alignTags(&buffer, tagsColumn: options.tagsColumn) 450 OrgCore.alignTags(&buffer, options: options)
426 return start + buffer.caret! 451 return start + buffer.caret!
427 } 452 }
428 let newPoint = map(point) 453 let newPoint = map(point)
Sources/OrgCore/Commands/Fill.swift +5 −1
@@ -34,7 +34,11 @@ extension EmacsBuffer {
34 /// The innermost element (not object) holding `offset`, as `org-element-at-point`. Greater 34 /// The innermost element (not object) holding `offset`, as `org-element-at-point`. Greater
35 /// blocks (quote, center, special) are parsed into their elements. 35 /// blocks (quote, center, special) are parsed into their elements.
36 func element(at offset: Int) -> Element? { 36 func element(at offset: Int) -> Element? {
37 element(at: offset, in: string, base: 0) 37 // Elements never cross a heading, so the section around `offset` is enough to parse.
38 let lineBegin = lineStart(offset)
39 let from = headingLevel(atLine: lineBegin) != nil ? lineBegin : (previousHeadingStart(before: lineBegin) ?? 0)
40 let to = nextHeadingStart(after: lineBegin) ?? length
41 return element(at: offset, in: substring(from, to), base: from)
38 } 42 }
39 43
40 private func element(at offset: Int, in text: String, base: Int) -> Element? { 44 private func element(at offset: Int, in text: String, base: Int) -> Element? {
Sources/OrgCore/Commands/HeadingCommands.swift +13 −29
@@ -111,18 +111,6 @@ struct LineBuffer {
111 let delta = edit(position..<position, string) 111 let delta = edit(position..<position, string)
112 caret = position + delta 112 caret = position + delta
113 } 113 }
114
115 /// Offset of display column `target` in the line, as `move-to-column`.
116 func offset(ofColumn target: Int) -> Int {
117 var column = 0
118 var offset = 0
119 for character in string {
120 if column >= target { break }
121 column = character == "\t" ? (column / 8 + 1) * 8 : column + displayWidth(of: character)
122 offset += String(character).utf16.count
123 }
124 return offset
125 }
126} 114}
127 115
128extension HeadingLine { 116extension HeadingLine {
@@ -156,25 +144,29 @@ extension HeadingLine {
156/// Commands align under `save-excursion`, where the caret is a marker: in the blanks before 144/// Commands align under `save-excursion`, where the caret is a marker: in the blanks before
157/// the tags it ends up where they start. Aligning tags directly (`preservingColumn`) keeps the 145/// the tags it ends up where they start. Aligning tags directly (`preservingColumn`) keeps the
158/// caret's column instead, as org does for point. 146/// caret's column instead, as org does for point.
159func alignTags(_ buffer: inout LineBuffer, tagsColumn: Int = -77, preservingColumn: Bool = false) { 147func alignTags(_ buffer: inout LineBuffer, options: EditingOptions = .org, preservingColumn: Bool = false) {
160 let line = buffer.string 148 let line = buffer.string
161 guard let match = line.range(of: "[ \\t]+(:[[:alnum:]_@#%]+)+:[ \\t]*$", options: .regularExpression) else { return } 149 guard let match = line.range(of: "[ \\t]+(:[[:alnum:]_@#%]+)+:[ \\t]*$", options: .regularExpression) else { return }
162 let matched = String(line[match]) 150 let matched = String(line[match])
163 let tags = matched.trimmingCharacters(in: .whitespaces) 151 let tags = matched.trimmingCharacters(in: .whitespaces)
164 let blankStart = (String(line[..<match.lowerBound]) as NSString).length 152 let blankStart = (String(line[..<match.lowerBound]) as NSString).length
165 let tagsStart = blankStart + (matched as NSString).range(of: tags).location 153 let tagsStart = blankStart + (matched as NSString).range(of: tags).location
166 let prefixColumn = column(of: String(line[..<match.lowerBound])) 154 // Columns as Emacs displays the line: link markup and, with the options, emphasis markers
167 let currentColumn = column(of: (line as NSString).substring(to: tagsStart)) 155 // don't count.
156 let visible = VisibleLine(line, options: options)
157 let prefixColumn = visible.column(at: blankStart)
158 let currentColumn = visible.column(at: tagsStart)
159 let tagsColumn = options.tagsColumn
168 let target = tagsColumn >= 0 ? tagsColumn : abs(tagsColumn) - displayWidth(tags) 160 let target = tagsColumn >= 0 ? tagsColumn : abs(tagsColumn) - displayWidth(tags)
169 let newColumn = max(target, prefixColumn + 1) 161 let newColumn = max(target, prefixColumn + 1)
170 guard newColumn != currentColumn else { return } 162 guard newColumn != currentColumn else { return }
171 let inBlanks = buffer.caret.map { $0 > blankStart && $0 <= tagsStart } ?? false 163 let inBlanks = buffer.caret.map { $0 > blankStart && $0 <= tagsStart } ?? false
172 let caretColumn: Int? = inBlanks && preservingColumn ? column(of: (line as NSString).substring(to: buffer.caret!)) : nil 164 let caretColumn: Int? = inBlanks && preservingColumn ? visible.column(at: buffer.caret!) : nil
173 let old = buffer.caret 165 let old = buffer.caret
174 buffer.text.replaceCharacters(in: NSRange(blankStart..<tagsStart), with: String(repeating: " ", count: newColumn - prefixColumn)) 166 buffer.text.replaceCharacters(in: NSRange(blankStart..<tagsStart), with: String(repeating: " ", count: newColumn - prefixColumn))
175 let delta = (newColumn - prefixColumn) - (tagsStart - blankStart) 167 let delta = (newColumn - prefixColumn) - (tagsStart - blankStart)
176 if let caretColumn { 168 if let caretColumn {
177 buffer.caret = buffer.offset(ofColumn: caretColumn) 169 buffer.caret = VisibleLine(buffer.string, options: options).offset(ofColumn: caretColumn)
178 } else if inBlanks { 170 } else if inBlanks {
179 buffer.caret = blankStart 171 buffer.caret = blankStart
180 } else if let old, old > tagsStart { 172 } else if let old, old > tagsStart {
@@ -182,14 +174,6 @@ func alignTags(_ buffer: inout LineBuffer, tagsColumn: Int = -77, preservingColu
182 } 174 }
183} 175}
184 176
185/// Display column at the end of `text`, starting from column 0.
186func column(of text: String) -> Int {
187 var column = 0
188 for character in text {
189 column = character == "\t" ? (column / 8 + 1) * 8 : column + displayWidth(of: character)
190 }
191 return column
192}
193 177
194// MARK: - TODO 178// MARK: - TODO
195 179
@@ -256,7 +240,7 @@ public struct TodoCycle: OrgCommand {
256 } 240 }
257 } 241 }
258 buffer.replaceBeforeMarkers(regionStart..<regionEnd, with: next.map { " \($0) " } ?? " ") 242 buffer.replaceBeforeMarkers(regionStart..<regionEnd, with: next.map { " \($0) " } ?? " ")
259 alignTags(&buffer, tagsColumn: context.options.tagsColumn) 243 alignTags(&buffer, options: context.options)
260 } 244 }
261 } 245 }
262} 246}
@@ -304,7 +288,7 @@ struct PriorityChange {
304 buffer.insert(cookie + " ", at: line.titleStart - line.start) 288 buffer.insert(cookie + " ", at: line.titleStart - line.start)
305 } 289 }
306 } 290 }
307 alignTags(&buffer, tagsColumn: context.options.tagsColumn) 291 alignTags(&buffer, options: context.options)
308 } 292 }
309 } 293 }
310 294
@@ -350,7 +334,7 @@ public struct PromoteHeading: OrgCommand {
350 guard line.level > 1 else { return .failed("Cannot promote to level 0") } 334 guard line.level > 1 else { return .failed("Cannot promote to level 0") }
351 return line.commit(context) { buffer in 335 return line.commit(context) { buffer in
352 buffer.replace(0..<(line.level + 1), with: String(repeating: "*", count: line.level - 1) + " ") 336 buffer.replace(0..<(line.level + 1), with: String(repeating: "*", count: line.level - 1) + " ")
353 alignTags(&buffer, tagsColumn: context.options.tagsColumn) 337 alignTags(&buffer, options: context.options)
354 fixPositionAfterPromote(&buffer, settings: context.tree.settings) 338 fixPositionAfterPromote(&buffer, settings: context.tree.settings)
355 } 339 }
356 } 340 }
@@ -367,7 +351,7 @@ public struct DemoteHeading: OrgCommand {
367 let line = HeadingLine(heading) 351 let line = HeadingLine(heading)
368 return line.commit(context) { buffer in 352 return line.commit(context) { buffer in
369 buffer.replace(0..<(line.level + 1), with: String(repeating: "*", count: line.level + 1) + " ") 353 buffer.replace(0..<(line.level + 1), with: String(repeating: "*", count: line.level + 1) + " ")
370 alignTags(&buffer, tagsColumn: context.options.tagsColumn) 354 alignTags(&buffer, options: context.options)
371 fixPositionAfterPromote(&buffer, settings: context.tree.settings) 355 fixPositionAfterPromote(&buffer, settings: context.tree.settings)
372 } 356 }
373 } 357 }
Sources/OrgCore/Commands/Table.swift added +631
@@ -0,0 +1,631 @@
1import Foundation
2
3// Org tables (org-table.el, Org 9.8.7): alignment and the editing commands, with
4// `org-table-automatic-realign` t, `org-table-tab-jumps-over-hlines` t, no shrunk columns,
5// `org-table-fix-formulas-confirm` nil (formulas are fixed silently).
6
7extension EmacsBuffer {
8 static let numberPattern = "^([<>]?[-+^.0-9]*[0-9][-+^.0-9eEdDx()%:]*|[<>]?[-+]?0[xX][0-9a-fA-F.]+|[<>]?[-+]?[0-9]+#[0-9a-zA-Z.]+|nan|[-+u]?inf)$"
9
10 /// `org-at-table-p`: an org table line outside blocks.
11 var atTable: Bool {
12 guard lookingAtLine("^[ \\t]*\\|") else { return false }
13 return element(at: lineStart(point))?.kind == .table || element(at: lineStart(point))?.kind == .tableRow
14 }
15
16 var atTableHline: Bool { lookingAtLine("^[ \\t]*\\|-") }
17
18 func lookingAtLine(_ pattern: String) -> Bool {
19 saveExcursion {
20 beginningOfLine()
21 return looking(pattern, caseInsensitive: false, setsMatch: false) != nil
22 }
23 }
24
25 /// `org-table-begin`.
26 func tableBegin() -> Int {
27 saveExcursion {
28 searchBackward("^[ \\t]*[^| \\t]", caseInsensitive: false).map { _ in lineEnd(point) + 1 } ?? 0
29 }
30 }
31
32 /// `org-table-end`.
33 func tableEnd() -> Int {
34 saveExcursion {
35 if let match = searchForward("^[ \\t]*[^| \\t]", caseInsensitive: false) { return match[0]!.lowerBound }
36 point = length
37 skipBackward(Self.blanks)
38 return bolp ? point : lineEnd(point)
39 }
40 }
41
42 /// `org-table-to-lisp`: rows of trimmed fields, nil for rules.
43 func tableToLisp() -> [[String]?] {
44 saveExcursion {
45 point = tableBegin()
46 var table: [[String]?] = []
47 while true {
48 skipForward(Self.blanks)
49 guard character(at: point) == 124 else { break }
50 point += 1
51 if character(at: point) == 45 {
52 table.append(nil)
53 } else {
54 var row: [String] = []
55 while true {
56 skipForward(Self.blanks)
57 if eolp { break }
58 let start = point
59 while let c = character(at: point), c != 124, c != 10 { point += 1 }
60 let next = eolp ? point : point + 1
61 skipBackward(Self.blanks, bound: start)
62 row.append(substring(start, point))
63 point = next
64 }
65 table.append(row)
66 }
67 if forwardLine(1) != 0 { break }
68 }
69 return table
70 }
71 }
72
73 /// `org-string-width` of a cell as Emacs shows it.
74 func cellWidth(_ cell: String) -> Int {
75 VisibleLine(cell, options: options, settings: settings).width
76 }
77
78 /// `org-table--align-field`.
79 func alignField(_ field: String, width: Int, alignment: Character, fieldWidth: Int) -> String {
80 let spaces = width - fieldWidth
81 let prefix: Int
82 switch alignment {
83 case "r": prefix = spaces
84 case "c": prefix = spaces / 2
85 default: prefix = 0
86 }
87 return " " + String(repeating: " ", count: prefix) + field + String(repeating: " ", count: spaces - prefix) + " "
88 }
89
90 /// `org-table-align`, keeping point in the same field.
91 func tableAlign() {
92 let begin = tableBegin()
93 let end = marker(tableEnd())
94 defer { release(end) }
95 let line = marker(lineStart(point))
96 defer { release(line) }
97 let column = tableCurrentColumn()
98 point = begin
99 let table = tableToLisp()
100 let rows = table.compactMap { $0 }
101 var widths: [Int] = []
102 var alignments: [Character] = []
103 var widthCache: [String: Int] = [:]
104 func width(_ cell: String) -> Int {
105 if let cached = widthCache[cell] { return cached }
106 let value = cellWidth(cell)
107 widthCache[cell] = value
108 return value
109 }
110 let numberRegex = Self.regex(Self.numberPattern, caseInsensitive: false)
111 var columns = 1
112 if rows.isEmpty {
113 let lineEndOffset = lineEnd(point)
114 columns += substring(point, lineEndOffset).filter { $0 == "+" }.count
115 widths = Array(repeating: 1, count: columns)
116 alignments = Array(repeating: "l", count: columns)
117 } else {
118 columns = rows.map(\.count).max() ?? 1
119 for i in 0..<columns {
120 var maxWidth = 1
121 var fixed: Character?
122 var numbers = 0, nonEmpty = 0
123 for row in rows {
124 let cell = i < row.count ? row[i] : ""
125 maxWidth = max(maxWidth, width(cell))
126 if fixed != nil || cell.isEmpty { continue }
127 if let cookie = cell.firstMatch(of: /^<([lrc])[0-9]*>$/) {
128 fixed = cookie.1.first
129 } else {
130 nonEmpty += 1
131 if numberRegex.firstMatch(in: cell, range: NSRange(location: 0, length: (cell as NSString).length)) != nil { numbers += 1 }
132 }
133 }
134 widths.append(maxWidth)
135 alignments.append(fixed ?? (Double(numbers) >= 0.5 * Double(nonEmpty) ? "r" : "l"))
136 }
137 }
138 let rule = widths.map { String(repeating: "-", count: $0 + 2) }.joined(separator: "+")
139 let indent = looking("[ \\t]*\\|", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } ?? "|"
140 for row in table {
141 let previous = substring(point, lineEnd(point))
142 let new: String
143 if let row {
144 let fields = row + Array(repeating: "", count: max(0, columns - row.count))
145 new = indent + zip(fields, zip(widths, alignments)).map { field, spec in
146 alignField(field, width: spec.0, alignment: spec.1, fieldWidth: width(field))
147 }.joined(separator: "|") + "|"
148 } else {
149 new = indent + rule + "|"
150 }
151 if new == previous {
152 forwardLine(1)
153 } else {
154 insert(new + "\n")
155 deleteRegion(point, min(length, lineEnd(point) + 1))
156 }
157 }
158 point = line.position
159 tableGotoColumn(column)
160 }
161
162 /// `org-table-current-column`.
163 func tableCurrentColumn() -> Int {
164 saveExcursion {
165 let pos = point
166 beginningOfLine()
167 let text = substring(point, pos)
168 guard let first = text.firstIndex(of: "|") else { return 0 }
169 let separators: Set<Character> = atTableHline ? ["+", "|"] : ["|"]
170 return 1 + text[text.index(after: first)...].filter { separators.contains($0) }.count
171 }
172 }
173
174 /// `org-table-goto-column`.
175 func tableGotoColumn(_ n: Int, onDelim: Bool = false, force: Bool = false) {
176 beginningOfLine()
177 guard n > 0 else { return }
178 var remaining = n
179 while remaining > 0 {
180 remaining -= 1
181 if let match = searchForward("\\|", bound: lineEnd(point), caseInsensitive: false) {
182 _ = match
183 } else if force {
184 endOfLine()
185 skipBackwardNot([124])
186 insert(" | ")
187 } else {
188 break
189 }
190 }
191 if force, looking(".*\\|", caseInsensitive: false, setsMatch: false) == nil {
192 saveExcursion {
193 endOfLine()
194 insert(" | ")
195 }
196 }
197 if onDelim {
198 point -= 1
199 } else if character(at: point) == 32 {
200 point += 1
201 }
202 }
203
204 /// `skip-chars-backward "^|"`.
205 func skipBackwardNot(_ set: Set<unichar>) {
206 while point > lineStart(point), let c = character(at: point - 1), !set.contains(c) { point -= 1 }
207 }
208
209 /// `org-table-current-dline`.
210 func tableCurrentDataLine() -> Int {
211 saveExcursion {
212 let pos = lineStart(point)
213 point = tableBegin()
214 var count = 0
215 while point <= pos {
216 if looking("^[ \\t]*\\|[^-]", caseInsensitive: false, setsMatch: false) != nil { count += 1 }
217 if forwardLine(1) != 0 { break }
218 }
219 return count
220 }
221 }
222
223 /// `org-table-clean-line`.
224 func cleanTableLine(_ line: String) -> String {
225 if line.range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil {
226 return String(line.map { $0 == "|" || $0 == "+" ? "|" : " " })
227 }
228 var result = line
229 let regex = try! NSRegularExpression(pattern: "\\|([ \\t]*?[^ \\t\\r\\n|][^\\r\\n|]*)\\|")
230 while let match = regex.firstMatch(in: result, range: NSRange(location: 0, length: (result as NSString).length)) {
231 let cell = (result as NSString).substring(with: match.range(at: 1))
232 result = (result as NSString).replacingCharacters(in: match.range, with: "|" + String(repeating: " ", count: cellWidth(cell)) + "|")
233 }
234 return result
235 }
236
237 /// `org-table-insert-row`; `below` as with a prefix argument.
238 func tableInsertRow(below: Bool) throws {
239 guard atTable else { throw UserError(message: "Not at a table") }
240 if eobp { saveExcursion { insert("\n") } }
241 if substring(lineStart(point), lineEnd(point)).range(of: "\\|[ \\t]*$", options: .regularExpression) == nil { tableAlign() }
242 let line = substring(lineStart(point), lineEnd(point))
243 var new = cleanTableLine(line)
244 if let match = line.range(of: "^[ \\t]*\\| *[#*$] *\\|", options: .regularExpression), let target = new.range(of: "^[ \\t]*\\| *[#*$] *\\|", options: .regularExpression) ?? new.range(of: "^[ \\t]*\\|[^|]*\\|", options: .regularExpression) {
245 new.replaceSubrange(target, with: line[match])
246 }
247 forwardLine(below ? 1 : 0)
248 if !bolp { insert("\n") }
249 insertBeforeMarkers(new + "\n")
250 forwardLine(-1)
251 searchForward("\\| ?", bound: lineEnd(point), caseInsensitive: false)
252 // `org-table-may-need-update' is set by any edit to a table, so org realigns here.
253 tableAlign()
254 fixFormulas("@", replace: [:], limit: tableCurrentDataLine() - 1, delta: 1)
255 }
256
257 /// `org-table-insert-hline`; `above` as with a prefix argument.
258 func tableInsertHline(above: Bool) throws {
259 guard atTable else { throw UserError(message: "Not at a table") }
260 if eobp { saveExcursion { insert("\n") } }
261 if substring(lineStart(point), lineEnd(point)).range(of: "\\|[ \\t]*$", options: .regularExpression) == nil { tableAlign() }
262 var line = cleanTableLine(substring(lineStart(point), lineEnd(point)))
263 let column = currentColumn
264 let regex = try! NSRegularExpression(pattern: "\\|( +)\\|")
265 while let match = regex.firstMatch(in: line, range: NSRange(location: 0, length: (line as NSString).length)) {
266 line = (line as NSString).replacingCharacters(in: match.range, with: "+" + String(repeating: "-", count: match.range(at: 1).length) + "|")
267 }
268 if let plus = line.firstIndex(of: "+") { line.replaceSubrange(plus...plus, with: "|") }
269 forwardLine(above ? 0 : 1)
270 insert(line + "\n")
271 forwardLine(above ? 0 : -2)
272 moveToColumn(column)
273 }
274
275 /// What `org-table-maybe-eval-formula` does to point: `org-table-get-field` leaves it one
276 /// character into the field.
277 func moveIntoField() {
278 skipBackwardNot([124, 10])
279 if bolp || looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil { return }
280 let start = point
281 point = min(lineEnd(point), start + 1)
282 }
283
284 /// `org-table-next-field`.
285 func tableNextField() throws {
286 moveIntoField()
287 tableAlign()
288 let end = tableEnd()
289 if atTableHline { endOfLine() }
290 func toNextBar() -> Bool { searchForward("\\|", bound: end, caseInsensitive: false) != nil }
291 var ok = toNextBar()
292 if ok, looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil { ok = toNextBar() }
293 guard ok else {
294 try tableInsertRow(below: true)
295 return
296 }
297 if looking("-", caseInsensitive: false, setsMatch: false) != nil, let match = searchForward("^[ \\t]*\\|([^-])", bound: end, caseInsensitive: false) {
298 point = match[1]!.lowerBound
299 }
300 if looking("-", caseInsensitive: false, setsMatch: false) != nil {
301 forwardLine(-1)
302 try tableInsertRow(below: true)
303 } else if character(at: point) == 32 {
304 point += 1
305 }
306 }
307
308 /// `org-table-previous-field`.
309 func tablePreviousField() throws {
310 tableAlign()
311 if atTableHline { endOfLine() }
312 let start = tableBegin()
313 let origin = point
314 func back() -> Bool {
315 guard let match = searchBackward("\\|", bound: start, caseInsensitive: false) else { return false }
316 point = match[0]!.lowerBound
317 return true
318 }
319 var ok = back() && back()
320 while ok, looking("\\|(?:-|[ \\t]*$)", caseInsensitive: false, setsMatch: false) != nil { ok = back() }
321 guard ok else {
322 point = origin
323 throw UserError(message: "Cannot move to previous table field")
324 }
325 if let bar = looking("\\| ?", caseInsensitive: false, setsMatch: false) { point = bar[0]!.upperBound }
326 }
327
328 /// `org-table-next-row`.
329 func tableNextRow() throws {
330 moveIntoField()
331 tableAlign()
332 let column = tableCurrentColumn()
333 forwardLine(1)
334 if !bolp { insert("\n") }
335 if !atTable || atTableHline {
336 forwardLine(-1)
337 try tableInsertRow(below: true)
338 }
339 tableGotoColumn(column)
340 skipBackwardNot([124, 10, 13])
341 if character(at: point) == 32 { point += 1 }
342 }
343
344 /// `org-table-move-row`.
345 func tableMoveRow(up: Bool) throws {
346 let column = currentColumn
347 let pos = point
348 let hline1 = atTableHline
349 let dline1 = tableCurrentDataLine()
350 let dline2 = dline1 + (up ? -1 : 1)
351 if up, lineStart(point) == 0 { throw UserError(message: "Cannot move row further") }
352 forwardLine(up ? -1 : 1)
353 if (!up && eobp) || !atTable {
354 point = pos
355 throw UserError(message: "Cannot move row further")
356 }
357 let hline2 = atTableHline
358 point = pos
359 let rowStart = lineStart(point)
360 let rowEnd = min(length, lineEnd(point) + 1)
361 let row = substring(rowStart, rowEnd)
362 deleteRegion(rowStart, rowEnd)
363 forwardLine(up ? -1 : 1)
364 if !bolp { insert("\n") }
365 insert(row)
366 if !bolp { insert("\n") }
367 forwardLine(-1)
368 moveToColumn(column)
369 if !hline1, !hline2 {
370 fixFormulas("@", replace: [String(dline1): String(dline2), String(dline2): String(dline1)])
371 }
372 }
373
374 /// `org-table-move-column`.
375 func tableMoveColumn(left: Bool) throws {
376 guard atTable else { throw UserError(message: "Not at a table") }
377 try findDataLine()
378 try checkInsideDataField()
379 let column = tableCurrentColumn()
380 let first = left ? column - 1 : column
381 let target = left ? column - 1 : column + 1
382 let begin = tableBegin()
383 let end = marker(tableEnd())
384 defer { release(end) }
385 if left, column == 1 { throw UserError(message: "Cannot move column further left") }
386 if !left, looking("[^|\\n]*\\|[^|\\n]*$", caseInsensitive: false, setsMatch: false) != nil { throw UserError(message: "Cannot move column further right") }
387 let line = marker(lineStart(point))
388 point = begin
389 while point < end.position {
390 if !atTableHline {
391 tableGotoColumn(first, onDelim: true)
392 if let match = looking("\\|([^|\\n]+)\\|([^|\\n]+)\\|", caseInsensitive: false, setsMatch: false) {
393 let a = substring(match[1]!.lowerBound, match[1]!.upperBound)
394 let b = substring(match[2]!.lowerBound, match[2]!.upperBound)
395 replace(match[1]!.lowerBound..<match[2]!.upperBound, with: b + "|" + a)
396 }
397 }
398 if forwardLine(1) != 0 { break }
399 }
400 point = line.position
401 release(line)
402 tableGotoColumn(column)
403 tableGotoColumn(target)
404 tableAlign()
405 fixFormulas("$", replace: [String(column): String(target), String(target): String(column)])
406 }
407
408 /// `org-table-insert-column`.
409 func tableInsertColumn() throws {
410 guard atTable else { throw UserError(message: "Not at a table") }
411 if eobp { saveExcursion { insert("\n") } }
412 if substring(lineStart(point), lineEnd(point)).range(of: "\\|[ \\t]*$", options: .regularExpression) == nil { tableAlign() }
413 try findDataLine()
414 let column = max(1, tableCurrentColumn())
415 let begin = tableBegin()
416 let end = marker(tableEnd())
417 defer { release(end) }
418 saveExcursion {
419 point = begin
420 while point < end.position {
421 if !atTableHline {
422 tableGotoColumn(column, onDelim: true)
423 insert("|")
424 }
425 if forwardLine(1) != 0 { break }
426 }
427 }
428 tableGotoColumn(column)
429 tableAlign()
430 fixFormulas("$", replace: [:], limit: column - 1, delta: 1)
431 }
432
433 /// `org-table-delete-column`.
434 func tableDeleteColumn() throws {
435 guard atTable else { throw UserError(message: "Not at a table") }
436 try findDataLine()
437 if saveExcursion({ () -> Bool in skipForward(Self.blanks); return eolp }) {
438 if let match = searchBackward("\\|", caseInsensitive: false) { point = match[0]!.lowerBound }
439 }
440 try checkInsideDataField()
441 let column = tableCurrentColumn()
442 let begin = tableBegin()
443 let end = marker(tableEnd())
444 defer { release(end) }
445 let line = marker(lineStart(point))
446 point = begin
447 while point < end.position {
448 if !atTableHline {
449 tableGotoColumn(column, onDelim: true)
450 if let match = looking("\\|[^|\\n]+\\|", caseInsensitive: false, setsMatch: false) { replace(match[0]!, with: "|") }
451 }
452 if forwardLine(1) != 0 { break }
453 }
454 point = line.position
455 release(line)
456 tableGotoColumn(column)
457 tableAlign()
458 fixFormulas("$", replace: [String(column): "INVALID"], limit: column, delta: -1, remove: column)
459 }
460
461 /// `org-table-kill-row`.
462 func tableKillRow() throws {
463 guard atTable else { throw UserError(message: "Not at a table") }
464 let column = currentColumn
465 let dline = atTableHline ? nil : tableCurrentDataLine()
466 deleteRegion(lineStart(point), min(lineEnd(point) + 1, length))
467 if !atTable { forwardLine(-1) }
468 moveToColumn(column)
469 if let dline { fixFormulas("@", replace: [String(dline): "INVALID"], limit: dline, delta: -1, remove: dline) }
470 }
471
472 /// `org-table-find-dataline`.
473 func findDataLine() throws {
474 guard atTableHline else { return }
475 let column = currentColumn
476 let end = tableEnd()
477 forwardLine(1)
478 while point < end, atTableHline { forwardLine(1) }
479 if point >= end { throw UserError(message: "Cannot find data row for column operation") }
480 moveToColumn(column)
481 }
482
483 /// `org-table-check-inside-data-field`.
484 func checkInsideDataField() throws {
485 let atLineStart = saveExcursion { () -> Bool in skipBackward(Self.blanks); return bolp }
486 if atLineStart || atTableHline || looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil {
487 throw UserError(message: "Not in table data field")
488 }
489 }
490
491 /// `org-table-fix-formulas`.
492 func fixFormulas(_ key: String, replace replacements: [String: String], limit: Int? = nil, delta: Int = 0, remove: Int? = nil) {
493 saveExcursion {
494 point = tableEnd()
495 while looking("[ \\t]*#\\+tblfm:", setsMatch: false) != nil {
496 if let remove {
497 let pattern = key == "$" ? "(@[0-9]+)?\\$\(remove)=.*?(::|$)" : "@\(remove)\\$[0-9]+=.*?(::|$)"
498 saveExcursion {
499 while let match = searchForward(pattern, bound: lineEnd(point), caseInsensitive: false) {
500 replace(match[0]!, with: "")
501 point = match[0]!.lowerBound
502 }
503 }
504 }
505 let lineStartOffset = point
506 let pattern = NSRegularExpression.escapedPattern(for: key) + "([0-9]+)"
507 while let match = searchForward(pattern, bound: lineEnd(point), caseInsensitive: false) {
508 let number = substring(match[1]!.lowerBound, match[1]!.upperBound)
509 if let new = replacements[number] {
510 replace(match[0]!, with: key + new)
511 } else if let limit, let n = Int(number), n > limit {
512 replace(match[0]!, with: key + String(n + delta))
513 }
514 }
515 _ = lineStartOffset
516 if forwardLine(1) != 0 { break }
517 }
518 }
519 }
520
521 /// `org-table-create` with a size like `3x2`.
522 func tableCreate(_ size: String) throws {
523 let parts = size.split(separator: "x").map { $0.trimmingCharacters(in: .whitespaces) }
524 guard parts.count == 2, let columns = Int(parts[0]), let rows = Int(parts[1]) else { throw UserError(message: "Size must be like 5x2") }
525 let pos = point
526 let indent = String(repeating: " ", count: currentColumn)
527 let line = indent + "|" + String(repeating: " |", count: columns) + "\n"
528 if substring(lineStart(point), point).allSatisfy({ $0 == " " || $0 == "\t" }) {
529 beginningOfLine()
530 } else {
531 insert("\n")
532 }
533 for _ in 0..<rows { insert(line) }
534 point = pos
535 if rows > 1 {
536 endOfLine()
537 insert("\n|-")
538 point = pos
539 }
540 tableAlign()
541 }
542}
543
544
545private func tableCommand(_ context: EditContext, _ body: (EmacsBuffer) throws -> Void) -> CommandStep {
546 commitBuffer(context, body)
547}
548
549/// A table command that needs the caret in an org table.
550public struct TableCommand: OrgCommand {
551 public enum Action: String, Sendable, CaseIterable {
552 case align, nextField, previousField, nextRow, moveRowUp, moveRowDown, moveColumnLeft, moveColumnRight
553 case insertColumn, deleteColumn, killRow, insertRowAbove, insertHline
554 }
555
556 public let action: Action
557 public init(_ action: Action) { self.action = action }
558
559 public var id: String {
560 switch action {
561 case .align: "org.table.align"
562 case .nextField: "org.table.next-field"
563 case .previousField: "org.table.previous-field"
564 case .nextRow: "org.table.next-row"
565 case .moveRowUp: "org.table.row-up"
566 case .moveRowDown: "org.table.row-down"
567 case .moveColumnLeft: "org.table.column-left"
568 case .moveColumnRight: "org.table.column-right"
569 case .insertColumn: "org.table.insert-column"
570 case .deleteColumn: "org.table.delete-column"
571 case .killRow: "org.table.kill-row"
572 case .insertRowAbove: "org.table.insert-row"
573 case .insertHline: "org.table.insert-hline"
574 }
575 }
576
577 public var title: String {
578 switch action {
579 case .align: "Align Table"
580 case .nextField: "Next Table Field"
581 case .previousField: "Previous Table Field"
582 case .nextRow: "Next Table Row"
583 case .moveRowUp: "Move Table Row Up"
584 case .moveRowDown: "Move Table Row Down"
585 case .moveColumnLeft: "Move Table Column Left"
586 case .moveColumnRight: "Move Table Column Right"
587 case .insertColumn: "Insert Table Column"
588 case .deleteColumn: "Delete Table Column"
589 case .killRow: "Delete Table Row"
590 case .insertRowAbove: "Insert Table Row"
591 case .insertHline: "Insert Table Rule"
592 }
593 }
594
595 public func applies(in context: EditContext) -> Bool { KeyContext.holds("table", in: context) }
596
597 public func run(in context: EditContext) -> CommandStep {
598 tableCommand(context) { buffer in
599 guard buffer.atTable else { throw EmacsBuffer.UserError(message: "Not at a table") }
600 switch action {
601 case .align: buffer.tableAlign()
602 case .nextField: try buffer.tableNextField()
603 case .previousField: try buffer.tablePreviousField()
604 case .nextRow: try buffer.tableNextRow()
605 case .moveRowUp: try buffer.tableMoveRow(up: true)
606 case .moveRowDown: try buffer.tableMoveRow(up: false)
607 case .moveColumnLeft: try buffer.tableMoveColumn(left: true)
608 case .moveColumnRight: try buffer.tableMoveColumn(left: false)
609 case .insertColumn: try buffer.tableInsertColumn()
610 case .deleteColumn: try buffer.tableDeleteColumn()
611 case .killRow: try buffer.tableKillRow()
612 case .insertRowAbove: try buffer.tableInsertRow(below: false)
613 case .insertHline: try buffer.tableInsertHline(above: false)
614 }
615 }
616 }
617}
618
619/// C-c |: `org-table-create` with a size prompt.
620public struct CreateTable: OrgCommand {
621 public init() {}
622 public var id: String { "org.table.create" }
623 public var title: String { "Create Table" }
624 public func applies(in context: EditContext) -> Bool { !KeyContext.holds("table", in: context) }
625 public func run(in context: EditContext) -> CommandStep {
626 guard let size = context.answers["size"] else {
627 return .prompt(Prompt(key: "size", message: "Table size Columns x Rows [e.g. 5x2]:", initial: ""))
628 }
629 return tableCommand(context) { try $0.tableCreate(size.isEmpty ? "5x2" : size) }
630 }
631}
Sources/OrgCore/Commands/VisibleText.swift added +112
@@ -0,0 +1,112 @@
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}
Sources/OrgCore/Keymap/Presets.swift +94
@@ -32,6 +32,18 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
32 [[bind]] 32 [[bind]]
33 keys = "S-TAB" 33 keys = "S-TAB"
34 command = "org.cycle-global" 34 command = "org.cycle-global"
35 [[bind]]
36 keys = "TAB"
37 command = "org.table.next-field"
38 when = "table"
39 [[bind]]
40 keys = "S-TAB"
41 command = "org.table.previous-field"
42 when = "table"
43 [[bind]]
44 keys = "RET"
45 command = "org.table.next-row"
46 when = "table"
35 47
36 """ 48 """
37 49
@@ -270,6 +282,49 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
270 keys = "M-q" 282 keys = "M-q"
271 command = "org.fill-paragraph" 283 command = "org.fill-paragraph"
272 [[bind]] 284 [[bind]]
285 keys = "C-c C-c"
286 command = "org.table.align"
287 when = "table"
288 [[bind]]
289 keys = "M-<up>"
290 command = "org.table.row-up"
291 when = "table"
292 [[bind]]
293 keys = "M-<down>"
294 command = "org.table.row-down"
295 when = "table"
296 [[bind]]
297 keys = "M-<left>"
298 command = "org.table.column-left"
299 when = "table"
300 [[bind]]
301 keys = "M-<right>"
302 command = "org.table.column-right"
303 when = "table"
304 [[bind]]
305 keys = "M-S-<right>"
306 command = "org.table.insert-column"
307 when = "table"
308 [[bind]]
309 keys = "M-S-<left>"
310 command = "org.table.delete-column"
311 when = "table"
312 [[bind]]
313 keys = "M-S-<up>"
314 command = "org.table.kill-row"
315 when = "table"
316 [[bind]]
317 keys = "M-S-<down>"
318 command = "org.table.insert-row"
319 when = "table"
320 [[bind]]
321 keys = "C-c -"
322 command = "org.table.insert-hline"
323 when = "table"
324 [[bind]]
325 keys = "C-c |"
326 command = "org.table.create"
327 [[bind]]
273 keys = "S-<up>" 328 keys = "S-<up>"
274 command = "org.timestamp.up" 329 command = "org.timestamp.up"
275 when = "timestamp" 330 when = "timestamp"
@@ -394,6 +449,45 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
394 command = "org.fill-paragraph" 449 command = "org.fill-paragraph"
395 [[bind]] 450 [[bind]]
396 keys = "C-s-<up>" 451 keys = "C-s-<up>"
452 command = "org.table.row-up"
453 when = "table"
454 [[bind]]
455 keys = "C-s-<down>"
456 command = "org.table.row-down"
457 when = "table"
458 [[bind]]
459 keys = "C-s-<left>"
460 command = "org.table.column-left"
461 when = "table"
462 [[bind]]
463 keys = "C-s-<right>"
464 command = "org.table.column-right"
465 when = "table"
466 [[bind]]
467 keys = "C-M-s-<right>"
468 command = "org.table.insert-column"
469 when = "table"
470 [[bind]]
471 keys = "C-M-s-<left>"
472 command = "org.table.delete-column"
473 when = "table"
474 [[bind]]
475 keys = "C-M-s-<up>"
476 command = "org.table.kill-row"
477 when = "table"
478 [[bind]]
479 keys = "C-M-s-<down>"
480 command = "org.table.insert-row"
481 when = "table"
482 [[bind]]
483 keys = "C-s--"
484 command = "org.table.insert-hline"
485 when = "table"
486 [[bind]]
487 keys = "C-s-\\\\"
488 command = "org.table.create"
489 [[bind]]
490 keys = "C-s-<up>"
397 command = "org.timestamp.up" 491 command = "org.timestamp.up"
398 when = "timestamp" 492 when = "timestamp"
399 [[bind]] 493 [[bind]]
Sources/OrgCore/Semantic/OrgEntities.swift added +419
@@ -0,0 +1,419 @@
1// Generated from `org-entities` (Org 9.8.7): entity name to the text org shows for it with
2// `org-pretty-entities`. The first definition of a name wins, as `assoc` finds it.
3
4enum OrgEntities {
5 static let display: [String: String] = [
6 "Agrave": "À",
7 "agrave": "à",
8 "Aacute": "Á",
9 "aacute": "á",
10 "Acirc": "Â",
11 "acirc": "â",
12 "Amacr": "Ã",
13 "amacr": "ã",
14 "Atilde": "Ã",
15 "atilde": "ã",
16 "Auml": "Ä",
17 "auml": "ä",
18 "Aring": "Å",
19 "AA": "Å",
20 "aring": "å",
21 "AElig": "Æ",
22 "aelig": "æ",
23 "Ccedil": "Ç",
24 "ccedil": "ç",
25 "Egrave": "È",
26 "egrave": "è",
27 "Eacute": "É",
28 "eacute": "é",
29 "Ecirc": "Ê",
30 "ecirc": "ê",
31 "Euml": "Ë",
32 "euml": "ë",
33 "Igrave": "Ì",
34 "igrave": "ì",
35 "Iacute": "Í",
36 "iacute": "í",
37 "Idot": "İ",
38 "inodot": "ı",
39 "Icirc": "Î",
40 "icirc": "î",
41 "Iuml": "Ï",
42 "iuml": "ï",
43 "Ntilde": "Ñ",
44 "ntilde": "ñ",
45 "Ograve": "Ò",
46 "ograve": "ò",
47 "Oacute": "Ó",
48 "oacute": "ó",
49 "Ocirc": "Ô",
50 "ocirc": "ô",
51 "Otilde": "Õ",
52 "otilde": "õ",
53 "Ouml": "Ö",
54 "ouml": "ö",
55 "Oslash": "Ø",
56 "oslash": "ø",
57 "OElig": "Œ",
58 "oelig": "œ",
59 "Scaron": "Š",
60 "scaron": "š",
61 "szlig": "ß",
62 "Ugrave": "Ù",
63 "ugrave": "ù",
64 "Uacute": "Ú",
65 "uacute": "ú",
66 "Ucirc": "Û",
67 "ucirc": "û",
68 "Uuml": "Ü",
69 "uuml": "ü",
70 "Yacute": "Ý",
71 "yacute": "ý",
72 "Yuml": "Ÿ",
73 "yuml": "ÿ",
74 "fnof": "ƒ",
75 "real": "ℜ",
76 "image": "ℑ",
77 "weierp": "℘",
78 "ell": "ℓ",
79 "imath": "ı",
80 "jmath": "ȷ",
81 "Alpha": "Α",
82 "alpha": "α",
83 "Beta": "Β",
84 "beta": "β",
85 "Gamma": "Γ",
86 "gamma": "γ",
87 "Delta": "Δ",
88 "delta": "δ",
89 "Epsilon": "Ε",
90 "epsilon": "ε",
91 "varepsilon": "ε",
92 "Zeta": "Ζ",
93 "zeta": "ζ",
94 "Eta": "Η",
95 "eta": "η",
96 "Theta": "Θ",
97 "theta": "θ",
98 "thetasym": "ϑ",
99 "vartheta": "ϑ",
100 "Iota": "Ι",
101 "iota": "ι",
102 "Kappa": "Κ",
103 "kappa": "κ",
104 "Lambda": "Λ",
105 "lambda": "λ",
106 "Mu": "Μ",
107 "mu": "μ",
108 "nu": "ν",
109 "Nu": "Ν",
110 "Xi": "Ξ",
111 "xi": "ξ",
112 "Omicron": "Ο",
113 "omicron": "ο",
114 "Pi": "Π",
115 "pi": "π",
116 "Rho": "Ρ",
117 "rho": "ρ",
118 "Sigma": "Σ",
119 "sigma": "σ",
120 "sigmaf": "ς",
121 "varsigma": "ς",
122 "Tau": "Τ",
123 "Upsilon": "Υ",
124 "upsih": "ϒ",
125 "upsilon": "υ",
126 "Phi": "Φ",
127 "phi": "ɸ",
128 "varphi": "φ",
129 "Chi": "Χ",
130 "chi": "χ",
131 "acutex": "𝑥́",
132 "Psi": "Ψ",
133 "psi": "ψ",
134 "tau": "τ",
135 "Omega": "Ω",
136 "omega": "ω",
137 "piv": "ϖ",
138 "varpi": "ϖ",
139 "partial": "∂",
140 "alefsym": "ℵ",
141 "aleph": "ℵ",
142 "gimel": "ℷ",
143 "beth": "ב",
144 "dalet": "ד",
145 "ETH": "Ð",
146 "eth": "ð",
147 "THORN": "Þ",
148 "thorn": "þ",
149 "dots": "…",
150 "cdots": "⋯",
151 "hellip": "…",
152 "middot": "·",
153 "iexcl": "¡",
154 "iquest": "¿",
155 "shy": "",
156 "ndash": "–",
157 "mdash": "—",
158 "quot": "\"",
159 "acute": "´",
160 "ldquo": "“",
161 "rdquo": "”",
162 "bdquo": "„",
163 "lsquo": "‘",
164 "rsquo": "’",
165 "sbquo": "‚",
166 "laquo": "«",
167 "raquo": "»",
168 "lsaquo": "‹",
169 "rsaquo": "›",
170 "circ": "∘",
171 "vert": "|",
172 "vbar": "|",
173 "brvbar": "¦",
174 "S": "§",
175 "sect": "§",
176 "P": "",
177 "para": "",
178 "amp": "&",
179 "lt": "<",
180 "gt": ">",
181 "tilde": "~",
182 "slash": "/",
183 "plus": "+",
184 "under": "_",
185 "equal": "=",
186 "asciicirc": "^",
187 "dagger": "†",
188 "dag": "†",
189 "Dagger": "‡",
190 "ddag": "‡",
191 "nbsp": " ",
192 "ensp": " ",
193 "emsp": " ",
194 "thinsp": " ",
195 "curren": "¤",
196 "cent": "¢",
197 "pound": "£",
198 "yen": "¥",
199 "euro": "€",
200 "EUR": "€",
201 "dollar": "$",
202 "USD": "$",
203 "copy": "©",
204 "reg": "®",
205 "trade": "™",
206 "minus": "−",
207 "pm": "±",
208 "plusmn": "±",
209 "times": "×",
210 "frasl": "⁄",
211 "colon": ":",
212 "div": "÷",
213 "frac12": "½",
214 "frac14": "¼",
215 "frac34": "¾",
216 "permil": "‰",
217 "sup1": "¹",
218 "sup2": "²",
219 "sup3": "³",
220 "radic": "√",
221 "sum": "∑",
222 "prod": "∏",
223 "micro": "µ",
224 "macr": "¯",
225 "deg": "°",
226 "prime": "′",
227 "Prime": "″",
228 "infin": "∞",
229 "infty": "∞",
230 "prop": "∝",
231 "propto": "∝",
232 "not": "¬",
233 "neg": "¬",
234 "land": "∧",
235 "wedge": "∧",
236 "lor": "∨",
237 "vee": "∨",
238 "cap": "∩",
239 "cup": "∪",
240 "smile": "⌣",
241 "frown": "⌢",
242 "int": "∫",
243 "therefore": "∴",
244 "there4": "∴",
245 "because": "∵",
246 "sim": "∼",
247 "cong": "≅",
248 "simeq": "≅",
249 "asymp": "≈",
250 "approx": "≈",
251 "ne": "≠",
252 "neq": "≠",
253 "equiv": "≡",
254 "triangleq": "≜",
255 "le": "≤",
256 "leq": "≤",
257 "ge": "≥",
258 "geq": "≥",
259 "lessgtr": "≶",
260 "lesseqgtr": "⋚",
261 "ll": "≪",
262 "Ll": "⋘",
263 "lll": "⋘",
264 "gg": "≫",
265 "Gg": "⋙",
266 "ggg": "⋙",
267 "prec": "≺",
268 "preceq": "≼",
269 "preccurlyeq": "≼",
270 "succ": "≻",
271 "succeq": "≽",
272 "succcurlyeq": "≽",
273 "sub": "⊂",
274 "subset": "⊂",
275 "sup": "⊃",
276 "supset": "⊃",
277 "nsub": "⊄",
278 "sube": "⊆",
279 "nsup": "⊅",
280 "supe": "⊇",
281 "setminus": "⧵",
282 "forall": "∀",
283 "exist": "∃",
284 "exists": "∃",
285 "nexist": "∄",
286 "nexists": "∄",
287 "empty": "∅",
288 "emptyset": "∅",
289 "isin": "∈",
290 "in": "∈",
291 "notin": "∉",
292 "ni": "∋",
293 "nabla": "∇",
294 "ang": "∠",
295 "angle": "∠",
296 "perp": "⊥",
297 "parallel": "∥",
298 "sdot": "⋅",
299 "cdot": "⋅",
300 "lceil": "⌈",
301 "rceil": "⌉",
302 "lfloor": "⌊",
303 "rfloor": "⌋",
304 "lang": "⟨",
305 "rang": "⟩",
306 "langle": "⟨",
307 "rangle": "⟩",
308 "hbar": "ℏ",
309 "mho": "℧",
310 "larr": "←",
311 "leftarrow": "←",
312 "gets": "←",
313 "lArr": "⇐",
314 "Leftarrow": "⇐",
315 "uarr": "↑",
316 "uparrow": "↑",
317 "uArr": "⇑",
318 "Uparrow": "⇑",
319 "rarr": "→",
320 "to": "→",
321 "rightarrow": "→",
322 "rArr": "⇒",
323 "Rightarrow": "⇒",
324 "darr": "↓",
325 "downarrow": "↓",
326 "dArr": "⇓",
327 "Downarrow": "⇓",
328 "harr": "↔",
329 "leftrightarrow": "↔",
330 "hArr": "⇔",
331 "Leftrightarrow": "⇔",
332 "crarr": "↵",
333 "hookleftarrow": "↵",
334 "arccos": "arccos",
335 "arcsin": "arcsin",
336 "arctan": "arctan",
337 "arg": "arg",
338 "cos": "cos",
339 "cosh": "cosh",
340 "cot": "cot",
341 "coth": "coth",
342 "csc": "csc",
343 "det": "det",
344 "dim": "dim",
345 "exp": "exp",
346 "gcd": "gcd",
347 "hom": "hom",
348 "inf": "inf",
349 "ker": "ker",
350 "lg": "lg",
351 "lim": "lim",
352 "liminf": "liminf",
353 "limsup": "limsup",
354 "ln": "ln",
355 "log": "log",
356 "max": "max",
357 "min": "min",
358 "Pr": "Pr",
359 "sec": "sec",
360 "sin": "sin",
361 "sinh": "sinh",
362 "tan": "tan",
363 "tanh": "tanh",
364 "bull": "•",
365 "bullet": "•",
366 "star": "⋆",
367 "lowast": "∗",
368 "ast": "*",
369 "odot": "ʘ",
370 "oplus": "⊕",
371 "otimes": "⊗",
372 "check": "✓",
373 "checkmark": "✓",
374 "ordf": "ª",
375 "ordm": "º",
376 "cedil": "¸",
377 "oline": "‾",
378 "uml": "¨",
379 "zwnj": "‌",
380 "zwj": "‍",
381 "lrm": "‎",
382 "rlm": "‏",
383 "smiley": "☺",
384 "blacksmile": "☻",
385 "sad": "☹",
386 "frowny": "☹",
387 "clubs": "♣",
388 "clubsuit": "♣",
389 "spades": "♠",
390 "spadesuit": "♠",
391 "hearts": "♥",
392 "heartsuit": "♥",
393 "diams": "◆",
394 "diamondsuit": "◆",
395 "diamond": "◆",
396 "Diamond": "◆",
397 "loz": "⧫",
398 "_ ": " ",
399 "_ ": "  ",
400 "_ ": "   ",
401 "_ ": "    ",
402 "_ ": "     ",
403 "_ ": "      ",
404 "_ ": "       ",
405 "_ ": "        ",
406 "_ ": "         ",
407 "_ ": "          ",
408 "_ ": "           ",
409 "_ ": "            ",
410 "_ ": "             ",
411 "_ ": "              ",
412 "_ ": "               ",
413 "_ ": "                ",
414 "_ ": "                 ",
415 "_ ": "                  ",
416 "_ ": "                   ",
417 "_ ": "                    ",
418 ]
419}
Tests/OrgCoreTests/EmacsOracle.swift +4 −2
@@ -49,6 +49,8 @@ enum EmacsOracle {
49 (org-mode) 49 (org-mode)
50 ;; `#+STARTUP' may fold the buffer; cases run with everything visible. 50 ;; `#+STARTUP' may fold the buffer; cases run with everything visible.
51 (org-fold-show-all) 51 (org-fold-show-all)
52 ;; Fontified, as in a displayed buffer: widths follow what is visible.
53 (font-lock-ensure)
52 (goto-char (alist-get 'point c)) 54 (goto-char (alist-get 'point c))
53 ;; As an interactive call, not a repeat: some commands cycle differently on repeats. 55 ;; As an interactive call, not a repeat: some commands cycle differently on repeats.
54 (setq this-command 'orgstar-oracle last-command nil) 56 (setq this-command 'orgstar-oracle last-command nil)
@@ -124,11 +126,11 @@ enum EmacsOracle {
124/// The options as Emacs Lisp bindings around `form`. 126/// The options as Emacs Lisp bindings around `form`.
125func withOptions(_ options: EditingOptions, _ form: String) -> String { 127func withOptions(_ options: EditingOptions, _ form: String) -> String {
126 func flag(_ value: Bool) -> String { value ? "t" : "nil" } 128 func flag(_ value: Bool) -> String { value ? "t" : "nil" }
127 return "(let ((org-tags-column \(options.tagsColumn)) (org-insert-heading-respect-content \(flag(options.insertHeadingRespectContent))) (org-M-RET-may-split-line \(flag(options.metaReturnMaySplitLine))) (org-list-allow-alphabetical \(flag(options.listAllowAlphabetical))) (fill-column \(options.fillColumn)) (sentence-end-double-space nil)) \(form))" 129 return "(let ((org-tags-column \(options.tagsColumn)) (org-insert-heading-respect-content \(flag(options.insertHeadingRespectContent))) (org-M-RET-may-split-line \(flag(options.metaReturnMaySplitLine))) (org-list-allow-alphabetical \(flag(options.listAllowAlphabetical))) (fill-column \(options.fillColumn)) (sentence-end-double-space nil) (org-hide-emphasis-markers \(flag(options.hideEmphasisMarkers))) (org-pretty-entities \(flag(options.prettyEntities))) (org-use-sub-superscripts \(options.subSuperscriptsNeedBraces ? "'{}" : "t"))) (font-lock-flush) (font-lock-ensure) \(form))"
128} 130}
129 131
130/// The options in the user's Doom Emacs. 132/// The options in the user's Doom Emacs.
131let doomOptions = EditingOptions(tagsColumn: 0, insertHeadingRespectContent: true, metaReturnMaySplitLine: false, listAllowAlphabetical: true, fillColumn: 80) 133let doomOptions = EditingOptions(tagsColumn: 0, insertHeadingRespectContent: true, metaReturnMaySplitLine: false, listAllowAlphabetical: true, fillColumn: 80, hideEmphasisMarkers: true, prettyEntities: true, subSuperscriptsNeedBraces: true)
132 134
133/// Runs a command the way an editor would: build a context, run, apply the edits. 135/// Runs a command the way an editor would: build a context, run, apply the edits.
134func runCommand(_ command: any OrgCommand, _ text: String, caret: Int, answers: [String: String] = [:], options: EditingOptions = .org) -> (text: String, caret: Int, failure: String?) { 136func runCommand(_ command: any OrgCommand, _ text: String, caret: Int, answers: [String: String] = [:], options: EditingOptions = .org) -> (text: String, caret: Int, failure: String?) {
Tests/OrgCoreTests/TableTests.swift added +101
@@ -0,0 +1,101 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct TableTests {
6 @Test func aligns() {
7 let result = runCommand(TableCommand(.align), "|a|bb|\n|ccc|1|\n", caret: 1).text
8 #expect(result == "| a | bb |\n| ccc | 1 |\n")
9 }
10
11 static let variants = [
12 "|a|bb|\n|ccc|1|\n",
13 "| Name | Qty |\n|-\n| apple | 3 |\n| pear | 12 |\n",
14 " | x | y |\n |---+---|\n | 1 | 2 |\n",
15 "| [[https://example.com][link]] | *bold* | =code= |\n| a | b | c |\n",
16 "| \\\\alpha | a_{bc} | x^2 |\n| 1 | 2 | 3 |\n",
17 "| <r> | <c5> |\n| a | b |\n",
18 "| 1 | 2 |\n| 3 | 4 |\n#+TBLFM: $2=$1*2::@2$1=7\n",
19 "text\n| a |\n| b |\nmore\n",
20 "| ü | 漢字 |\n| x | y |\n",
21 "| a | b\n| c |\n",
22 "|-+-|\n",
23 ]
24
25 static let commands: [(command: any OrgCommand, form: String, headingLineOnly: Bool)] = [
26 (TableCommand(.align), "(org-table-align)", false),
27 (TableCommand(.nextField), "(org-table-next-field)", false),
28 (TableCommand(.previousField), "(org-table-previous-field)", false),
29 (TableCommand(.nextRow), "(org-table-next-row)", false),
30 (TableCommand(.moveRowUp), "(org-table-move-row-up)", false),
31 (TableCommand(.moveRowDown), "(org-table-move-row-down)", false),
32 (TableCommand(.moveColumnLeft), "(org-table-move-column-left)", false),
33 (TableCommand(.moveColumnRight), "(org-table-move-column-right)", false),
34 (TableCommand(.insertColumn), "(org-table-insert-column)", false),
35 (TableCommand(.deleteColumn), "(org-table-delete-column)", false),
36 (TableCommand(.killRow), "(org-table-kill-row)", false),
37 (TableCommand(.insertRowAbove), "(org-table-insert-row)", false),
38 (TableCommand(.insertHline), "(org-table-insert-hline)", false),
39 ]
40
41 static func tableCarets(_ text: String) -> [Int] {
42 EmacsOracle.positions(text).filter { offset in
43 let ns = text as NSString
44 let line = ns.lineRange(for: NSRange(location: min(offset, max(0, ns.length - 1)), length: 0))
45 return offset < ns.length && ns.substring(with: line).trimmingCharacters(in: .whitespaces).hasPrefix("|")
46 }
47 }
48
49 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
50 func tableCommandsMatchEmacs() throws {
51 let inputs = Self.variants.map { ($0, Self.tableCarets($0)) }
52 try compareWithEmacs(inputs, Self.commands)
53 try compareWithEmacs(inputs, Self.commands, options: doomOptions)
54 }
55
56 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
57 func createTableMatchesEmacs() throws {
58 for (text, caret) in [("", 0), ("x\n", 1), (" \n", 2)] {
59 for size in ["3x2", "1x1", "2x3"] {
60 let emacs = try EmacsOracle.run([EmacsOracle.Case(text: text, point: caret + 1, form: "(org-table-create \"\(size)\")")])[0]
61 let ours = runCommand(CreateTable(), text, caret: caret, answers: ["size": size])
62 #expect(ours.text == emacs.text && ours.caret == EmacsOracle.offset(emacs.point, in: emacs.text), "\(size) in \(text.debugDescription): ours \(ours.text.debugDescription)@\(ours.caret) emacs \(emacs.text.debugDescription)@\(EmacsOracle.offset(emacs.point, in: emacs.text))")
63 }
64 }
65 }
66
67 /// Real tables: `ORGSTAR_ORACLE_CORPUS=<folder>` aligns and TABs in the first cell of up to
68 /// eight table rows per file, in up to 60 files.
69 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_ORACLE_CORPUS"] != nil))
70 func tablesMatchEmacsOnACorpus() throws {
71 let root = URL(fileURLWithPath: ProcessInfo.processInfo.environment["ORGSTAR_ORACLE_CORPUS"]!)
72 let files = FileManager.default.enumerator(at: root, includingPropertiesForKeys: nil)!
73 .compactMap { $0 as? URL }.filter { $0.pathExtension == "org" }.sorted { $0.path < $1.path }
74 var samples: [(String, [Int])] = []
75 for file in files where samples.count < 60 {
76 guard let text = try? String(contentsOf: file, encoding: .utf8), text.utf16.count <= 30_000, !text.contains("\r") else { continue }
77 let ns = text as NSString
78 var carets: [Int] = []
79 var location = 0
80 var inBlock = false
81 while location < ns.length, carets.count < 8 {
82 let line = ns.lineRange(for: NSRange(location: location, length: 0))
83 let content = ns.substring(with: line)
84 let trimmed = content.trimmingCharacters(in: .whitespaces).lowercased()
85 if trimmed.hasPrefix("#+begin_") { inBlock = true }
86 if trimmed.hasPrefix("#+end_") { inBlock = false }
87 if !inBlock, trimmed.hasPrefix("|"), let bar = content.firstIndex(of: "|") {
88 carets.append(line.location + content.distance(from: content.startIndex, to: bar) + 2)
89 }
90 location = NSMaxRange(line)
91 }
92 if !carets.isEmpty { samples.append((text, carets)) }
93 }
94 let commands: [(command: any OrgCommand, form: String, headingLineOnly: Bool)] = [
95 (TableCommand(.align), "(org-table-align)", false),
96 (TableCommand(.nextField), "(org-table-next-field)", false),
97 ]
98 try compareWithEmacs(samples, commands)
99 try compareWithEmacs(samples, commands, options: doomOptions)
100 }
101}