Cancel Emacs tasks; Mac preset keys !169

merged merged by cmc on 2026-10-08 05:44 UTC · krz/orgstar:cancel-and-mac-keys into main

7 files changed, +149 −6

Layout: unified · split

Sources/OrgApp/DocumentSession.swift +2
@@ -721,6 +721,8 @@ public final class DocumentSession {
721721 if case .failed(let message) = current.perform(PrecomputedEdit(result), now: Date(), answers: [:]) { return message }
722722 changed(entry)
723723 return "Recalculated in Emacs"
724 } catch is CancellationError {
725 return "Recalculation canceled"
724726 } catch {
725727 return "\(error)"
726728 }
Sources/OrgCore/Keymap/Presets.swift +84
@@ -1225,5 +1225,89 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
12251225 command = "org.timestamp.day-earlier"
12261226 when = "timestamp"
12271227
1228 # Act at point, links, clocking, export and the rest of C-c.
1229 [[bind]]
1230 keys = "C-s-x"
1231 command = "org.ctrl-c-ctrl-c"
1232 [[bind]]
1233 keys = "C-s-x"
1234 command = "org.dblock.update"
1235 when = "dblock"
1236 [[bind]]
1237 keys = "C-s-x"
1238 command = "org.table.recalc-all"
1239 when = "tblfm"
1240 [[bind]]
1241 keys = "C-s-x"
1242 command = "org.babel.execute"
1243 when = "src"
1244 [[bind]]
1245 keys = "s-k"
1246 command = "org.link.insert"
1247 [[bind]]
1248 keys = "C-s-o"
1249 command = "org.link.open"
1250 [[bind]]
1251 keys = "C-s-l"
1252 command = "org.link.store"
1253 [[bind]]
1254 keys = "C-s-i"
1255 command = "app.clock.in"
1256 [[bind]]
1257 keys = "C-S-s-i"
1258 command = "app.clock.out"
1259 [[bind]]
1260 keys = "C-s-j"
1261 command = "app.clock.goto"
1262 [[bind]]
1263 keys = "s-S-e"
1264 command = "app.export-dialog"
1265 [[bind]]
1266 keys = "C-s-n"
1267 command = "org.narrow.toggle"
1268 [[bind]]
1269 keys = "C-S-s-s"
1270 command = "org.sort"
1271 [[bind]]
1272 keys = "C-S-s-p"
1273 command = "org.property.read-and-set"
1274 [[bind]]
1275 keys = "C-S-s-f"
1276 command = "org.footnote.action"
1277 [[bind]]
1278 keys = "C-s-'"
1279 command = "org.edit-special"
1280 [[bind]]
1281 keys = "C-S-s-<right>"
1282 command = "org.todo.next"
1283 when = "heading"
1284 [[bind]]
1285 keys = "C-S-s-<left>"
1286 command = "org.todo.previous"
1287 when = "heading"
1288 [[bind]]
1289 keys = "C-S-s-<right>"
1290 command = "org.property.next-value"
1291 when = "property"
1292 [[bind]]
1293 keys = "C-S-s-<left>"
1294 command = "org.property.previous-value"
1295 when = "property"
1296 [[bind]]
1297 keys = "M-S-s-<down>"
1298 command = "org.heading.forward-same-level"
1299 [[bind]]
1300 keys = "M-S-s-<up>"
1301 command = "org.heading.backward-same-level"
1302 [[bind]]
1303 keys = "C-S-s-e"
1304 command = "org.effort.set"
1305 [[bind]]
1306 keys = "C-s-,"
1307 command = "org.priority.set"
1308 [[bind]]
1309 keys = "C-s-v"
1310 command = "org.babel.tangle"
1311
12281312 """
12291313}
Sources/OrgEditorAppKit/EditorCommands.swift +1 −1
@@ -68,7 +68,7 @@ public enum EditorCommands {
6868 EditorCommand(id: "app.buffer.next", title: "Next Buffer", action: .app),
6969 EditorCommand(id: "app.buffer.previous", title: "Previous Buffer", action: .app),
7070 EditorCommand(id: "app.buffer.last", title: "Last Buffer", action: .app),
71 EditorCommand(id: "app.babel.cancel", title: "Cancel Running Block", action: .app),
71 EditorCommand(id: "app.babel.cancel", title: "Cancel Running Task", action: .app),
7272 EditorCommand(id: "app.toggle-tab-bar", title: "Show or Hide Tab Bar", action: .app),
7373 EditorCommand(id: "app.toggle-outline", title: "Show or Hide Outline", action: .app),
7474 EditorCommand(id: "app.toggle-backlinks", title: "Show or Hide Backlinks", action: .app),
Sources/Orgstar/ContentView.swift +11 −3
@@ -401,7 +401,7 @@ struct ContentView: View {
401401 let functions = ["pdf": "org-latex-export-to-pdf", "odt": "org-odt-export-to-odt", "latex": "org-latex-export-to-latex", "text": "org-ascii-export-to-ascii"]
402402 guard let function = functions[format] else { return }
403403 commands.show("Exporting with Emacs…")
404 Task {
404 commands.runInEmacs {
405405 do {
406406 var written = URL(fileURLWithPath: try await EmacsRunner.export(text: text, path: path, function: function))
407407 if let destination, destination.standardizedFileURL != written.standardizedFileURL {
@@ -411,6 +411,8 @@ struct ContentView: View {
411411 }
412412 commands.show("Exported to \(written.lastPathComponent)")
413413 if open { NSWorkspace.shared.open(written) }
414 } catch is CancellationError {
415 commands.show("Export canceled")
414416 } catch {
415417 commands.show("\(error)")
416418 }
@@ -435,7 +437,7 @@ struct ContentView: View {
435437 case .emacsTable(let table, let point, let form, let reason, let lisp):
436438 let run = {
437439 commands.show("Recalculating in Emacs (\(reason))…")
438 Task { commands.show(await session.recalculateInEmacs(table: table, point: point, form: form)) }
440 commands.runInEmacs { commands.show(await session.recalculateInEmacs(table: table, point: point, form: form)) }
439441 }
440442 guard lisp, let path = session.path, let text = session.document?.text else { return run() }
441443 let content = (text as NSString).substring(with: NSRange(location: table.lowerBound, length: min((text as NSString).length, table.upperBound + 2000) - table.lowerBound))
@@ -550,7 +552,13 @@ struct ContentView: View {
550552 if UnsavedChanges.resolve(session, paths: all) { all.forEach(session.kill) }
551553 case "app.toggle-tab-bar": showTabBar.toggle()
552554 case "app.babel.cancel":
553 if session.isRunningBabel { session.cancelBabel() } else { commands.show("No block is running") }
555 if session.isRunningBabel {
556 session.cancelBabel()
557 } else if let task = commands.emacsTask {
558 task.cancel()
559 } else {
560 commands.show("Nothing is running")
561 }
554562 case "app.toggle-outline": toggleOutline()
555563 case "app.toggle-backlinks": backlinksShown.toggle()
556564 case "app.toggle-inspector": inspectorShown.toggle()
Sources/Orgstar/OrgstarApp.swift +17 −2
@@ -70,6 +70,21 @@ final class AppCommands {
7070 request.reply(text)
7171 }
7272
73 /// An export or table recalculation running in Emacs, for Cancel Running Task.
74 private(set) var emacsTask: Task<Void, Never>?
75 private var emacsSerial = 0
76
77 /// Runs `work` as the Emacs task, cancelling the one before.
78 func runInEmacs(_ work: @escaping @MainActor () async -> Void) {
79 emacsTask?.cancel()
80 emacsSerial += 1
81 let serial = emacsSerial
82 emacsTask = Task {
83 await work()
84 if emacsSerial == serial { emacsTask = nil }
85 }
86 }
87
7388 /// An app command from a menu, run by the main window.
7489 private(set) var runRequest: (id: String, serial: Int)?
7590 private var runSerial = 0
@@ -363,9 +378,9 @@ struct OrgstarApp: App {
363378 FindMenu(commands: commands)
364379 }
365380 CommandGroup(after: .textEditing) {
366 Button("Cancel Running Block") { commands.run("app.babel.cancel") }
381 Button("Cancel Running Task") { commands.run("app.babel.cancel") }
367382 .keyboardShortcut(".")
368 .disabled(!session.isRunningBabel)
383 .disabled(!session.isRunningBabel && commands.emacsTask == nil)
369384 Button("Search Notes") { commands.focusSearch = true }
370385 .keyboardShortcut("f", modifiers: [.command, .shift])
371386 }
Tests/OrgAppTests/AppTests.swift +18
@@ -721,6 +721,24 @@ struct EmacsFallbackTests {
721721 #expect(message == "Recalculated in Emacs")
722722 #expect(session.document?.text == "* Prices\n| item | n | twice |\n|------+---+-------|\n| x | a | 2 a |\n| y | 2 | 4 |\n#+TBLFM: $3=$2*2\n")
723723 }
724
725 @Test(.enabled(if: EmacsRunner.executable() != nil && ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
726 func cancelingARecalculationLeavesTheTable() async throws {
727 let notes = try Folder()
728 let text = "| n | twice |\n|---+-------|\n| a | |\n#+TBLFM: $2=$1*2\n"
729 let file = try notes.write("t.org", text)
730 let session = DocumentSession(recovery: FileRecoveryStore(directory: notes.url.appendingPathComponent(".r")))
731 session.open(file.path)
732 let step = session.perform(TableRecalculate(all: true), path: file.path, at: 2, expect: nil)
733 guard case .external(.emacsTable(let table, let point, let form, _, _)) = step else {
734 Issue.record("\(step)")
735 return
736 }
737 let task = Task { await session.recalculateInEmacs(table: table, point: point, form: form) }
738 task.cancel()
739 #expect(await task.value == "Recalculation canceled")
740 #expect(session.document?.text == text)
741 }
724742}
725743
726744@MainActor
Tests/OrgCoreTests/KeymapTests.swift +16
@@ -153,6 +153,22 @@ struct KeymapTests {
153153 #expect(KeymapPreset.mac.keymap.keys(for: "org.deadline") == [KeyChord("e", [.control, .command])])
154154 }
155155
156 /// The Mac preset reaches links, clocking, export and act-at-point without the palette.
157 @Test func macPresetCoversOrgAreas() {
158 let mac = KeymapPreset.mac.keymap
159 let expected: [(String, String)] = [
160 ("org.ctrl-c-ctrl-c", "C-s-x"), ("org.link.insert", "s-k"), ("org.link.open", "C-s-o"), ("org.link.store", "C-s-l"),
161 ("app.clock.in", "C-s-i"), ("app.clock.out", "C-s-I"), ("app.clock.goto", "C-s-j"), ("app.export-dialog", "s-E"),
162 ("org.narrow.toggle", "C-s-n"), ("org.sort", "C-s-S"), ("org.property.read-and-set", "C-s-P"),
163 ("org.footnote.action", "C-s-F"), ("org.edit-special", "C-s-'"), ("org.todo.next", "C-S-s-<right>"),
164 ("org.heading.forward-same-level", "M-S-s-<down>"), ("org.effort.set", "C-s-E"), ("org.priority.set", "C-s-,"),
165 ("org.babel.tangle", "C-s-v"),
166 ]
167 for (command, keys) in expected {
168 #expect(mac.keys(for: command).map(KeySequence.format) == keys, "\(command)")
169 }
170 }
171
156172 /// A binding whose keys start a longer live binding never runs: the dispatcher waits for more.
157173 @Test func presetsBindNoPrefixes() {
158174 for preset in KeymapPreset.allCases {