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 {
101101 public func keyLabels(for command: String, modes: [String?] = [nil], limit: Int = 2) -> [String] {
102102 var found: [(keys: [KeyChord], rank: [Int])] = []
103103 for (order, mode) in modes.enumerated() {
104 for binding in bindings.reversed() where binding.mode == mode && binding.command == command
105 && candidates(for: binding.keys, mode: mode).contains(binding) {
104 for binding in bindings.reversed() where binding.mode == mode && binding.command == command && isLive(binding) {
106105 found.append((binding.keys, [order, binding.when == nil ? 0 : 1, binding.keys.count, found.count]))
107106 }
108107 }
@@ -114,6 +113,15 @@ public struct Keymap: Sendable, Equatable {
114113 .map { $0 }
115114 }
116115
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
117125 /// The first live binding of `command`, for showing its keys.
118126 public func keys(for command: String, mode: String? = nil) -> [KeyChord]? {
119127 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 {
8787 command = "app.quick-open"
8888 mode = "normal"
8989 [[bind]]
90 keys = "SPC b b"
91 command = "app.quick-open"
92 mode = "normal"
93 [[bind]]
9490 keys = "SPC f s"
9591 command = "app.save"
9692 mode = "normal"
@@ -576,9 +572,6 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
576572 keys = "C-x C-f"
577573 command = "app.quick-open"
578574 [[bind]]
579 keys = "C-x b"
580 command = "app.quick-open"
581 [[bind]]
582575 keys = "M-x"
583576 command = "app.palette"
584577 [[bind]]
Tests/OrgCoreTests/KeymapTests.swift +21
@@ -98,6 +98,27 @@ struct KeymapTests {
9898 #expect(doom.keyLabels(for: "app.buffer.kill", modes: ["normal"], limit: 3) == ["C-x k", "SPC b d", "SPC b k"])
9999 #expect(doom.keyLabels(for: "app.buffer.next", modes: ["normal"]) == ["] b", "C-x <right>"])
100100 #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"))
101122 }
102123
103124 @Test func dispatchesPrefixes() {