krz/orgstar

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

Commit 0b9419ea7c

0b9419ea7c722f5d4fc50f85334716b50545ac2b

parent: 1d5593eadf

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 03:39 UTC

Show the key hint for the command that would run

Hints after a prefix pick the binding whose context holds and whose
command applies, as dispatch does, instead of the highest-priority one.

Layout: unified · split

Sources/OrgCore/Keymap/Keymap.swift +6 −4
@@ -92,8 +92,9 @@ public struct Keymap: Sendable, Equatable {
9292 }
9393
9494 /// What can follow `prefix`: each next key with the command it runs, or nil when it
95 /// leads to a longer sequence. Sorted by key.
96 public func continuations(of prefix: [KeyChord], mode: String? = nil) -> [(key: KeyChord, command: String?)] {
95 /// leads to a longer sequence. Sorted by key. The command is the first candidate that
96 /// `runs` accepts, as dispatch picks it, or the first candidate when none does.
97 public func continuations(of prefix: [KeyChord], mode: String? = nil, runs: (KeyBinding) -> Bool = { _ in true }) -> [(key: KeyChord, command: String?)] {
9798 var seen: [KeyChord: String?] = [:]
9899 for binding in bindings where binding.mode == mode && binding.keys.count > prefix.count && Array(binding.keys.prefix(prefix.count)) == prefix {
99100 let next = binding.keys[prefix.count]
@@ -101,8 +102,9 @@ public struct Keymap: Sendable, Equatable {
101102 guard seen[next] == nil else { continue }
102103 if isPrefix(sequence, mode: mode) {
103104 seen[next] = .some(nil)
104 } else if let command = candidates(for: sequence, mode: mode).first?.command {
105 seen[next] = .some(command)
105 } else {
106 let found = candidates(for: sequence, mode: mode)
107 if let command = (found.first(where: runs) ?? found.first)?.command { seen[next] = .some(command) }
106108 }
107109 }
108110 return seen.map { (key: $0.key, command: $0.value) }.sorted { $0.key.description < $1.key.description }
Sources/OrgEditorAppKit/Modal.swift +7 −4
@@ -16,7 +16,8 @@ extension OrgEditor {
1616 }
1717 switch dispatcher.feed(chord) {
1818 case .pending(let keys):
19 let next = keymap.continuations(of: keys, mode: dispatcher.mode).map { item in
19 let context = editContext()
20 let next = keymap.continuations(of: keys, mode: dispatcher.mode) { modalRuns($0, in: context) }.map { item in
2021 (key: item.key.description, title: item.command.map { EditorCommands.command($0)?.title ?? $0 } ?? "+prefix")
2122 }
2223 onKeysPending?(KeySequence.format(keys), next)
@@ -27,9 +28,7 @@ extension OrgEditor {
2728 case .complete(let keys, let candidates):
2829 if keys.count > 1 { onKeysPending?(nil, []) }
2930 let context = editContext()
30 if let binding = candidates.first(where: { binding in
31 (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && commandApplies(binding.command, in: context)
32 }) {
31 if let binding = candidates.first(where: { modalRuns($0, in: context) }) {
3332 run(binding.command)
3433 updateCursor()
3534 return true
@@ -104,6 +103,10 @@ extension OrgEditor {
104103 return EditContext(revision: document.revision, text: document.text, tree: document.tree, selection: [selected.location..<NSMaxRange(selected)], options: options)
105104 }
106105
106 private func modalRuns(_ binding: KeyBinding, in context: EditContext) -> Bool {
107 (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && commandApplies(binding.command, in: context)
108 }
109
107110 private func commandApplies(_ id: String, in context: EditContext) -> Bool {
108111 if !isOrg, id.hasPrefix("org.") { return false }
109112 return switch EditorCommands.command(id)?.action {
Sources/OrgEditorAppKit/OrgEditor.swift +8 −4
@@ -395,7 +395,8 @@ public final class OrgEditor: NSObject {
395395 if modal { return handleModal(chord) }
396396 switch dispatcher.feed(chord) {
397397 case .pending(let keys):
398 let next = keymap.continuations(of: keys).map { item in
398 let context = context()
399 let next = keymap.continuations(of: keys) { runs($0, in: context) }.map { item in
399400 (key: item.key.description, title: item.command.map { EditorCommands.command($0)?.title ?? $0 } ?? "+prefix")
400401 }
401402 onKeysPending?(KeySequence.format(keys), next)
@@ -413,9 +414,7 @@ public final class OrgEditor: NSObject {
413414 case .complete(let keys, let candidates):
414415 if keys.count > 1 { onKeysPending?(nil, []) }
415416 let context = context()
416 let runnable = candidates.first { binding in
417 (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && applies(binding.command, in: context)
418 }
417 let runnable = candidates.first { runs($0, in: context) }
419418 if let binding = runnable ?? (keys.count > 1 ? candidates.first : nil) {
420419 run(binding.command)
421420 return true
@@ -435,6 +434,11 @@ public final class OrgEditor: NSObject {
435434 return true
436435 }
437436
437 /// Whether a key's `binding` runs in `context`: its context holds and its command applies.
438 private func runs(_ binding: KeyBinding, in context: EditContext) -> Bool {
439 (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && applies(binding.command, in: context)
440 }
441
438442 private func applies(_ id: String, in context: EditContext) -> Bool {
439443 if !isOrg, id.hasPrefix("org.") { return false }
440444 return switch EditorCommands.command(id)?.action {
Tests/OrgCoreTests/KeymapTests.swift +13
@@ -194,6 +194,19 @@ struct KeymapTests {
194194 #expect(keymap.continuations(of: []).contains { $0.key == KeyChord("c", .control) && $0.command == nil })
195195 }
196196
197 @Test func continuationsFollowContext() {
198 let keymap = KeymapPreset.emacs.keymap
199 let text = "* a\n#+begin_src sh\necho\n#+end_src\n"
200 func hint(at caret: Int) -> String?? {
201 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
202 return keymap.continuations(of: [KeyChord("c", .control)]) { binding in
203 binding.when.map { KeyContext.holds($0, in: context) } ?? true
204 }.first { $0.key == KeyChord("c", .control) }?.command
205 }
206 #expect(hint(at: 1) == "org.ctrl-c-ctrl-c")
207 #expect(hint(at: 20) == "org.babel.execute")
208 }
209
197210 @Test func contexts() {
198211 let text = "* a\n| x |\n- item\n"
199212 let tree = OrgParser.parse(text)
Tests/OrgEditorAppKitTests/ModalTests.swift +15
@@ -72,6 +72,21 @@ struct ModalTests {
7272 #expect(h.editor.vim.mode == .normal)
7373 }
7474
75 @Test func keyHintsFollowContext() {
76 let h = doom("* a\n#+begin_src sh\necho\n#+end_src\n")
77 var hints: [(key: String, title: String)] = []
78 h.editor.onKeysPending = { _, next in if !next.isEmpty { hints = next } }
79 func hint() -> String? { hints.first { $0.key == "C-c" }?.title }
80 h.caret(at: 1)
81 type(h, "C-c")
82 #expect(hint() != nil && hint() != "Run Source Block")
83 type(h, "C-g")
84 h.caret(at: 20)
85 type(h, "C-c")
86 #expect(hint() == "Run Source Block")
87 type(h, "C-g")
88 }
89
7590 @Test func visualSelection() {
7691 let h = doom("one two three\n")
7792 h.caret(at: 0)