Sources/OrgCore/Commands/Table.swift
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}