Commit 0b9419ea7c
Verified · cmc
Layout: unified · split
Sources/OrgCore/Keymap/Keymap.swift +6 −4
| @@ -92,8 +92,9 @@ public struct Keymap: Sendable, Equatable { | |||
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | /// What can follow `prefix`: each next key with the command it runs, or nil when it | 94 | /// 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. | 95 | /// leads to a longer sequence. Sorted by key. The command is the first candidate that |
| 96 | public func continuations(of prefix: [KeyChord], mode: String? = nil) -> [(key: KeyChord, command: String?)] { | 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?)] { | ||
| 97 | var seen: [KeyChord: String?] = [:] | 98 | var seen: [KeyChord: String?] = [:] |
| 98 | for binding in bindings where binding.mode == mode && binding.keys.count > prefix.count && Array(binding.keys.prefix(prefix.count)) == prefix { | 99 | for binding in bindings where binding.mode == mode && binding.keys.count > prefix.count && Array(binding.keys.prefix(prefix.count)) == prefix { |
| 99 | let next = binding.keys[prefix.count] | 100 | let next = binding.keys[prefix.count] |
| @@ -101,8 +102,9 @@ public struct Keymap: Sendable, Equatable { | |||
| 101 | guard seen[next] == nil else { continue } | 102 | guard seen[next] == nil else { continue } |
| 102 | if isPrefix(sequence, mode: mode) { | 103 | if isPrefix(sequence, mode: mode) { |
| 103 | seen[next] = .some(nil) | 104 | seen[next] = .some(nil) |
| 104 | } else if let command = candidates(for: sequence, mode: mode).first?.command { | 105 | } else { |
| 105 | seen[next] = .some(command) | 106 | let found = candidates(for: sequence, mode: mode) |
| 107 | if let command = (found.first(where: runs) ?? found.first)?.command { seen[next] = .some(command) } | ||
| 106 | } | 108 | } |
| 107 | } | 109 | } |
| 108 | return seen.map { (key: $0.key, command: $0.value) }.sorted { $0.key.description < $1.key.description } | 110 | 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 { | |||
| 16 | } | 16 | } |
| 17 | switch dispatcher.feed(chord) { | 17 | switch dispatcher.feed(chord) { |
| 18 | case .pending(let keys): | 18 | 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 | ||
| 20 | (key: item.key.description, title: item.command.map { EditorCommands.command($0)?.title ?? $0 } ?? "+prefix") | 21 | (key: item.key.description, title: item.command.map { EditorCommands.command($0)?.title ?? $0 } ?? "+prefix") |
| 21 | } | 22 | } |
| 22 | onKeysPending?(KeySequence.format(keys), next) | 23 | onKeysPending?(KeySequence.format(keys), next) |
| @@ -27,9 +28,7 @@ extension OrgEditor { | |||
| 27 | case .complete(let keys, let candidates): | 28 | case .complete(let keys, let candidates): |
| 28 | if keys.count > 1 { onKeysPending?(nil, []) } | 29 | if keys.count > 1 { onKeysPending?(nil, []) } |
| 29 | let context = editContext() | 30 | let context = editContext() |
| 30 | if let binding = candidates.first(where: { binding in | 31 | if let binding = candidates.first(where: { modalRuns($0, in: context) }) { |
| 31 | (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && commandApplies(binding.command, in: context) | ||
| 32 | }) { | ||
| 33 | run(binding.command) | 32 | run(binding.command) |
| 34 | updateCursor() | 33 | updateCursor() |
| 35 | return true | 34 | return true |
| @@ -104,6 +103,10 @@ extension OrgEditor { | |||
| 104 | return EditContext(revision: document.revision, text: document.text, tree: document.tree, selection: [selected.location..<NSMaxRange(selected)], options: options) | 103 | return EditContext(revision: document.revision, text: document.text, tree: document.tree, selection: [selected.location..<NSMaxRange(selected)], options: options) |
| 105 | } | 104 | } |
| 106 | 105 | ||
| 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 | |||
| 107 | private func commandApplies(_ id: String, in context: EditContext) -> Bool { | 110 | private func commandApplies(_ id: String, in context: EditContext) -> Bool { |
| 108 | if !isOrg, id.hasPrefix("org.") { return false } | 111 | if !isOrg, id.hasPrefix("org.") { return false } |
| 109 | return switch EditorCommands.command(id)?.action { | 112 | return switch EditorCommands.command(id)?.action { |
Sources/OrgEditorAppKit/OrgEditor.swift +8 −4
| @@ -395,7 +395,8 @@ public final class OrgEditor: NSObject { | |||
| 395 | if modal { return handleModal(chord) } | 395 | if modal { return handleModal(chord) } |
| 396 | switch dispatcher.feed(chord) { | 396 | switch dispatcher.feed(chord) { |
| 397 | case .pending(let keys): | 397 | 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 | ||
| 399 | (key: item.key.description, title: item.command.map { EditorCommands.command($0)?.title ?? $0 } ?? "+prefix") | 400 | (key: item.key.description, title: item.command.map { EditorCommands.command($0)?.title ?? $0 } ?? "+prefix") |
| 400 | } | 401 | } |
| 401 | onKeysPending?(KeySequence.format(keys), next) | 402 | onKeysPending?(KeySequence.format(keys), next) |
| @@ -413,9 +414,7 @@ public final class OrgEditor: NSObject { | |||
| 413 | case .complete(let keys, let candidates): | 414 | case .complete(let keys, let candidates): |
| 414 | if keys.count > 1 { onKeysPending?(nil, []) } | 415 | if keys.count > 1 { onKeysPending?(nil, []) } |
| 415 | let context = context() | 416 | let context = context() |
| 416 | let runnable = candidates.first { binding in | 417 | let runnable = candidates.first { runs($0, in: context) } |
| 417 | (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && applies(binding.command, in: context) | ||
| 418 | } | ||
| 419 | if let binding = runnable ?? (keys.count > 1 ? candidates.first : nil) { | 418 | if let binding = runnable ?? (keys.count > 1 ? candidates.first : nil) { |
| 420 | run(binding.command) | 419 | run(binding.command) |
| 421 | return true | 420 | return true |
| @@ -435,6 +434,11 @@ public final class OrgEditor: NSObject { | |||
| 435 | return true | 434 | return true |
| 436 | } | 435 | } |
| 437 | 436 | ||
| 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 | |||
| 438 | private func applies(_ id: String, in context: EditContext) -> Bool { | 442 | private func applies(_ id: String, in context: EditContext) -> Bool { |
| 439 | if !isOrg, id.hasPrefix("org.") { return false } | 443 | if !isOrg, id.hasPrefix("org.") { return false } |
| 440 | return switch EditorCommands.command(id)?.action { | 444 | return switch EditorCommands.command(id)?.action { |
Tests/OrgCoreTests/KeymapTests.swift +13
| @@ -194,6 +194,19 @@ struct KeymapTests { | |||
| 194 | #expect(keymap.continuations(of: []).contains { $0.key == KeyChord("c", .control) && $0.command == nil }) | 194 | #expect(keymap.continuations(of: []).contains { $0.key == KeyChord("c", .control) && $0.command == nil }) |
| 195 | } | 195 | } |
| 196 | 196 | ||
| 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 | |||
| 197 | @Test func contexts() { | 210 | @Test func contexts() { |
| 198 | let text = "* a\n| x |\n- item\n" | 211 | let text = "* a\n| x |\n- item\n" |
| 199 | let tree = OrgParser.parse(text) | 212 | let tree = OrgParser.parse(text) |
Tests/OrgEditorAppKitTests/ModalTests.swift +15
| @@ -72,6 +72,21 @@ struct ModalTests { | |||
| 72 | #expect(h.editor.vim.mode == .normal) | 72 | #expect(h.editor.vim.mode == .normal) |
| 73 | } | 73 | } |
| 74 | 74 | ||
| 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 | |||
| 75 | @Test func visualSelection() { | 90 | @Test func visualSelection() { |
| 76 | let h = doom("one two three\n") | 91 | let h = doom("one two three\n") |
| 77 | h.caret(at: 0) | 92 | h.caret(at: 0) |