krz/orgstar

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

Tests/OrgCoreTests/KeymapTests.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Tests/OrgCoreTests/KeymapTests.swift history · blame · raw

285 lines · 13513 bytes

  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"), ("org.babel.tangle-block", "C-s-V"), ("org.babel.tangle-target", "C-M-s-v"),
166            ("app.clock.cancel", "C-s-k"), ("org.table.recalc", "C-s-*"),
167        ]
168        for (command, keys) in expected {
169            #expect(mac.keys(for: command).map(KeySequence.format) == keys, "\(command)")
170        }
171    }
172
173    /// A binding whose keys start a longer live binding never runs: the dispatcher waits for more.
174    @Test func presetsBindNoPrefixes() {
175        for preset in KeymapPreset.allCases {
176            let keymap = preset.keymap
177            for binding in keymap.bindings where !binding.unbinds && keymap.isLive(binding) {
178                #expect(!keymap.isPrefix(binding.keys, mode: binding.mode),
179                        "\(preset): \(KeySequence.format(binding.keys)) (\(binding.command)) is also a prefix")
180            }
181        }
182    }
183
184    @Test func shadowedBindingsShowNoKeys() throws {
185        var problems: [String] = []
186        let map = Keymap.layered([KeymapPreset.emacs.keymap, try Keymap(toml: "[[bind]]\nkeys = \"C-c C-t\"\ncommand = \"app.save\"\n", problems: &problems)])
187        #expect(!map.keyLabels(for: "org.todo.cycle").contains("C-c C-t"))
188        #expect(map.keyLabels(for: "app.save").contains("C-c C-t"))
189    }
190
191    @Test func dispatchesPrefixes() {
192        var dispatcher = KeyDispatcher(keymap: KeymapPreset.emacs.keymap)
193        let c = KeyChord("c", .control), t = KeyChord("t", .control), z = KeyChord("z", .control)
194        #expect(dispatcher.feed(c) == .pending([c]))
195        guard case .complete(let keys, let candidates) = dispatcher.feed(t) else {
196            Issue.record("expected a binding")
197            return
198        }
199        #expect(keys == [c, t] && candidates.first?.command == "org.todo.cycle")
200        #expect(dispatcher.feed(c) == .pending([c]))
201        #expect(dispatcher.feed(z) == .undefined([c, z]))
202        #expect(dispatcher.feed(c) == .pending([c]))
203        #expect(dispatcher.feed(KeyDispatcher.quit) == .cancelled)
204        #expect(dispatcher.feed(KeyChord("z")) == .unbound)
205    }
206
207    @Test func continuationsListNextKeys() {
208        let keymap = KeymapPreset.emacs.keymap
209        let next = keymap.continuations(of: [KeyChord("x", .control)])
210        #expect(next.contains { $0.key == KeyChord("s", .control) && $0.command == "app.save" })
211        #expect(keymap.continuations(of: []).contains { $0.key == KeyChord("c", .control) && $0.command == nil })
212    }
213
214    @Test func continuationsFollowContext() {
215        let keymap = KeymapPreset.emacs.keymap
216        let text = "* a\n#+begin_src sh\necho\n#+end_src\n"
217        func hint(at caret: Int) -> String?? {
218            let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
219            return keymap.continuations(of: [KeyChord("c", .control)]) { binding in
220                binding.when.map { KeyContext.holds($0, in: context) } ?? true
221            }.first { $0.key == KeyChord("c", .control) }?.command
222        }
223        #expect(hint(at: 1) == "org.ctrl-c-ctrl-c")
224        #expect(hint(at: 20) == "org.babel.execute")
225    }
226
227    @Test func contexts() {
228        let text = "* a\n| x |\n- item\n"
229        let tree = OrgParser.parse(text)
230        func holds(_ name: String, _ caret: Int, _ selection: Range<Int>? = nil) -> Bool {
231            KeyContext.holds(name, in: EditContext(revision: 0, text: text, tree: tree, selection: [selection ?? caret..<caret]))
232        }
233        #expect(holds("heading", 1) && !holds("heading", 5))
234        #expect(holds("table", 5) && !holds("table", 12))
235        #expect(holds("item", 12))
236        #expect(holds("region", 0, 0..<2) && !holds("region", 0))
237    }
238
239    @Test func presetsParse() {
240        for preset in KeymapPreset.allCases { #expect(!preset.keymap.bindings.isEmpty) }
241    }
242}
243
244struct ToggleKeyTests {
245    @Test func ctrlCStarAndMinusPreferTables() {
246        let keymap = KeymapPreset.emacs.keymap
247        let table = "| a |\n"
248        let tableContext = EditContext(revision: 0, text: table, tree: OrgParser.parse(table), selection: [2..<2])
249        let star = KeySequence.parse("C-c *")!
250        let minus = KeySequence.parse("C-c -")!
251        func first(_ keys: [KeyChord], _ context: EditContext) -> String? {
252            keymap.candidates(for: keys).first { $0.when.map { KeyContext.holds($0, in: context) } ?? true }?.command
253        }
254        #expect(first(star, tableContext) == "org.table.recalc")
255        #expect(first(minus, tableContext) == "org.table.insert-hline")
256        let text = "* a\nb\n"
257        let plain = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [5..<5])
258        #expect(first(star, plain) == "org.heading.toggle")
259        #expect(first(minus, plain) == "org.item.toggle")
260    }
261}
262
263struct DynamicBlockKeyTests {
264    @Test func ctrlCCtrlCUpdatesOnTheBeginLine() {
265        let keymap = KeymapPreset.emacs.keymap
266        let text = "#+BEGIN: clocktable :maxlevel 2\n#+END:\n* a\n"
267        let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [3..<3])
268        let found = keymap.candidates(for: KeySequence.parse("C-c C-c")!).first { $0.when.map { KeyContext.holds($0, in: context) } ?? true }
269        #expect(found?.command == "org.dblock.update")
270    }
271}
272
273struct CtrlCCtrlCKeyTests {
274    @Test func headingsAndItemsGoThroughOrgCtrlCCtrlC() {
275        let keymap = KeymapPreset.emacs.keymap
276        let text = "* a\n- [ ] b\n| x |\n"
277        func command(at offset: Int) -> String? {
278            let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [offset..<offset])
279            return keymap.candidates(for: KeySequence.parse("C-c C-c")!).first { $0.when.map { KeyContext.holds($0, in: context) } ?? true }?.command
280        }
281        #expect(command(at: 1) == "org.ctrl-c-ctrl-c")
282        #expect(command(at: 5) == "org.ctrl-c-ctrl-c")
283        #expect(command(at: 13) == "org.ctrl-c-ctrl-c")
284    }
285}