krz/orgstar

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

Commit f36736932d

f36736932de274a2d437c25707091b56f82b1f4e

parent: 9a9fb2ac00

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-06 00:11 UTC

Fix duplicate bindings

C-x b in the Emacs preset and SPC b b in Doom kept their old Quick Open
bindings under the new buffer switch, so C-x b never switched buffers.
Menu and palette key labels now leave out bindings an always-applying
binding takes first, and a test catches a preset binding the same keys
twice.

Layout: unified · split

Sources/OrgCore/Keymap/Keymap.swift +10 −2
@@ -101,8 +101,7 @@ public struct Keymap: Sendable, Equatable {
101 public func keyLabels(for command: String, modes: [String?] = [nil], limit: Int = 2) -> [String] { 101 public func keyLabels(for command: String, modes: [String?] = [nil], limit: Int = 2) -> [String] {
102 var found: [(keys: [KeyChord], rank: [Int])] = [] 102 var found: [(keys: [KeyChord], rank: [Int])] = []
103 for (order, mode) in modes.enumerated() { 103 for (order, mode) in modes.enumerated() {
104 for binding in bindings.reversed() where binding.mode == mode && binding.command == command 104 for binding in bindings.reversed() where binding.mode == mode && binding.command == command && isLive(binding) {
105 && candidates(for: binding.keys, mode: mode).contains(binding) {
106 found.append((binding.keys, [order, binding.when == nil ? 0 : 1, binding.keys.count, found.count])) 105 found.append((binding.keys, [order, binding.when == nil ? 0 : 1, binding.keys.count, found.count]))
107 } 106 }
108 } 107 }
@@ -114,6 +113,15 @@ public struct Keymap: Sendable, Equatable {
114 .map { $0 } 113 .map { $0 }
115 } 114 }
116 115
116 /// Whether `binding` can run: no binding that always applies takes its keys first.
117 func isLive(_ binding: KeyBinding) -> Bool {
118 for candidate in candidates(for: binding.keys, mode: binding.mode) {
119 if candidate == binding { return true }
120 if candidate.when == nil { return false }
121 }
122 return false
123 }
124
117 /// The first live binding of `command`, for showing its keys. 125 /// The first live binding of `command`, for showing its keys.
118 public func keys(for command: String, mode: String? = nil) -> [KeyChord]? { 126 public func keys(for command: String, mode: String? = nil) -> [KeyChord]? {
119 bindings.reversed().first { $0.mode == mode && $0.command == command && candidates(for: $0.keys, mode: mode).contains($0) }?.keys 127 bindings.reversed().first { $0.mode == mode && $0.command == command && candidates(for: $0.keys, mode: mode).contains($0) }?.keys
Sources/OrgCore/Keymap/Presets.swift −7
@@ -87,10 +87,6 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
87 command = "app.quick-open" 87 command = "app.quick-open"
88 mode = "normal" 88 mode = "normal"
89 [[bind]] 89 [[bind]]
90 keys = "SPC b b"
91 command = "app.quick-open"
92 mode = "normal"
93 [[bind]]
94 keys = "SPC f s" 90 keys = "SPC f s"
95 command = "app.save" 91 command = "app.save"
96 mode = "normal" 92 mode = "normal"
@@ -576,9 +572,6 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
576 keys = "C-x C-f" 572 keys = "C-x C-f"
577 command = "app.quick-open" 573 command = "app.quick-open"
578 [[bind]] 574 [[bind]]
579 keys = "C-x b"
580 command = "app.quick-open"
581 [[bind]]
582 keys = "M-x" 575 keys = "M-x"
583 command = "app.palette" 576 command = "app.palette"
584 [[bind]] 577 [[bind]]
Tests/OrgCoreTests/KeymapTests.swift +21
@@ -98,6 +98,27 @@ struct KeymapTests {
98 #expect(doom.keyLabels(for: "app.buffer.kill", modes: ["normal"], limit: 3) == ["C-x k", "SPC b d", "SPC b k"]) 98 #expect(doom.keyLabels(for: "app.buffer.kill", modes: ["normal"], limit: 3) == ["C-x k", "SPC b d", "SPC b k"])
99 #expect(doom.keyLabels(for: "app.buffer.next", modes: ["normal"]) == ["] b", "C-x <right>"]) 99 #expect(doom.keyLabels(for: "app.buffer.next", modes: ["normal"]) == ["] b", "C-x <right>"])
100 #expect(emacs.keyLabels(for: "app.buffer.switch") == ["C-x C-b", "C-x b"]) 100 #expect(emacs.keyLabels(for: "app.buffer.switch") == ["C-x C-b", "C-x b"])
101 #expect(!emacs.keyLabels(for: "app.quick-open").contains("C-x b"))
102 }
103
104 /// Two bindings of the same keys in the same state, both always applying: the earlier one
105 /// can never run.
106 @Test func presetsBindEachKeyOnce() {
107 for preset in KeymapPreset.allCases {
108 var seen: [String: String] = [:]
109 for binding in preset.keymap.bindings where binding.when == nil && !binding.unbinds {
110 let key = "\(binding.mode ?? "-") \(KeySequence.format(binding.keys))"
111 #expect(seen[key] == nil, "\(preset): \(key) is \(seen[key] ?? "") and \(binding.command)")
112 seen[key] = binding.command
113 }
114 }
115 }
116
117 @Test func shadowedBindingsShowNoKeys() throws {
118 var problems: [String] = []
119 let map = Keymap.layered([KeymapPreset.emacs.keymap, try Keymap(toml: "[[bind]]\nkeys = \"C-c C-t\"\ncommand = \"app.save\"\n", problems: &problems)])
120 #expect(!map.keyLabels(for: "org.todo.cycle").contains("C-c C-t"))
121 #expect(map.keyLabels(for: "app.save").contains("C-c C-t"))
101 } 122 }
102 123
103 @Test func dispatchesPrefixes() { 124 @Test func dispatchesPrefixes() {