krz/orgstar

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

Sources/OrgCore/Commands/Table.swift

abca8dc581329472f7fb8ccd3af147e5fb882f29
orgstar/Sources/OrgCore/Commands/Table.swift history · blame · raw

631 lines · 26392 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, 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}