Tests/OrgCoreTests/KeymapTests.swift
284 lines · 13362 bytes
31 symbols in this file
KeyNotationTestsparsesAndFormatsTOMLTestsparsesTablesreportsErrorsWithLinesKeymapTestsmapskipsBadEntriesreportsUnknownModesAndCommandslaterLayersWinAndCanUnbindkeyLabelsForMenuspresetsBindEachKeyOncemacPresetAvoidsSystemShortcutsmacPresetCoversOrgAreaspresetsBindNoPrefixesshadowedBindingsShowNoKeysdispatchesPrefixescontinuationsListNextKeyscontinuationsFollowContexthintcontextsholdspresetsParseToggleKeyTestsctrlCStarAndMinusPreferTablesfirstDynamicBlockKeyTestsctrlCCtrlCUpdatesOnTheBeginLineCtrlCCtrlCKeyTestsheadingsAndItemsGoThroughOrgCtrlCCtrlCcommand
1import Testing
2@testable import OrgCore
3
4struct KeyNotationTests {
5 @Test func parsesAndFormats() {
6 #expect(KeySequence.parse("C-c C-x C-i").map(KeySequence.format) == "C-c C-x C-i")
7 #expect(KeyChord.parse("C-M-x") == KeyChord("x", [.control, .meta]))
8 #expect(KeyChord.parse("S-a") == KeyChord("A"))
9 #expect(KeyChord.parse("S-TAB") == KeyChord("TAB", .shift))
10 #expect(KeyChord.parse("<backtab>") == KeyChord("TAB", .shift))
11 #expect(KeyChord.parse("C--") == KeyChord("-", .control))
12 #expect(KeyChord.parse("M-<") == KeyChord("<", .meta))
13 #expect(KeyChord.parse("s-<left>")?.description == "s-<left>")
14 #expect(KeyChord.parse("<tab>") == KeyChord("TAB"))
15 #expect(KeyChord.parse("X-a") == nil)
16 #expect(KeyChord.parse("abc") == nil)
17 #expect(KeySequence.parse("") == nil)
18 }
19}
20
21struct TOMLTests {
22 @Test func parsesTables() throws {
23 let tables = try TOML.parse("""
24 top = 1 # comment
25 [[bind]]
26 keys = "C-c \\"x\\""
27 when = 'heading'
28 on = true
29
30 [options]
31 name = "a\\u00e9"
32 """)
33 #expect(tables.count == 3)
34 #expect(tables[0].values["top"] == .integer(1))
35 #expect(tables[1].name == "bind" && tables[1].isArrayElement && tables[1].line == 2)
36 #expect(tables[1].values["keys"] == .string("C-c \"x\""))
37 #expect(tables[1].values["when"] == .string("heading"))
38 #expect(tables[1].values["on"] == .bool(true))
39 #expect(tables[2].values["name"] == .string("aé"))
40 }
41
42 @Test func reportsErrorsWithLines() {
43 #expect(throws: TOML.ParseError(line: 2, message: "unclosed string")) { try TOML.parse("[[bind]]\nkeys = \"C-c") }
44 #expect(throws: TOML.ParseError(line: 1, message: "expected key = value")) { try TOML.parse("keys") }
45 #expect(throws: TOML.ParseError(line: 2, message: "duplicate key a")) { try TOML.parse("a = 1\na = 2") }
46 }
47}
48
49struct KeymapTests {
50 func map(_ toml: String) throws -> (Keymap, [String]) {
51 var problems: [String] = []
52 let keymap = try Keymap(toml: toml, problems: &problems)
53 return (keymap, problems)
54 }
55
56 @Test func skipsBadEntries() throws {
57 let (keymap, problems) = try map("""
58 [[bind]]
59 keys = "C-c C-t"
60 command = "org.todo.cycle"
61 [[bind]]
62 keys = "Q-x"
63 command = "a"
64 [[bind]]
65 keys = "TAB"
66 command = "a"
67 when = "nowhere"
68 [other]
69 """)
70 #expect(keymap.bindings.count == 1)
71 #expect(problems.count == 3)
72 }
73
74 @Test func reportsUnknownModesAndCommands() throws {
75 var problems: [String] = []
76 let keymap = try Keymap(toml: """
77 [[bind]]
78 keys = "g x"
79 command = "org.todo.cycle"
80 mode = "visual-line"
81 [[bind]]
82 keys = "g y"
83 command = "org.todo.cycel"
84 mode = "normal"
85 [[bind]]
86 keys = "g z"
87 command = "none"
88 mode = "visual"
89 [[bind]]
90 keys = "C-c x"
91 command = "org.todo.cycle"
92 """, problems: &problems, commands: ["org.todo.cycle"])
93 #expect(problems == ["line 1: unknown mode visual-line", "line 5: unknown command org.todo.cycel"])
94 #expect(keymap.bindings.map(\.command) == ["none", "org.todo.cycle"])
95 // Without a command list, ids aren't checked.
96 problems = []
97 _ = try Keymap(toml: "[[bind]]\nkeys = \"x\"\ncommand = \"anything\"\nmode = \"insert\"\n", problems: &problems)
98 #expect(problems.isEmpty)
99 }
100
101 @Test func laterLayersWinAndCanUnbind() throws {
102 let preset = KeymapPreset.emacs.keymap
103 let (user, _) = try map("""
104 [[bind]]
105 keys = "C-c C-t"
106 command = "org.priority.up"
107 [[bind]]
108 keys = "C-k"
109 command = "none"
110 """)
111 let keymap = Keymap.layered([preset, user])
112 #expect(keymap.candidates(for: KeySequence.parse("C-c C-t")!).first?.command == "org.priority.up")
113 #expect(keymap.candidates(for: KeySequence.parse("C-k")!).isEmpty)
114 #expect(keymap.keys(for: "org.todo.next").map(KeySequence.format) == "S-<right>")
115 }
116
117 @Test func keyLabelsForMenus() {
118 let emacs = KeymapPreset.emacs.keymap
119 #expect(emacs.keyLabels(for: "org.todo.cycle") == ["C-c C-t"])
120 #expect(emacs.keyLabels(for: "org.todo.next") == ["S-<right>"])
121 #expect(emacs.keyLabels(for: "org.schedule") == ["C-c C-s"])
122 let doom = KeymapPreset.doom.keymap
123 let labels = doom.keyLabels(for: "org.todo.cycle", modes: ["normal", "insert"])
124 #expect(labels == ["C-c C-t", "SPC m t"])
125 #expect(doom.keyLabels(for: "app.quit", modes: ["normal", "insert"]) == ["SPC q q"])
126 #expect(doom.keyLabels(for: "app.buffer.kill", modes: ["normal"], limit: 3) == ["C-x k", "SPC b d", "SPC b k"])
127 #expect(doom.keyLabels(for: "app.buffer.next", modes: ["normal"]) == ["] b", "C-x <right>"])
128 #expect(emacs.keyLabels(for: "app.buffer.switch") == ["C-x C-b", "C-x b"])
129 #expect(!emacs.keyLabels(for: "app.quick-open").contains("C-x b"))
130 }
131
132 /// Two bindings of the same keys in the same state, both always applying: the earlier one
133 /// can never run.
134 @Test func presetsBindEachKeyOnce() {
135 for preset in KeymapPreset.allCases {
136 var seen: [String: String] = [:]
137 for binding in preset.keymap.bindings where binding.when == nil && !binding.unbinds {
138 let key = "\(binding.mode ?? "-") \(KeySequence.format(binding.keys))"
139 #expect(seen[key] == nil, "\(preset): \(key) is \(seen[key] ?? "") and \(binding.command)")
140 seen[key] = binding.command
141 }
142 }
143 }
144
145 /// ⌃⌘Q locks the screen, ⌃⌘D looks up a word, ⌃⌘F toggles full screen and ⌃⌘Space opens
146 /// the character viewer: the app never sees them.
147 @Test func macPresetAvoidsSystemShortcuts() {
148 let reserved = ["q", "d", "f", "SPC"].map { KeyChord($0, [.control, .command]) }
149 for binding in KeymapPreset.mac.keymap.bindings {
150 #expect(!reserved.contains(binding.keys[0]), "\(KeySequence.format(binding.keys)) is \(binding.command)")
151 }
152 #expect(KeymapPreset.mac.keymap.keys(for: "org.tags.set") == [KeyChord("g", [.control, .command])])
153 #expect(KeymapPreset.mac.keymap.keys(for: "org.deadline") == [KeyChord("e", [.control, .command])])
154 }
155
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
172 /// A binding whose keys start a longer live binding never runs: the dispatcher waits for more.
173 @Test func presetsBindNoPrefixes() {
174 for preset in KeymapPreset.allCases {
175 let keymap = preset.keymap
176 for binding in keymap.bindings where !binding.unbinds && keymap.isLive(binding) {
177 #expect(!keymap.isPrefix(binding.keys, mode: binding.mode),
178 "\(preset): \(KeySequence.format(binding.keys)) (\(binding.command)) is also a prefix")
179 }
180 }
181 }
182
183 @Test func shadowedBindingsShowNoKeys() throws {
184 var problems: [String] = []
185 let map = Keymap.layered([KeymapPreset.emacs.keymap, try Keymap(toml: "[[bind]]\nkeys = \"C-c C-t\"\ncommand = \"app.save\"\n", problems: &problems)])
186 #expect(!map.keyLabels(for: "org.todo.cycle").contains("C-c C-t"))
187 #expect(map.keyLabels(for: "app.save").contains("C-c C-t"))
188 }
189
190 @Test func dispatchesPrefixes() {
191 var dispatcher = KeyDispatcher(keymap: KeymapPreset.emacs.keymap)
192 let c = KeyChord("c", .control), t = KeyChord("t", .control), z = KeyChord("z", .control)
193 #expect(dispatcher.feed(c) == .pending([c]))
194 guard case .complete(let keys, let candidates) = dispatcher.feed(t) else {
195 Issue.record("expected a binding")
196 return
197 }
198 #expect(keys == [c, t] && candidates.first?.command == "org.todo.cycle")
199 #expect(dispatcher.feed(c) == .pending([c]))
200 #expect(dispatcher.feed(z) == .undefined([c, z]))
201 #expect(dispatcher.feed(c) == .pending([c]))
202 #expect(dispatcher.feed(KeyDispatcher.quit) == .cancelled)
203 #expect(dispatcher.feed(KeyChord("z")) == .unbound)
204 }
205
206 @Test func continuationsListNextKeys() {
207 let keymap = KeymapPreset.emacs.keymap
208 let next = keymap.continuations(of: [KeyChord("x", .control)])
209 #expect(next.contains { $0.key == KeyChord("s", .control) && $0.command == "app.save" })
210 #expect(keymap.continuations(of: []).contains { $0.key == KeyChord("c", .control) && $0.command == nil })
211 }
212
213 @Test func continuationsFollowContext() {
214 let keymap = KeymapPreset.emacs.keymap
215 let text = "* a\n#+begin_src sh\necho\n#+end_src\n"
216 func hint(at caret: Int) -> String?? {
217 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
218 return keymap.continuations(of: [KeyChord("c", .control)]) { binding in
219 binding.when.map { KeyContext.holds($0, in: context) } ?? true
220 }.first { $0.key == KeyChord("c", .control) }?.command
221 }
222 #expect(hint(at: 1) == "org.ctrl-c-ctrl-c")
223 #expect(hint(at: 20) == "org.babel.execute")
224 }
225
226 @Test func contexts() {
227 let text = "* a\n| x |\n- item\n"
228 let tree = OrgParser.parse(text)
229 func holds(_ name: String, _ caret: Int, _ selection: Range<Int>? = nil) -> Bool {
230 KeyContext.holds(name, in: EditContext(revision: 0, text: text, tree: tree, selection: [selection ?? caret..<caret]))
231 }
232 #expect(holds("heading", 1) && !holds("heading", 5))
233 #expect(holds("table", 5) && !holds("table", 12))
234 #expect(holds("item", 12))
235 #expect(holds("region", 0, 0..<2) && !holds("region", 0))
236 }
237
238 @Test func presetsParse() {
239 for preset in KeymapPreset.allCases { #expect(!preset.keymap.bindings.isEmpty) }
240 }
241}
242
243struct ToggleKeyTests {
244 @Test func ctrlCStarAndMinusPreferTables() {
245 let keymap = KeymapPreset.emacs.keymap
246 let table = "| a |\n"
247 let tableContext = EditContext(revision: 0, text: table, tree: OrgParser.parse(table), selection: [2..<2])
248 let star = KeySequence.parse("C-c *")!
249 let minus = KeySequence.parse("C-c -")!
250 func first(_ keys: [KeyChord], _ context: EditContext) -> String? {
251 keymap.candidates(for: keys).first { $0.when.map { KeyContext.holds($0, in: context) } ?? true }?.command
252 }
253 #expect(first(star, tableContext) == "org.table.recalc")
254 #expect(first(minus, tableContext) == "org.table.insert-hline")
255 let text = "* a\nb\n"
256 let plain = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [5..<5])
257 #expect(first(star, plain) == "org.heading.toggle")
258 #expect(first(minus, plain) == "org.item.toggle")
259 }
260}
261
262struct DynamicBlockKeyTests {
263 @Test func ctrlCCtrlCUpdatesOnTheBeginLine() {
264 let keymap = KeymapPreset.emacs.keymap
265 let text = "#+BEGIN: clocktable :maxlevel 2\n#+END:\n* a\n"
266 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [3..<3])
267 let found = keymap.candidates(for: KeySequence.parse("C-c C-c")!).first { $0.when.map { KeyContext.holds($0, in: context) } ?? true }
268 #expect(found?.command == "org.dblock.update")
269 }
270}
271
272struct CtrlCCtrlCKeyTests {
273 @Test func headingsAndItemsGoThroughOrgCtrlCCtrlC() {
274 let keymap = KeymapPreset.emacs.keymap
275 let text = "* a\n- [ ] b\n| x |\n"
276 func command(at offset: Int) -> String? {
277 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [offset..<offset])
278 return keymap.candidates(for: KeySequence.parse("C-c C-c")!).first { $0.when.map { KeyContext.holds($0, in: context) } ?? true }?.command
279 }
280 #expect(command(at: 1) == "org.ctrl-c-ctrl-c")
281 #expect(command(at: 5) == "org.ctrl-c-ctrl-c")
282 #expect(command(at: 13) == "org.ctrl-c-ctrl-c")
283 }
284}