Commit e4003d118e
e4003d118e0bebe476c67fcaf17455fd815bd5cd
parent: aeb1840b38
Verified · cmc
cmc <hello@cleberg.net> · 2026-10-08 03:40 UTC
C-c C-c in a table runs org-ctrl-c-ctrl-c
The Emacs preset (and Doom, built from it) bound C-c C-c in tables to org.table.align. org-ctrl-c-ctrl-c now evaluates a typed formula, then recalculates a row marked # or aligns, falling back to Emacs when the row needs it.
Layout: unified · split
Sources/OrgCore/Commands/CtrlCCtrlC.swift
+13 −3
| @@ -3,7 +3,8 @@ import Foundation |
| 3 | 3 | // C-c C-c (`org-ctrl-c-ctrl-c`, Org 9.8.7) for the contexts no more specific binding takes: |
| 4 | 4 | // headings set tags, clock lines get their duration again, timestamps their weekday, cookies |
| 5 | 5 | // update, items toggle their checkbox, or without one have their list repaired, setup keywords |
| 6 | | // refresh, and elsewhere there's nothing to do. |
| 6 | // refresh, tables evaluate a typed formula, then align or recalculate a row marked `#`, and |
| 7 | // elsewhere there's nothing to do. |
| 7 | 8 | |
| 8 | 9 | /// `org-ctrl-c-ctrl-c`. |
| 9 | 10 | public struct CtrlCCtrlC: OrgCommand { |
| @@ -20,10 +21,19 @@ public struct CtrlCCtrlC: OrgCommand { |
| 20 | 21 | if KeyContext.holds("table", in: context) || TableRecalculate.tblfmLine(in: context) != nil { |
| 21 | 22 | // At the table's very start the context is the table itself, which org recalculates. |
| 22 | 23 | let atStart = buffer.saveExcursion { buffer.tableBegin() } == context.caret |
| 23 | | return TableRecalculate.tblfmLine(in: context) != nil || atStart ? TableRecalculate(all: true).run(in: context) : commitBuffer(context) { buffer in |
| 24 | if TableRecalculate.tblfmLine(in: context) != nil || atStart { return TableRecalculate(all: true).run(in: context) } |
| 25 | // `org-table-maybe-eval-formula`, then `org-table-maybe-recalculate-line` on a row |
| 26 | // marked `#`, which aligns, or else `org-table-align`. |
| 27 | let step = commitBuffer(context) { buffer in |
| 24 | 28 | try buffer.tableMaybeEvalFormula() |
| 25 | | buffer.tableAlign() |
| 29 | guard buffer.lookingAtLine("^[ \\t]*\\| *# *(\\||$)") else { return buffer.tableAlign() } |
| 30 | do { |
| 31 | try buffer.tableRecalculate(all: false) |
| 32 | } catch let error as Calc.Unsupported { |
| 33 | throw EmacsBuffer.UserError(message: TableRecalculate.emacsMessage + error.reason) |
| 34 | } |
| 26 | 35 | } |
| 36 | return TableRecalculate.inEmacs(step, form: "(org-ctrl-c-ctrl-c)", in: context) |
| 27 | 37 | } |
| 28 | 38 | if buffer.footnoteReferenceAtPoint() != nil |
| 29 | 39 | || buffer.footnoteDefinitions().contains(where: { $0.begin <= context.caret && context.caret < ($0.contentsBegin ?? $0.end) }) { |
Sources/OrgCore/Compute/TableFormulas.swift
+11 −6
| @@ -950,19 +950,24 @@ public struct TableRecalculate: OrgCommand { |
| 950 | 950 | throw EmacsBuffer.UserError(message: Self.emacsMessage + error.reason) |
| 951 | 951 | } |
| 952 | 952 | } |
| 953 | | guard case .failed(let message) = step, message.hasPrefix(Self.emacsMessage) else { return step } |
| 954 | | // Emacs recalculates the table; its text replaces the table here. |
| 953 | let form = Self.tblfmLine(in: context) != nil ? "(org-table-calc-current-TBLFM)" : all ? "(org-table-recalculate t)" : "(org-table-recalculate nil)" |
| 954 | return Self.inEmacs(step, form: form, in: context) |
| 955 | } |
| 956 | |
| 957 | /// A step that failed with `emacsMessage` as a request to run `form` on the table in |
| 958 | /// Emacs, whose text then replaces the table here; other steps unchanged. |
| 959 | static func inEmacs(_ step: CommandStep, form: String, in context: EditContext) -> CommandStep { |
| 960 | guard case .failed(let message) = step, message.hasPrefix(emacsMessage) else { return step } |
| 955 | 961 | let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options) |
| 956 | | let onTblfm = Self.tblfmLine(in: context) != nil |
| 962 | let onTblfm = tblfmLine(in: context) != nil |
| 957 | 963 | if onTblfm { |
| 958 | 964 | while buffer.forwardLine(-1) == 0, !buffer.lookingAtLine("^[ \\t]*\\|") {} |
| 959 | 965 | } |
| 960 | 966 | let table = buffer.tableBegin()..<buffer.tableEnd() |
| 961 | | let tblfm = onTblfm ? Self.tblfmLine(in: context) ?? "" : buffer.firstTblfm(after: table.upperBound) ?? "" |
| 962 | | let form = onTblfm ? "(org-table-calc-current-TBLFM)" : all ? "(org-table-recalculate t)" : "(org-table-recalculate nil)" |
| 967 | let tblfm = onTblfm ? tblfmLine(in: context) ?? "" : buffer.firstTblfm(after: table.upperBound) ?? "" |
| 963 | 968 | return .external(.emacsTable( |
| 964 | 969 | table: table, point: context.caret, form: form, |
| 965 | | reason: String(message.dropFirst(Self.emacsMessage.count)), lisp: tblfm.contains("'(") |
| 970 | reason: String(message.dropFirst(emacsMessage.count)), lisp: tblfm.contains("'(") |
| 966 | 971 | )) |
| 967 | 972 | } |
| 968 | 973 | } |
Sources/OrgCore/Keymap/Presets.swift
+1 −1
| @@ -998,7 +998,7 @@ public enum KeymapPreset: String, Sendable, CaseIterable { |
| 998 | 998 | command = "org.fill-paragraph" |
| 999 | 999 | [[bind]] |
| 1000 | 1000 | keys = "C-c C-c" |
| 1001 | | command = "org.table.align" |
| 1001 | command = "org.ctrl-c-ctrl-c" |
| 1002 | 1002 | when = "table" |
| 1003 | 1003 | [[bind]] |
| 1004 | 1004 | keys = "C-c C-c" |
Tests/OrgCoreTests/KeymapTests.swift
+1 −1
| @@ -212,6 +212,6 @@ struct CtrlCCtrlCKeyTests { |
| 212 | 212 | } |
| 213 | 213 | #expect(command(at: 1) == "org.ctrl-c-ctrl-c") |
| 214 | 214 | #expect(command(at: 5) == "org.ctrl-c-ctrl-c") |
| 215 | | #expect(command(at: 13) == "org.table.align") |
| 215 | #expect(command(at: 13) == "org.ctrl-c-ctrl-c") |
| 216 | 216 | } |
| 217 | 217 | } |
Tests/OrgCoreTests/TableFormulaEntryTests.swift
+5
| @@ -13,6 +13,8 @@ struct TableFormulaEntryTests { |
| 13 | 13 | let replaced = "| a | b |\n| 1 | =$1*5 |\n#+TBLFM: $2=$1*2::@1$2=x\n" |
| 14 | 14 | let reuse = "| a | b |\n| 2 | = |\n#+TBLFM: $2=$1*2\n\nText\n" |
| 15 | 15 | let last = "| 1 | =$1+1 |\n" |
| 16 | let marked = "| | x | y |\n|---+---+---|\n| # | 2 | |\n| | 3 | |\n#+TBLFM: $3=$2*10\n" |
| 17 | let markedTyped = "| # | 2 | =$2+1 | |\n| # | 5 | | |\n#+TBLFM: $4=$2*2\n" |
| 16 | 18 | let prompt = { (arg: String) in "(cl-letf (((symbol-function 'read-string) (lambda (&rest _) \"\"))) (org-table-eval-formula \(arg)))" } |
| 17 | 19 | let cases: [(String, String, any OrgCommand, [String: String], String)] = [ |
| 18 | 20 | (plain, "*2", TableCommand(.nextField), [:], "(org-table-next-field)"), |
| @@ -26,6 +28,9 @@ struct TableFormulaEntryTests { |
| 26 | 28 | (stored, "| 3", TableEvalFormula(named: false), ["formula": "$1*3"], "(org-table-eval-formula nil \"$1*3\")"), |
| 27 | 29 | (stored, "| 3", TableEvalFormula(named: true), ["formula": "99"], "(org-table-eval-formula '(4) \"99\")"), |
| 28 | 30 | (stored, "| 1 | 2", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"), |
| 31 | (marked, "| 2", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"), |
| 32 | (marked, "| 3", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"), |
| 33 | (markedTyped, "+1", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"), |
| 29 | 34 | (stored, "| 3 | 4", TableEvalFormula(named: false), ["formula": ""], prompt("nil")), |
| 30 | 35 | (stored, "| 1", TableEvalFormula(named: true), ["formula": ""], prompt("'(4)")), |
| 31 | 36 | ] |