Commit aeb1840b38
Verified · cmc
Layout: unified · split
Sources/OrgCore/Commands/EmacsBuffer.swift +6 −3
| @@ -38,6 +38,8 @@ final class EmacsBuffer { | |||
| 38 | var calendar = Calendar.current | 38 | var calendar = Calendar.current |
| 39 | /// `org-last-inserted-timestamp`. | 39 | /// `org-last-inserted-timestamp`. |
| 40 | var lastInsertedTimestamp: String? | 40 | var lastInsertedTimestamp: String? |
| 41 | /// Shown after the command commits, as `message`. | ||
| 42 | var message: String? | ||
| 41 | 43 | ||
| 42 | let options: EditingOptions | 44 | let options: EditingOptions |
| 43 | 45 | ||
| @@ -466,7 +468,7 @@ final class EmacsBuffer { | |||
| 466 | } | 468 | } |
| 467 | 469 | ||
| 468 | /// Runs a buffer-editing body against a context and turns the result into one minimal edit | 470 | /// Runs a buffer-editing body against a context and turns the result into one minimal edit |
| 469 | /// and the new caret. A user error becomes `.failed`. | 471 | /// and the new caret, with the buffer's message. A user error becomes `.failed`. |
| 470 | func commitBuffer(_ context: EditContext, _ body: (EmacsBuffer) throws -> Void) -> CommandStep { | 472 | func commitBuffer(_ context: EditContext, _ body: (EmacsBuffer) throws -> Void) -> CommandStep { |
| 471 | let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options) | 473 | let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options) |
| 472 | buffer.now = context.now | 474 | buffer.now = context.now |
| @@ -499,9 +501,10 @@ func commitBuffer(_ context: EditContext, _ body: (EmacsBuffer) throws -> Void) | |||
| 499 | while prefix > 0, prefix < old.count, UTF16.isTrailSurrogate(old[prefix]) { prefix -= 1 } | 501 | while prefix > 0, prefix < old.count, UTF16.isTrailSurrogate(old[prefix]) { prefix -= 1 } |
| 500 | while suffix > 0, UTF16.isTrailSurrogate(old[old.count - suffix]) { suffix -= 1 } | 502 | while suffix > 0, UTF16.isTrailSurrogate(old[old.count - suffix]) { suffix -= 1 } |
| 501 | let caret = buffer.point | 503 | let caret = buffer.point |
| 504 | let effects: [Effect] = buffer.message.map { [.message($0)] } ?? [] | ||
| 502 | guard old.count != new.count || prefix != old.count else { | 505 | guard old.count != new.count || prefix != old.count else { |
| 503 | return .commit(EditResult(baseRevision: context.revision, edits: [], selection: [caret..<caret])) | 506 | return .commit(EditResult(baseRevision: context.revision, edits: [], selection: [caret..<caret], effects: effects)) |
| 504 | } | 507 | } |
| 505 | let edit = TextEdit(range: (lower + prefix)..<(lower + old.count - suffix), replacement: String(decoding: new[prefix..<(new.count - suffix)], as: UTF16.self)) | 508 | let edit = TextEdit(range: (lower + prefix)..<(lower + old.count - suffix), replacement: String(decoding: new[prefix..<(new.count - suffix)], as: UTF16.self)) |
| 506 | return .commit(EditResult(baseRevision: context.revision, edits: [edit], selection: [caret..<caret])) | 509 | return .commit(EditResult(baseRevision: context.revision, edits: [edit], selection: [caret..<caret], effects: effects)) |
| 507 | } | 510 | } |
Sources/OrgCore/Compute/TableFormulas.swift +14 −10
| @@ -1012,18 +1012,21 @@ extension EmacsBuffer { | |||
| 1012 | } | 1012 | } |
| 1013 | } | 1013 | } |
| 1014 | 1014 | ||
| 1015 | /// `org-table-get-formula` with EQUATION given: stores it for the current column, or with | 1015 | /// `org-table-get-formula`: stores EQUATION for the current column, or with `named` the |
| 1016 | /// `named` the current field, and returns it. | 1016 | /// current field, and returns it; nil when a blank EQUATION removed the formula. `read` |
| 1017 | func tableGetFormula(_ equation: String, named: Bool, analysis: TableAnalysis) throws -> String { | 1017 | /// when EQUATION is the prompt's answer, which the stored formula never replaces. |
| 1018 | func tableGetFormula(_ equation: String, named: Bool, read: Bool = false, analysis: TableAnalysis) throws -> String? { | ||
| 1018 | var stored = try Self.storedFormulas(firstTblfm(after: tableEnd()) ?? "") | 1019 | var stored = try Self.storedFormulas(firstTblfm(after: tableEnd()) ?? "") |
| 1019 | let ref = "@\(tableCurrentDataLine())$\(tableCurrentColumn())" | 1020 | let ref = "@\(tableCurrentDataLine())$\(tableCurrentColumn())" |
| 1020 | let scol = named ? ref : "$\(tableCurrentColumn())" | 1021 | let scol = named ? ref : "$\(tableCurrentColumn())" |
| 1021 | let old = stored.first { $0.lhs == scol }?.rhs | 1022 | let old = stored.first { $0.lhs == scol }?.rhs |
| 1022 | var eq = old != nil && equation.range(of: "^ *=? *$", options: .regularExpression) != nil ? old! : equation | 1023 | var eq = !read && old != nil && equation.range(of: "^ *=? *$", options: .regularExpression) != nil ? old! : equation |
| 1023 | guard eq.contains(where: { !$0.isWhitespace }) else { | 1024 | guard eq.contains(where: { !$0.isWhitespace }) else { |
| 1024 | stored.removeAll { $0.lhs == scol } | 1025 | stored.removeAll { $0.lhs == scol } |
| 1025 | tableStoreFormulas(stored, analysis: analysis) | 1026 | tableStoreFormulas(stored, analysis: analysis) |
| 1026 | throw UserError(message: "Formula removed") | 1027 | // Org's user error, which keeps the stored formulas and stops the command. |
| 1028 | message = "Formula removed" | ||
| 1029 | return nil | ||
| 1027 | } | 1030 | } |
| 1028 | eq = eq.replacingOccurrences(of: "^ *=?", with: "", options: .regularExpression) | 1031 | eq = eq.replacingOccurrences(of: "^ *=?", with: "", options: .regularExpression) |
| 1029 | eq = eq.replacingOccurrences(of: " *$", with: "", options: .regularExpression) | 1032 | eq = eq.replacingOccurrences(of: " *$", with: "", options: .regularExpression) |
| @@ -1042,14 +1045,15 @@ extension EmacsBuffer { | |||
| 1042 | } | 1045 | } |
| 1043 | 1046 | ||
| 1044 | /// `org-table-eval-formula` for the current field with EQUATION, which is stored first. | 1047 | /// `org-table-eval-formula` for the current field with EQUATION, which is stored first. |
| 1045 | func tableEvalFormula(equation: String, named: Bool) throws { | 1048 | /// A formula only Emacs can evaluate stays stored, the field unchanged. |
| 1049 | func tableEvalFormula(equation: String, named: Bool, read: Bool = false) throws { | ||
| 1046 | try checkInsideDataField() | 1050 | try checkInsideDataField() |
| 1047 | let analysis = try tableAnalyze() | 1051 | let analysis = try tableAnalyze() |
| 1048 | let formula = try tableGetFormula(equation, named: named, analysis: analysis) | 1052 | guard let formula = try tableGetFormula(equation, named: named, read: read, analysis: analysis) else { return } |
| 1049 | do { | 1053 | do { |
| 1050 | try tableEvalFormula(formula, analysis: analysis, substituted: false) | 1054 | try tableEvalFormula(formula, analysis: analysis, substituted: false) |
| 1051 | } catch let error as Calc.Unsupported { | 1055 | } catch let error as Calc.Unsupported { |
| 1052 | throw UserError(message: "The formula was stored; recalculating it needs Emacs: " + error.reason) | 1056 | message = "The formula was stored; recalculating it needs Emacs: " + error.reason |
| 1053 | } | 1057 | } |
| 1054 | tableAlign() | 1058 | tableAlign() |
| 1055 | } | 1059 | } |
| @@ -1085,7 +1089,7 @@ public struct TableEvalFormula: OrgCommand { | |||
| 1085 | let stored = (try? EmacsBuffer.storedFormulas(buffer.firstTblfm(after: buffer.tableEnd()) ?? ""))?.first { $0.lhs == scol }?.rhs | 1089 | let stored = (try? EmacsBuffer.storedFormulas(buffer.firstTblfm(after: buffer.tableEnd()) ?? ""))?.first { $0.lhs == scol }?.rhs |
| 1086 | return .prompt(Prompt(key: "formula", message: "\(named ? "Field" : "Column") formula \(scol)=", initial: stored ?? "")) | 1090 | return .prompt(Prompt(key: "formula", message: "\(named ? "Field" : "Column") formula \(scol)=", initial: stored ?? "")) |
| 1087 | } | 1091 | } |
| 1088 | return commitBuffer(context) { try $0.tableEvalFormula(equation: answer, named: named) } | 1092 | return commitBuffer(context) { try $0.tableEvalFormula(equation: answer, named: named, read: true) } |
| 1089 | } | 1093 | } |
| 1090 | } | 1094 | } |
| 1091 | 1095 | ||
| @@ -1163,7 +1167,7 @@ extension BlockEdit { | |||
| 1163 | } | 1167 | } |
| 1164 | guard case .commit(let result) = step else { return step } | 1168 | guard case .commit(let result) = step else { return step } |
| 1165 | return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection, | 1169 | return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection, |
| 1166 | effects: result.effects + [.message("New formulas installed - press C-u C-c C-c to apply.")])) | 1170 | effects: result.effects + [.message("New formulas installed - Recalculate Table (C-c C-c on the #+TBLFM line) applies them.")])) |
| 1167 | } | 1171 | } |
| 1168 | } | 1172 | } |
| 1169 | 1173 | ||
Tests/OrgCoreTests/TableFormulaEntryTests.swift +31
| @@ -13,6 +13,7 @@ struct TableFormulaEntryTests { | |||
| 13 | let replaced = "| a | b |\n| 1 | =$1*5 |\n#+TBLFM: $2=$1*2::@1$2=x\n" | 13 | let replaced = "| a | b |\n| 1 | =$1*5 |\n#+TBLFM: $2=$1*2::@1$2=x\n" |
| 14 | let reuse = "| a | b |\n| 2 | = |\n#+TBLFM: $2=$1*2\n\nText\n" | 14 | let reuse = "| a | b |\n| 2 | = |\n#+TBLFM: $2=$1*2\n\nText\n" |
| 15 | let last = "| 1 | =$1+1 |\n" | 15 | let last = "| 1 | =$1+1 |\n" |
| 16 | let prompt = { (arg: String) in "(cl-letf (((symbol-function 'read-string) (lambda (&rest _) \"\"))) (org-table-eval-formula \(arg)))" } | ||
| 16 | let cases: [(String, String, any OrgCommand, [String: String], String)] = [ | 17 | let cases: [(String, String, any OrgCommand, [String: String], String)] = [ |
| 17 | (plain, "*2", TableCommand(.nextField), [:], "(org-table-next-field)"), | 18 | (plain, "*2", TableCommand(.nextField), [:], "(org-table-next-field)"), |
| 18 | (plain, "*2", TableCommand(.nextRow), [:], "(org-table-next-row)"), | 19 | (plain, "*2", TableCommand(.nextRow), [:], "(org-table-next-row)"), |
| @@ -25,6 +26,8 @@ struct TableFormulaEntryTests { | |||
| 25 | (stored, "| 3", TableEvalFormula(named: false), ["formula": "$1*3"], "(org-table-eval-formula nil \"$1*3\")"), | 26 | (stored, "| 3", TableEvalFormula(named: false), ["formula": "$1*3"], "(org-table-eval-formula nil \"$1*3\")"), |
| 26 | (stored, "| 3", TableEvalFormula(named: true), ["formula": "99"], "(org-table-eval-formula '(4) \"99\")"), | 27 | (stored, "| 3", TableEvalFormula(named: true), ["formula": "99"], "(org-table-eval-formula '(4) \"99\")"), |
| 27 | (stored, "| 1 | 2", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"), | 28 | (stored, "| 1 | 2", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"), |
| 29 | (stored, "| 3 | 4", TableEvalFormula(named: false), ["formula": ""], prompt("nil")), | ||
| 30 | (stored, "| 1", TableEvalFormula(named: true), ["formula": ""], prompt("'(4)")), | ||
| 28 | ] | 31 | ] |
| 29 | let emacs = try EmacsOracle.run(cases.map { c in | 32 | let emacs = try EmacsOracle.run(cases.map { c in |
| 30 | let caret = NSMaxRange((c.0 as NSString).range(of: c.1)) | 33 | let caret = NSMaxRange((c.0 as NSString).range(of: c.1)) |
| @@ -67,3 +70,31 @@ extension TableFormulaEntryTests { | |||
| 67 | } | 70 | } |
| 68 | } | 71 | } |
| 69 | } | 72 | } |
| 73 | |||
| 74 | extension TableFormulaEntryTests { | ||
| 75 | /// Runs a command that should commit: the new text, the caret and the message. | ||
| 76 | func committed(_ command: any OrgCommand, _ text: String, after needle: String, answers: [String: String] = [:]) -> (text: String, caret: Int, message: String?)? { | ||
| 77 | let caret = NSMaxRange((text as NSString).range(of: needle)) | ||
| 78 | let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret], answers: answers) | ||
| 79 | guard case .commit(let result) = command.run(in: context) else { return nil } | ||
| 80 | var new = text | ||
| 81 | for edit in result.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) } | ||
| 82 | let message = result.effects.compactMap { effect -> String? in if case .message(let m) = effect { m } else { nil } }.first | ||
| 83 | return (new, result.selection?.first?.lowerBound ?? caret, message) | ||
| 84 | } | ||
| 85 | |||
| 86 | @Test func formulaEditsAreKeptWithTheirMessage() throws { | ||
| 87 | let text = "| a | b |\n|---+---|\n| 1 | 2 |\n#+TBLFM: $2=$1*2\n" | ||
| 88 | let removed = try #require(committed(TableEvalFormula(named: false), text, after: "| 1 | 2", answers: ["formula": ""])) | ||
| 89 | #expect(removed.text == "| a | b |\n|---+---|\n| 1 | 2 |\n#+TBLFM: \n" && removed.message == "Formula removed", "\(removed)") | ||
| 90 | // Text in a field is a Calc symbol, which needs Emacs: the formula is stored and TAB | ||
| 91 | // still moves on. | ||
| 92 | let symbolic = "| a | b |\n| x | =$1*2 |\n| y | |\n" | ||
| 93 | let tab = try #require(committed(TableCommand(.nextField), symbolic, after: "*2")) | ||
| 94 | #expect(tab.text == "| a | b |\n| x | =$1*2 |\n| y | |\n#+TBLFM: $2=$1*2\n", "\(tab.text.debugDescription)") | ||
| 95 | #expect(tab.caret == (tab.text as NSString).range(of: "| y").location + 2) | ||
| 96 | #expect(tab.message?.hasPrefix("The formula was stored; recalculating it needs Emacs") == true) | ||
| 97 | let prompted = try #require(committed(TableEvalFormula(named: false), symbolic, after: "| x", answers: ["formula": "$1+1"])) | ||
| 98 | #expect(prompted.text.hasSuffix("#+TBLFM: $1=$1+1\n") && prompted.message != nil, "\(prompted)") | ||
| 99 | } | ||
| 100 | } | ||