Sources/OrgCore/Commands/Table.swift
668 lines · 28031 bytes
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,
5// `org-table-fix-formulas-confirm` nil (formulas are fixed silently). Shrunk columns are only
6// drawn (`TableShrink`); these commands see the full text.
7
8extension EmacsBuffer {
9 static let numberPattern = "^([<>]?[-+^.0-9]*[0-9][-+^.0-9eEdDx()%:]*|[<>]?[-+]?0[xX][0-9a-fA-F.]+|[<>]?[-+]?[0-9]+#[0-9a-zA-Z.]+|nan|[-+u]?inf)$"
10
11 /// `org-at-table-p`: an org table line outside blocks.
12 var atTable: Bool {
13 guard lookingAtLine("^[ \\t]*\\|") else { return false }
14 // Dynamic blocks hold elements in Org; their tables count.
15 let kind = element(at: lineStart(point))?.kind
16 return kind == .table || kind == .tableRow || kind == .dynamicBlock
17 }
18
19 var atTableHline: Bool { lookingAtLine("^[ \\t]*\\|-") }
20
21 func lookingAtLine(_ pattern: String) -> Bool {
22 saveExcursion {
23 beginningOfLine()
24 return looking(pattern, caseInsensitive: false, setsMatch: false) != nil
25 }
26 }
27
28 /// `org-table-begin`.
29 func tableBegin() -> Int {
30 saveExcursion {
31 searchBackward("^[ \\t]*[^| \\t]", caseInsensitive: false).map { _ in lineEnd(point) + 1 } ?? 0
32 }
33 }
34
35 /// `org-table-end`.
36 func tableEnd() -> Int {
37 saveExcursion {
38 if let match = searchForward("^[ \\t]*[^| \\t]", caseInsensitive: false) { return match[0]!.lowerBound }
39 point = length
40 skipBackward(Self.blanks)
41 return bolp ? point : lineEnd(point)
42 }
43 }
44
45 /// `org-table-to-lisp`: rows of trimmed fields, nil for rules.
46 func tableToLisp() -> [[String]?] {
47 saveExcursion {
48 point = tableBegin()
49 var table: [[String]?] = []
50 while true {
51 skipForward(Self.blanks)
52 guard character(at: point) == 124 else { break }
53 point += 1
54 if character(at: point) == 45 {
55 table.append(nil)
56 } else {
57 var row: [String] = []
58 while true {
59 skipForward(Self.blanks)
60 if eolp { break }
61 let start = point
62 while let c = character(at: point), c != 124, c != 10 { point += 1 }
63 let next = eolp ? point : point + 1
64 skipBackward(Self.blanks, bound: start)
65 row.append(substring(start, point))
66 point = next
67 }
68 table.append(row)
69 }
70 if forwardLine(1) != 0 { break }
71 }
72 return table
73 }
74 }
75
76 /// `org-string-width` of a cell as Emacs shows it.
77 func cellWidth(_ cell: String) -> Int {
78 VisibleLine(cell, options: options, settings: settings).width
79 }
80
81 /// `org-table--align-field`.
82 func alignField(_ field: String, width: Int, alignment: Character, fieldWidth: Int) -> String {
83 let spaces = width - fieldWidth
84 let prefix: Int
85 switch alignment {
86 case "r": prefix = spaces
87 case "c": prefix = spaces / 2
88 default: prefix = 0
89 }
90 return " " + String(repeating: " ", count: prefix) + field + String(repeating: " ", count: spaces - prefix) + " "
91 }
92
93 /// `org-table-align`, keeping point in the same field.
94 func tableAlign() {
95 let begin = tableBegin()
96 let end = marker(tableEnd())
97 defer { release(end) }
98 let line = marker(lineStart(point))
99 defer { release(line) }
100 let column = tableCurrentColumn()
101 point = begin
102 let table = tableToLisp()
103 let rows = table.compactMap { $0 }
104 var widths: [Int] = []
105 var alignments: [Character] = []
106 var widthCache: [String: Int] = [:]
107 func width(_ cell: String) -> Int {
108 if let cached = widthCache[cell] { return cached }
109 let value = cellWidth(cell)
110 widthCache[cell] = value
111 return value
112 }
113 let numberRegex = Self.regex(Self.numberPattern, caseInsensitive: false)
114 var columns = 1
115 if rows.isEmpty {
116 let lineEndOffset = lineEnd(point)
117 columns += substring(point, lineEndOffset).filter { $0 == "+" }.count
118 widths = Array(repeating: 1, count: columns)
119 alignments = Array(repeating: "l", count: columns)
120 } else {
121 columns = rows.map(\.count).max() ?? 1
122 for i in 0..<columns {
123 var maxWidth = 1
124 var fixed: Character?
125 var numbers = 0, nonEmpty = 0
126 for row in rows {
127 let cell = i < row.count ? row[i] : ""
128 maxWidth = max(maxWidth, width(cell))
129 if fixed != nil || cell.isEmpty { continue }
130 if let cookie = cell.firstMatch(of: /^<([lrc])[0-9]*>$/) {
131 fixed = cookie.1.first
132 } else {
133 nonEmpty += 1
134 if numberRegex.firstMatch(in: cell, range: NSRange(location: 0, length: (cell as NSString).length)) != nil { numbers += 1 }
135 }
136 }
137 widths.append(maxWidth)
138 alignments.append(fixed ?? (Double(numbers) >= 0.5 * Double(nonEmpty) ? "r" : "l"))
139 }
140 }
141 let rule = widths.map { String(repeating: "-", count: $0 + 2) }.joined(separator: "+")
142 let indent = looking("[ \\t]*\\|", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } ?? "|"
143 for row in table {
144 let previous = substring(point, lineEnd(point))
145 let new: String
146 if let row {
147 let fields = row + Array(repeating: "", count: max(0, columns - row.count))
148 new = indent + zip(fields, zip(widths, alignments)).map { field, spec in
149 alignField(field, width: spec.0, alignment: spec.1, fieldWidth: width(field))
150 }.joined(separator: "|") + "|"
151 } else {
152 new = indent + rule + "|"
153 }
154 if new == previous {
155 forwardLine(1)
156 } else {
157 insert(new + "\n")
158 deleteRegion(point, min(length, lineEnd(point) + 1))
159 }
160 }
161 point = line.position
162 tableGotoColumn(column)
163 }
164
165 /// `org-table-current-column`.
166 func tableCurrentColumn() -> Int {
167 saveExcursion {
168 let pos = point
169 beginningOfLine()
170 let text = substring(point, pos)
171 guard let first = text.firstIndex(of: "|") else { return 0 }
172 let separators: Set<Character> = atTableHline ? ["+", "|"] : ["|"]
173 return 1 + text[text.index(after: first)...].filter { separators.contains($0) }.count
174 }
175 }
176
177 /// `org-table-goto-column`.
178 func tableGotoColumn(_ n: Int, onDelim: Bool = false, force: Bool = false) {
179 beginningOfLine()
180 guard n > 0 else { return }
181 var remaining = n
182 while remaining > 0 {
183 remaining -= 1
184 if let match = searchForward("\\|", bound: lineEnd(point), caseInsensitive: false) {
185 _ = match
186 } else if force {
187 endOfLine()
188 skipBackwardNot([124])
189 insert(" | ")
190 } else {
191 break
192 }
193 }
194 if force, looking(".*\\|", caseInsensitive: false, setsMatch: false) == nil {
195 saveExcursion {
196 endOfLine()
197 insert(" | ")
198 }
199 }
200 if onDelim {
201 point -= 1
202 } else if character(at: point) == 32 {
203 point += 1
204 }
205 }
206
207 /// `skip-chars-backward "^|"`.
208 func skipBackwardNot(_ set: Set<unichar>) {
209 while point > lineStart(point), let c = character(at: point - 1), !set.contains(c) { point -= 1 }
210 }
211
212 /// `org-table-current-dline`.
213 func tableCurrentDataLine() -> Int {
214 saveExcursion {
215 let pos = lineStart(point)
216 point = tableBegin()
217 var count = 0
218 while point <= pos {
219 if looking("^[ \\t]*\\|[^-]", caseInsensitive: false, setsMatch: false) != nil { count += 1 }
220 if forwardLine(1) != 0 { break }
221 }
222 return count
223 }
224 }
225
226 /// `org-table-clean-line`.
227 func cleanTableLine(_ line: String) -> String {
228 if line.range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil {
229 return String(line.map { $0 == "|" || $0 == "+" ? "|" : " " })
230 }
231 var result = line
232 let regex = try! NSRegularExpression(pattern: "\\|([ \\t]*?[^ \\t\\r\\n|][^\\r\\n|]*)\\|")
233 while let match = regex.firstMatch(in: result, range: NSRange(location: 0, length: (result as NSString).length)) {
234 let cell = (result as NSString).substring(with: match.range(at: 1))
235 result = (result as NSString).replacingCharacters(in: match.range, with: "|" + String(repeating: " ", count: cellWidth(cell)) + "|")
236 }
237 return result
238 }
239
240 /// `org-table-insert-row`; `below` as with a prefix argument.
241 func tableInsertRow(below: Bool) throws {
242 guard atTable else { throw UserError(message: "Not at a table") }
243 if eobp { saveExcursion { insert("\n") } }
244 if substring(lineStart(point), lineEnd(point)).range(of: "\\|[ \\t]*$", options: .regularExpression) == nil { tableAlign() }
245 let line = substring(lineStart(point), lineEnd(point))
246 var new = cleanTableLine(line)
247 if let match = line.range(of: "^[ \\t]*\\| *[#*$] *\\|", options: .regularExpression), let target = new.range(of: "^[ \\t]*\\| *[#*$] *\\|", options: .regularExpression) ?? new.range(of: "^[ \\t]*\\|[^|]*\\|", options: .regularExpression) {
248 new.replaceSubrange(target, with: line[match])
249 }
250 forwardLine(below ? 1 : 0)
251 if !bolp { insert("\n") }
252 insertBeforeMarkers(new + "\n")
253 forwardLine(-1)
254 searchForward("\\| ?", bound: lineEnd(point), caseInsensitive: false)
255 // `org-table-may-need-update' is set by any edit to a table, so org realigns here.
256 tableAlign()
257 fixFormulas("@", replace: [:], limit: tableCurrentDataLine() - 1, delta: 1)
258 }
259
260 /// `org-table-insert-hline`; `above` as with a prefix argument.
261 func tableInsertHline(above: Bool) throws {
262 guard atTable else { throw UserError(message: "Not at a table") }
263 if eobp { saveExcursion { insert("\n") } }
264 if substring(lineStart(point), lineEnd(point)).range(of: "\\|[ \\t]*$", options: .regularExpression) == nil { tableAlign() }
265 var line = cleanTableLine(substring(lineStart(point), lineEnd(point)))
266 let column = currentColumn
267 let regex = try! NSRegularExpression(pattern: "\\|( +)\\|")
268 while let match = regex.firstMatch(in: line, range: NSRange(location: 0, length: (line as NSString).length)) {
269 line = (line as NSString).replacingCharacters(in: match.range, with: "+" + String(repeating: "-", count: match.range(at: 1).length) + "|")
270 }
271 if let plus = line.firstIndex(of: "+") { line.replaceSubrange(plus...plus, with: "|") }
272 forwardLine(above ? 0 : 1)
273 insert(line + "\n")
274 forwardLine(above ? 0 : -2)
275 moveToColumn(column)
276 }
277
278 /// What `org-table-maybe-eval-formula` does to point: `org-table-get-field` leaves it one
279 /// character into the field.
280 func moveIntoField() {
281 skipBackwardNot([124, 10])
282 if bolp || looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil { return }
283 let start = point
284 point = min(lineEnd(point), start + 1)
285 }
286
287 /// `org-table-next-field`.
288 func tableNextField() throws {
289 try tableMaybeEvalFormula()
290 moveIntoField()
291 tableAlign()
292 let end = tableEnd()
293 if atTableHline { endOfLine() }
294 func toNextBar() -> Bool { searchForward("\\|", bound: end, caseInsensitive: false) != nil }
295 var ok = toNextBar()
296 if ok, looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil { ok = toNextBar() }
297 guard ok else {
298 try tableInsertRow(below: true)
299 return
300 }
301 if looking("-", caseInsensitive: false, setsMatch: false) != nil, let match = searchForward("^[ \\t]*\\|([^-])", bound: end, caseInsensitive: false) {
302 point = match[1]!.lowerBound
303 }
304 if looking("-", caseInsensitive: false, setsMatch: false) != nil {
305 forwardLine(-1)
306 try tableInsertRow(below: true)
307 } else if character(at: point) == 32 {
308 point += 1
309 }
310 }
311
312 /// `org-table-previous-field`.
313 func tablePreviousField() throws {
314 tableAlign()
315 if atTableHline { endOfLine() }
316 let start = tableBegin()
317 let origin = point
318 func back() -> Bool {
319 guard let match = searchBackward("\\|", bound: start, caseInsensitive: false) else { return false }
320 point = match[0]!.lowerBound
321 return true
322 }
323 var ok = back() && back()
324 while ok, looking("\\|(?:-|[ \\t]*$)", caseInsensitive: false, setsMatch: false) != nil { ok = back() }
325 guard ok else {
326 point = origin
327 throw UserError(message: "Cannot move to previous table field")
328 }
329 if let bar = looking("\\| ?", caseInsensitive: false, setsMatch: false) { point = bar[0]!.upperBound }
330 }
331
332 /// `org-table-next-row`.
333 func tableNextRow() throws {
334 try tableMaybeEvalFormula()
335 moveIntoField()
336 tableAlign()
337 let column = tableCurrentColumn()
338 forwardLine(1)
339 if !bolp { insert("\n") }
340 if !atTable || atTableHline {
341 forwardLine(-1)
342 try tableInsertRow(below: true)
343 }
344 tableGotoColumn(column)
345 skipBackwardNot([124, 10, 13])
346 if character(at: point) == 32 { point += 1 }
347 }
348
349 /// `org-table-move-row`.
350 func tableMoveRow(up: Bool) throws {
351 let column = currentColumn
352 let pos = point
353 let hline1 = atTableHline
354 let dline1 = tableCurrentDataLine()
355 let dline2 = dline1 + (up ? -1 : 1)
356 if up, lineStart(point) == 0 { throw UserError(message: "Cannot move row further") }
357 forwardLine(up ? -1 : 1)
358 if (!up && eobp) || !atTable {
359 point = pos
360 throw UserError(message: "Cannot move row further")
361 }
362 let hline2 = atTableHline
363 point = pos
364 let rowStart = lineStart(point)
365 let rowEnd = min(length, lineEnd(point) + 1)
366 let row = substring(rowStart, rowEnd)
367 deleteRegion(rowStart, rowEnd)
368 forwardLine(up ? -1 : 1)
369 if !bolp { insert("\n") }
370 insert(row)
371 if !bolp { insert("\n") }
372 forwardLine(-1)
373 moveToColumn(column)
374 if !hline1, !hline2 {
375 fixFormulas("@", replace: [String(dline1): String(dline2), String(dline2): String(dline1)])
376 }
377 }
378
379 /// `org-table-move-column`.
380 func tableMoveColumn(left: Bool) throws {
381 guard atTable else { throw UserError(message: "Not at a table") }
382 try findDataLine()
383 try checkInsideDataField()
384 let column = tableCurrentColumn()
385 let first = left ? column - 1 : column
386 let target = left ? column - 1 : column + 1
387 let begin = tableBegin()
388 let end = marker(tableEnd())
389 defer { release(end) }
390 if left, column == 1 { throw UserError(message: "Cannot move column further left") }
391 if !left, looking("[^|\\n]*\\|[^|\\n]*$", caseInsensitive: false, setsMatch: false) != nil { throw UserError(message: "Cannot move column further right") }
392 let line = marker(lineStart(point))
393 point = begin
394 while point < end.position {
395 if !atTableHline {
396 tableGotoColumn(first, onDelim: true)
397 if let match = looking("\\|([^|\\n]+)\\|([^|\\n]+)\\|", caseInsensitive: false, setsMatch: false) {
398 let a = substring(match[1]!.lowerBound, match[1]!.upperBound)
399 let b = substring(match[2]!.lowerBound, match[2]!.upperBound)
400 replace(match[1]!.lowerBound..<match[2]!.upperBound, with: b + "|" + a)
401 }
402 }
403 if forwardLine(1) != 0 { break }
404 }
405 point = line.position
406 release(line)
407 tableGotoColumn(column)
408 tableGotoColumn(target)
409 tableAlign()
410 fixFormulas("$", replace: [String(column): String(target), String(target): String(column)])
411 }
412
413 /// `org-table-insert-column`.
414 func tableInsertColumn() throws {
415 guard atTable else { throw UserError(message: "Not at a table") }
416 if eobp { saveExcursion { insert("\n") } }
417 if substring(lineStart(point), lineEnd(point)).range(of: "\\|[ \\t]*$", options: .regularExpression) == nil { tableAlign() }
418 try findDataLine()
419 let column = max(1, tableCurrentColumn())
420 let begin = tableBegin()
421 let end = marker(tableEnd())
422 defer { release(end) }
423 saveExcursion {
424 point = begin
425 while point < end.position {
426 if !atTableHline {
427 tableGotoColumn(column, onDelim: true)
428 insert("|")
429 }
430 if forwardLine(1) != 0 { break }
431 }
432 }
433 tableGotoColumn(column)
434 tableAlign()
435 fixFormulas("$", replace: [:], limit: column - 1, delta: 1)
436 }
437
438 /// `org-table-delete-column`.
439 func tableDeleteColumn() throws {
440 guard atTable else { throw UserError(message: "Not at a table") }
441 try findDataLine()
442 if saveExcursion({ () -> Bool in skipForward(Self.blanks); return eolp }) {
443 if let match = searchBackward("\\|", caseInsensitive: false) { point = match[0]!.lowerBound }
444 }
445 try checkInsideDataField()
446 let column = tableCurrentColumn()
447 let begin = tableBegin()
448 let end = marker(tableEnd())
449 defer { release(end) }
450 let line = marker(lineStart(point))
451 point = begin
452 while point < end.position {
453 if !atTableHline {
454 tableGotoColumn(column, onDelim: true)
455 if let match = looking("\\|[^|\\n]+\\|", caseInsensitive: false, setsMatch: false) { replace(match[0]!, with: "|") }
456 }
457 if forwardLine(1) != 0 { break }
458 }
459 point = line.position
460 release(line)
461 tableGotoColumn(column)
462 tableAlign()
463 fixFormulas("$", replace: [String(column): "INVALID"], limit: column, delta: -1, remove: column)
464 }
465
466 /// `org-table-kill-row`.
467 func tableKillRow() throws {
468 guard atTable else { throw UserError(message: "Not at a table") }
469 let column = currentColumn
470 let dline = atTableHline ? nil : tableCurrentDataLine()
471 deleteRegion(lineStart(point), min(lineEnd(point) + 1, length))
472 if !atTable { forwardLine(-1) }
473 moveToColumn(column)
474 if let dline { fixFormulas("@", replace: [String(dline): "INVALID"], limit: dline, delta: -1, remove: dline) }
475 }
476
477 /// `org-table-find-dataline`.
478 func findDataLine() throws {
479 guard atTableHline else { return }
480 let column = currentColumn
481 let end = tableEnd()
482 forwardLine(1)
483 while point < end, atTableHline { forwardLine(1) }
484 if point >= end { throw UserError(message: "Cannot find data row for column operation") }
485 moveToColumn(column)
486 }
487
488 /// `org-table-check-inside-data-field`.
489 func checkInsideDataField() throws {
490 let atLineStart = saveExcursion { () -> Bool in skipBackward(Self.blanks); return bolp }
491 if atLineStart || atTableHline || looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil {
492 throw UserError(message: "Not in table data field")
493 }
494 }
495
496 /// `org-table-fix-formulas`.
497 func fixFormulas(_ key: String, replace replacements: [String: String], limit: Int? = nil, delta: Int = 0, remove: Int? = nil) {
498 saveExcursion {
499 point = tableEnd()
500 while looking("[ \\t]*#\\+tblfm:", setsMatch: false) != nil {
501 if let remove {
502 let pattern = key == "$" ? "(@[0-9]+)?\\$\(remove)=.*?(::|$)" : "@\(remove)\\$[0-9]+=.*?(::|$)"
503 saveExcursion {
504 while let match = searchForward(pattern, bound: lineEnd(point), caseInsensitive: false) {
505 replace(match[0]!, with: "")
506 point = match[0]!.lowerBound
507 }
508 }
509 }
510 let lineStartOffset = point
511 let pattern = NSRegularExpression.escapedPattern(for: key) + "([0-9]+)"
512 while let match = searchForward(pattern, bound: lineEnd(point), caseInsensitive: false) {
513 let number = substring(match[1]!.lowerBound, match[1]!.upperBound)
514 if let new = replacements[number] {
515 replace(match[0]!, with: key + new)
516 } else if let limit, let n = Int(number), n > limit {
517 replace(match[0]!, with: key + String(n + delta))
518 }
519 }
520 _ = lineStartOffset
521 if forwardLine(1) != 0 { break }
522 }
523 }
524 }
525
526 /// `org-table-create` with a size like `3x2`.
527 func tableCreate(_ size: String) throws {
528 let parts = size.split(separator: "x").map { $0.trimmingCharacters(in: .whitespaces) }
529 guard parts.count == 2, let columns = Int(parts[0]), let rows = Int(parts[1]) else { throw UserError(message: "Size must be like 5x2") }
530 let pos = point
531 let indent = String(repeating: " ", count: currentColumn)
532 let line = indent + "|" + String(repeating: " |", count: columns) + "\n"
533 if substring(lineStart(point), point).allSatisfy({ $0 == " " || $0 == "\t" }) {
534 beginningOfLine()
535 } else {
536 insert("\n")
537 }
538 for _ in 0..<rows { insert(line) }
539 point = pos
540 if rows > 1 {
541 endOfLine()
542 insert("\n|-")
543 point = pos
544 }
545 tableAlign()
546 }
547}
548
549
550private func tableCommand(_ context: EditContext, _ body: (EmacsBuffer) throws -> Void) -> CommandStep {
551 commitBuffer(context, body)
552}
553
554/// A table command that needs the caret in an org table.
555public struct TableCommand: OrgCommand {
556 public enum Action: String, Sendable, CaseIterable {
557 case align, nextField, previousField, nextRow, moveRowUp, moveRowDown, moveColumnLeft, moveColumnRight
558 case insertColumn, deleteColumn, killRow, insertRowAbove, insertHline
559 }
560
561 public let action: Action
562 public init(_ action: Action) { self.action = action }
563
564 public var id: String {
565 switch action {
566 case .align: "org.table.align"
567 case .nextField: "org.table.next-field"
568 case .previousField: "org.table.previous-field"
569 case .nextRow: "org.table.next-row"
570 case .moveRowUp: "org.table.row-up"
571 case .moveRowDown: "org.table.row-down"
572 case .moveColumnLeft: "org.table.column-left"
573 case .moveColumnRight: "org.table.column-right"
574 case .insertColumn: "org.table.insert-column"
575 case .deleteColumn: "org.table.delete-column"
576 case .killRow: "org.table.kill-row"
577 case .insertRowAbove: "org.table.insert-row"
578 case .insertHline: "org.table.insert-hline"
579 }
580 }
581
582 public var title: String {
583 switch action {
584 case .align: "Align Table"
585 case .nextField: "Next Table Field"
586 case .previousField: "Previous Table Field"
587 case .nextRow: "Next Table Row"
588 case .moveRowUp: "Move Table Row Up"
589 case .moveRowDown: "Move Table Row Down"
590 case .moveColumnLeft: "Move Table Column Left"
591 case .moveColumnRight: "Move Table Column Right"
592 case .insertColumn: "Insert Table Column"
593 case .deleteColumn: "Delete Table Column"
594 case .killRow: "Delete Table Row"
595 case .insertRowAbove: "Insert Table Row"
596 case .insertHline: "Insert Table Rule"
597 }
598 }
599
600 public func applies(in context: EditContext) -> Bool { KeyContext.holds("table", in: context) }
601
602 public func run(in context: EditContext) -> CommandStep {
603 tableCommand(context) { buffer in
604 guard buffer.atTable else { throw EmacsBuffer.UserError(message: "Not at a table") }
605 switch action {
606 case .align: buffer.tableAlign()
607 case .nextField: try buffer.tableNextField()
608 case .previousField: try buffer.tablePreviousField()
609 case .nextRow: try buffer.tableNextRow()
610 case .moveRowUp: try buffer.tableMoveRow(up: true)
611 case .moveRowDown: try buffer.tableMoveRow(up: false)
612 case .moveColumnLeft: try buffer.tableMoveColumn(left: true)
613 case .moveColumnRight: try buffer.tableMoveColumn(left: false)
614 case .insertColumn: try buffer.tableInsertColumn()
615 case .deleteColumn: try buffer.tableDeleteColumn()
616 case .killRow: try buffer.tableKillRow()
617 case .insertRowAbove: try buffer.tableInsertRow(below: false)
618 case .insertHline: try buffer.tableInsertHline(above: false)
619 }
620 }
621 }
622}
623
624/// `#+STARTUP: align` (`org-startup-align-all-tables`): every table aligned.
625public struct AlignAllTables: OrgCommand {
626 public init() {}
627 public var id: String { "org.table.align-all" }
628 public var title: String { "Align All Tables" }
629 public func applies(in context: EditContext) -> Bool { true }
630 public func run(in context: EditContext) -> CommandStep {
631 let starts = context.tree.root.descendants().filter { $0.kind == .table }.map(\.range.lowerBound)
632 guard !starts.isEmpty else { return .commit(EditResult(baseRevision: context.revision, edits: [])) }
633 return commitBuffer(context) { buffer in
634 let caret = buffer.marker(buffer.point)
635 defer {
636 buffer.point = caret.position
637 buffer.release(caret)
638 }
639 for start in starts.reversed() {
640 buffer.point = start
641 buffer.skipForward([32, 9])
642 buffer.point += 1
643 buffer.tableAlign()
644 }
645 }
646 }
647}
648
649/// C-c |: `org-table-create` with a size prompt.
650public struct CreateTable: OrgCommand {
651 public init() {}
652 public var id: String { "org.table.create" }
653 public var title: String { "Create Table" }
654 public func applies(in context: EditContext) -> Bool { !KeyContext.holds("table", in: context) }
655 public func run(in context: EditContext) -> CommandStep {
656 if let converted = convert(context) { return converted }
657 guard let size = context.answers["size"] else {
658 return .prompt(Prompt(key: "size", message: "Table size Columns x Rows [e.g. 5x2]:", initial: ""))
659 }
660 return tableCommand(context) { try $0.tableCreate(size.isEmpty ? "5x2" : size) }
661 }
662
663 /// `org-table-create-or-convert-from-region`.
664 private func convert(_ context: EditContext) -> CommandStep? {
665 guard let region = context.selection.first, !region.isEmpty else { return nil }
666 return commitBuffer(context) { $0.tableConvertRegion(region.lowerBound, region.upperBound) }
667 }
668}