Tests/OrgEditorAppKitTests/ModalTests.swift
319 lines · 12048 bytes
25 symbols in this file
ModalTestsdoomtypenormalStateEditsAndInsertTypesundoleaderAndOrgKeysregistersAndMacrosAreShareduppercaseMarksAreGlobalfoldKeysToggleOpenAndClosefillOperatorsInTheEditorfillParagraphFillsTheVisualRegionzzSavesAndClosesAsWqwindowLinesInTheEditorundefinedLeaderSequencesDoNothingkeyHintsFollowContexthintvisualSelectionshiftTabinsertStateTabFollowsDoomshiftTabCyclesInNormalStatereportsStateAndQuitsmacrosReplayKeysjumpsAndMarksPresetCommandTestspresetsBindKnownCommands
1import AppKit
2import OrgCore
3import OrgPresentation
4import Testing
5@testable import OrgEditorAppKit
6
7/// The Doom preset in the editor: keys through the keymap and the modal engine.
8@MainActor
9struct ModalTests {
10 func doom(_ text: String) -> Harness {
11 let h = Harness(text)
12 h.editor.keymap = KeymapPreset.doom.keymap
13 h.editor.modal = true
14 return h
15 }
16
17 /// Types `keys` as key events: letters, `ESC`, `RET`, `TAB`, `SPC`.
18 func type(_ h: Harness, _ keys: String) {
19 let codes: [String: UInt16] = ["ESC": 53, "RET": 36, "TAB": 48, "SPC": 49]
20 for token in keys.split(separator: " ") {
21 let key = String(token)
22 if key.hasPrefix("C-") {
23 let letter = String(key.dropFirst(2))
24 let named = codes[letter]
25 let characters = named == nil ? letter : " "
26 KeyTests().send(h, characters: characters, ignoring: characters, code: named ?? 0, flags: .control)
27 } else if let code = codes[key] {
28 let characters = key == "ESC" ? "\u{1b}" : key == "RET" ? "\r" : key == "TAB" ? "\t" : " "
29 KeyTests().send(h, characters: characters, ignoring: characters, code: code)
30 } else {
31 let shifted = key.first!.isUppercase
32 KeyTests().send(h, characters: key, ignoring: key, code: 0, flags: shifted ? .shift : [])
33 }
34 }
35 }
36
37 @Test func normalStateEditsAndInsertTypes() {
38 let h = doom("hello world\nsecond\n")
39 h.caret(at: 0)
40 type(h, "d w")
41 #expect(h.editor.document.text == "world\nsecond\n")
42 type(h, "c i w")
43 #expect(h.editor.vim.mode == .insert)
44 h.textView.insertText("planet", replacementRange: h.textView.selectedRange())
45 type(h, "ESC")
46 #expect(h.editor.document.text == "planet\nsecond\n")
47 #expect(h.textView.selectedRange().location == 5)
48 type(h, "j .")
49 #expect(h.editor.document.text == "planet\nplanet\n")
50 }
51
52 @Test func undo() {
53 let h = doom("hello world\n")
54 h.caret(at: 0)
55 type(h, "d w")
56 #expect(h.editor.document.text == "world\n")
57 type(h, "u")
58 #expect(h.editor.document.text == "hello world\n")
59 }
60
61 @Test func leaderAndOrgKeys() {
62 let h = doom("* TODO Task\n")
63 var app: [String] = []
64 h.editor.onAppCommand = { app.append($0) }
65 h.caret(at: 2)
66 type(h, "SPC f s")
67 #expect(app == ["app.save"])
68 type(h, "RET")
69 #expect(h.editor.document.text == "* DONE Task\n")
70 type(h, "SPC m t")
71 #expect(h.editor.document.text == "* KILL Task\n" || h.editor.document.text.hasPrefix("* "))
72 type(h, "TAB")
73 #expect(h.editor.vim.mode == .normal)
74 }
75
76 @Test func registersAndMacrosAreShared() {
77 let a = doom("alpha beta\n")
78 let b = doom("one\ntwo\n")
79 a.caret(at: 0)
80 type(a, "\" r y w")
81 b.caret(at: 0)
82 type(b, "\" r P")
83 #expect(b.editor.document.text == "alpha one\ntwo\n")
84 type(a, "q m A ! ESC q")
85 type(b, "j @ m")
86 #expect(b.editor.document.text == "alpha one\ntwo!\n")
87 }
88
89 @Test func uppercaseMarksAreGlobal() {
90 let a = doom("first\n second\n")
91 let b = doom("other\ntext\n")
92 var jumps: [(OrgEditor, Int)] = []
93 b.editor.onJumpToEditor = { jumps.append(($0, $1)) }
94 a.caret(at: 10)
95 type(a, "m Q m q")
96 b.caret(at: 0)
97 type(b, "' Q")
98 #expect(jumps.count == 1 && jumps.first?.0 === a.editor && jumps.first?.1 == 8)
99 type(b, "` Q")
100 #expect(jumps.last?.1 == 10)
101 // Lowercase marks stay in their buffer.
102 type(b, "j ' q")
103 #expect(b.textView.selectedRange().location == 6)
104 // In its own buffer the mark is an ordinary jump, and follows edits.
105 type(a, "g g O")
106 a.textView.insertText("new", replacementRange: a.textView.selectedRange())
107 type(a, "ESC ` Q")
108 #expect(a.textView.selectedRange().location == 14)
109 }
110
111 @Test func foldKeysToggleOpenAndClose() {
112 let h = doom("* A\nbody\n** B\nb body\n* C\n")
113 h.caret(at: 0)
114 type(h, "z c")
115 #expect(h.editor.foldState.folds == [0])
116 type(h, "z o")
117 #expect(h.editor.foldState.folds == [9])
118 type(h, "z o")
119 #expect(h.editor.foldState.folds == [9])
120 type(h, "z a")
121 #expect(h.editor.foldState.folds.contains(0))
122 type(h, "z a")
123 #expect(h.editor.foldState.folds == [9])
124 // za and zo act on a heading line; zc closes the subtree around the caret.
125 h.editor.setFolds(FoldState())
126 h.caret(at: 5)
127 type(h, "z a")
128 type(h, "z o")
129 #expect(h.editor.foldState.folds.isEmpty)
130 h.caret(at: 15)
131 type(h, "z c")
132 #expect(h.editor.foldState.folds == [9])
133 }
134
135 @Test func fillOperatorsInTheEditor() {
136 let h = doom("short\nlines\n\nnext\n")
137 h.caret(at: 7)
138 type(h, "g w a p")
139 #expect(h.editor.document.text == "short lines\n\nnext\n")
140 #expect(h.textView.selectedRange().location == 7)
141 type(h, "u")
142 #expect(h.editor.document.text == "short\nlines\n\nnext\n")
143 }
144
145 /// M-q in visual states fills the region's paragraphs and leaves visual state, as in
146 /// Doom (checked against evil with `org-fill-paragraph`).
147 @Test func fillParagraphFillsTheVisualRegion() {
148 let h = doom("aaa\nbbb\n\nccc\nddd\n\neee\nfff\n")
149 h.caret(at: 0)
150 type(h, "v j j j")
151 KeyTests().press(h, "q", option: KeyEvents.leftOption, typed: "œ")
152 #expect(h.editor.document.text == "aaa bbb\n\nccc ddd\n\neee\nfff\n")
153 #expect(h.textView.selectedRange() == NSRange(location: 9, length: 0))
154 #expect(h.editor.vim.mode == .normal)
155 let lines = doom("aaa\nbbb\nccc\n\nx\ny\n")
156 lines.caret(at: 8)
157 type(lines, "v k")
158 KeyTests().press(lines, "q", option: KeyEvents.leftOption, typed: "œ")
159 #expect(lines.editor.document.text == "aaa bbb ccc\n\nx\ny\n")
160 #expect(lines.textView.selectedRange() == NSRange(location: 4, length: 0))
161 #expect(lines.editor.vim.mode == .normal)
162 }
163
164 @Test func zzSavesAndClosesAsWq() {
165 let h = doom("text\n")
166 var app: [String] = []
167 h.editor.onAppCommand = { app.append($0) }
168 h.caret(at: 0)
169 type(h, "Z Q")
170 #expect(app.isEmpty)
171 type(h, "Z Z")
172 #expect(app == ["app.save"])
173 }
174
175 @Test func windowLinesInTheEditor() {
176 let h = doom("one\ntwo\nthree\nfour\nfive\n")
177 h.caret(at: 0)
178 type(h, "L")
179 #expect(h.textView.selectedRange().location == 19)
180 type(h, "M")
181 #expect(h.textView.selectedRange().location == 8)
182 type(h, "H")
183 #expect(h.textView.selectedRange().location == 0)
184 }
185
186 @Test func undefinedLeaderSequencesDoNothing() {
187 let h = doom("* Task\nbody text\n")
188 var messages: [String] = []
189 h.editor.onMessage = { messages.append($0) }
190 h.caret(at: 0)
191 type(h, "SPC m z")
192 #expect(messages.last == "SPC m z is undefined")
193 #expect(h.textView.selectedRange().location == 0)
194 // No mark z was set: a jump to it fails and the caret stays.
195 type(h, "j ' z")
196 #expect(h.textView.selectedRange().location == 7)
197 type(h, "C-c z")
198 #expect(messages.last == "C-c z is undefined")
199 #expect(h.editor.document.text == "* Task\nbody text\n")
200 // Evil's own prefixes still reach it.
201 type(h, "g g")
202 #expect(h.textView.selectedRange().location == 0)
203 }
204
205 @Test func keyHintsFollowContext() {
206 let h = doom("* a\n#+begin_src sh\necho\n#+end_src\n")
207 var hints: [(key: String, title: String)] = []
208 h.editor.onKeysPending = { _, next in if !next.isEmpty { hints = next } }
209 func hint() -> String? { hints.first { $0.key == "C-c" }?.title }
210 h.caret(at: 1)
211 type(h, "C-c")
212 #expect(hint() != nil && hint() != "Run Source Block")
213 type(h, "C-g")
214 h.caret(at: 20)
215 type(h, "C-c")
216 #expect(hint() == "Run Source Block")
217 type(h, "C-g")
218 }
219
220 @Test func visualSelection() {
221 let h = doom("one two three\n")
222 h.caret(at: 0)
223 type(h, "v e")
224 #expect(h.textView.selectedRange() == NSRange(location: 0, length: 3))
225 type(h, "d")
226 #expect(h.editor.document.text == " two three\n")
227 #expect(h.textView.selectedRange().length == 0)
228 }
229
230 func shiftTab(_ h: Harness) {
231 KeyTests().send(h, characters: "\u{19}", ignoring: "\u{19}", code: 48, flags: .shift)
232 }
233
234 @Test func insertStateTabFollowsDoom() {
235 let h = doom("* Top\n** Sub\n- one\n- item\n - child\nbody\n")
236 h.caret(at: 8)
237 type(h, "i")
238 #expect(h.editor.vim.mode == .insert)
239 shiftTab(h)
240 #expect(h.editor.document.text.hasPrefix("* Top\n* Sub\n"))
241 type(h, "TAB")
242 #expect(h.editor.document.text.hasPrefix("* Top\n** Sub\n"))
243 let item = (h.editor.document.text as NSString).range(of: "- item").location
244 h.caret(at: item + 3)
245 type(h, "TAB")
246 #expect(h.editor.document.text.contains("- one\n - item\n - child\n"))
247 #expect(h.editor.vim.mode == .insert)
248 }
249
250 @Test func shiftTabCyclesInNormalState() {
251 let h = doom("* Top\nbody\n")
252 h.caret(at: 7)
253 let before = h.editor.document.text
254 shiftTab(h)
255 #expect(h.editor.document.text == before)
256 #expect(h.editor.vim.mode == .normal)
257 }
258
259 @Test func reportsStateAndQuits() {
260 let h = doom("text\n")
261 var states: [String?] = []
262 var app: [String] = []
263 h.editor.onModeChange = { states.append($0) }
264 h.editor.onAppCommand = { app.append($0) }
265 h.caret(at: 0)
266 type(h, "i")
267 type(h, "ESC")
268 type(h, "V")
269 type(h, "ESC")
270 type(h, "SPC q q")
271 #expect(states == ["insert", "normal", "visual-line", "normal"])
272 #expect(app == ["app.quit"])
273 h.editor.modal = false
274 #expect(states.last == .some(nil))
275 }
276
277 @Test func macrosReplayKeys() {
278 let h = doom("one two\nthree four\nfive six\nseven\n")
279 h.caret(at: 0)
280 type(h, "q a d w j q")
281 #expect(h.editor.document.text == "two\nthree four\nfive six\nseven\n")
282 type(h, "@ a")
283 #expect(h.editor.document.text == "two\nfour\nfive six\nseven\n")
284 type(h, "@ @")
285 #expect(h.editor.document.text == "two\nfour\nsix\nseven\n")
286 type(h, "g g q b A ! ESC j q 2 @ b")
287 #expect(h.editor.document.text == "two!\nfour!\nsix!\nseven\n", "\(h.editor.document.text.debugDescription)")
288 }
289
290 @Test func jumpsAndMarks() {
291 let h = doom("one\ntwo\nthree\nfour\n")
292 h.caret(at: 4)
293 type(h, "G")
294 #expect(h.textView.selectedRange().location == 19)
295 type(h, "C-o")
296 #expect(h.textView.selectedRange().location == 4)
297 type(h, "C-i")
298 #expect(h.textView.selectedRange().location == 19)
299 type(h, "g g m a G ' a")
300 #expect(h.textView.selectedRange().location == 0)
301 // Marks follow edits above them.
302 type(h, "G m b g g O")
303 h.textView.insertText("new", replacementRange: h.textView.selectedRange())
304 type(h, "ESC ` b")
305 #expect(h.textView.selectedRange().location == 23)
306 }
307}
308
309struct PresetCommandTests {
310 /// Every command a preset binds is one the editor runs, and every mode is a state.
311 @Test func presetsBindKnownCommands() throws {
312 let known = Set(EditorCommands.all.map(\.id))
313 for preset in KeymapPreset.allCases {
314 var problems: [String] = []
315 _ = try Keymap(toml: preset.toml, problems: &problems, commands: known)
316 #expect(problems.isEmpty, "\(preset): \(problems)")
317 }
318 }
319}