Tests/OrgCoreTests/VimTests.swift
355 lines · 16126 bytes
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// Plays keys through `Vim` the way the editor does: printable keys in insert state type,
6/// actions apply to the text.
7struct VimDriver {
8 var vim = Vim()
9 var text: String
10 var caret: Int
11 var clipboard: String?
12 var commands: [String] = []
13 var halfPage = 15
14 var visible: Range<Int>?
15 var org = false
16
17 init(_ text: String, caret: Int) {
18 self.text = text
19 self.caret = caret
20 }
21
22 mutating func type(_ keys: [KeyChord]) {
23 for key in keys {
24 if vim.mode == .insert, vim.passesThrough(key) {
25 if key == KeyChord("DEL") {
26 if caret > 0 {
27 let ns = text as NSString
28 text = ns.replacingCharacters(in: NSRange(location: caret - 1, length: 1), with: "")
29 caret -= 1
30 vim.deletedBackward()
31 }
32 } else {
33 let s = key.key == "SPC" ? " " : key.key == "RET" ? "\n" : key.key == "TAB" ? "\t" : key.key
34 guard key.modifiers.isEmpty, s.count == 1 else { continue }
35 let ns = text as NSString
36 text = ns.replacingCharacters(in: NSRange(location: caret, length: 0), with: s)
37 caret += s.utf16.count
38 vim.inserted(s)
39 }
40 continue
41 }
42 let actions = vim.feed(key, Vim.Context(text: text, caret: caret, clipboard: clipboard, halfPage: halfPage, visible: visible, org: org))
43 for action in actions {
44 switch action {
45 case .edit(let edits, let to):
46 for edit in edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { text = edit.apply(to: text) }
47 caret = to
48 case .move(let to): caret = to
49 case .setClipboard(let s): clipboard = s
50 case .command(let id): commands.append(id)
51 case .fill(let range, let keep):
52 let result = runCommand(FillRegion(range: range, keep: keep), text, caret: caret)
53 text = result.text
54 caret = result.caret
55 default: break
56 }
57 }
58 }
59 }
60}
61
62struct VimTests {
63 static func keys(_ s: String) -> [KeyChord] { KeySequence.parse(s)! }
64
65 static let cases: [(String, Int, String)] = [
66 ("hello world foo\nsecond line here\n", 0, "d w"),
67 ("hello world foo\nsecond line here\n", 0, "j c i w X ESC"),
68 ("hello world foo\nsecond line here\n", 6, "d i w"),
69 ("hello world foo\nsecond line here\n", 6, "d a w"),
70 ("hello world foo\nsecond line here\n", 0, "2 d w"),
71 ("hello world foo\nsecond line here\n", 0, "d 2 w"),
72 ("hello world foo\nsecond line here\n", 12, "d w"),
73 ("hello world foo\nsecond line here\n", 4, "x"),
74 ("hello world foo\nsecond line here\n", 4, "3 x"),
75 ("hello world foo\nsecond line here\n", 4, "X"),
76 ("hello world foo\nsecond line here\n", 4, "D"),
77 ("hello world foo\nsecond line here\n", 4, "C n e w ESC"),
78 ("hello world foo\nsecond line here\n", 4, "d d"),
79 ("one\ntwo\nthree\nfour\n", 4, "2 d d"),
80 ("one\ntwo\nthree\nfour\n", 14, "d d"),
81 ("one\ntwo\nthree\nfour", 14, "d d"),
82 ("one\ntwo\nthree\n", 0, "y y p"),
83 ("one\ntwo\nthree\n", 0, "y y P"),
84 ("one\ntwo\nthree\n", 4, "y w $ p"),
85 ("one\ntwo\nthree\n", 0, "d d p"),
86 ("one two three\n", 0, "d w w P"),
87 ("one two three\n", 4, "Y $ p"),
88 ("one\ntwo\nthree\n", 0, "J"),
89 ("one\n two\nthree\n", 0, "3 J"),
90 ("one two\n", 0, "r X"),
91 ("one two\n", 0, "~ ~"),
92 ("Hello World\n", 0, "g U i w"),
93 ("Hello World\n", 0, "g u u"),
94 ("one two\n", 0, "o n e w ESC"),
95 ("one two\n", 4, "O n e w ESC"),
96 (" one two\n", 6, "I x ESC"),
97 ("one two\n", 2, "A ! ESC"),
98 ("one two\n", 2, "a X ESC"),
99 ("one two three four\n", 0, "d w ."),
100 ("one two three four\n", 0, "c w X ESC w ."),
101 ("one\ntwo\nthree\nfour\n", 0, "d d j ."),
102 ("a(b, c) d\n", 3, "d i ("),
103 ("a(b, c) d\n", 3, "d a ("),
104 ("say \"hi there\" now\n", 7, "c i \" X ESC"),
105 ("say \"hi there\" now\n", 7, "d a \""),
106 ("x = [1, 2, 3]\n", 6, "d i ["),
107 ("f {\n body\n}\n", 6, "d i {"),
108 ("one two three\n", 0, "v e d"),
109 ("one two three\n", 4, "v i w y $ p"),
110 ("one\ntwo\nthree\n", 4, "V d"),
111 ("one\ntwo\nthree\n", 0, "V j d"),
112 ("one\ntwo\nthree\n", 0, "V j y G p"),
113 ("one two three\n", 0, "f t x"),
114 ("one two three\n", 0, "t t x"),
115 ("one two three\n", 12, "F o x"),
116 ("one two three\n", 0, "f e ; x"),
117 ("one two three\n", 0, "d f t"),
118 ("one two three\n", 0, "d t t"),
119 ("one two three\n", 0, "$ x"),
120 ("one two three\n", 8, "0 x"),
121 (" one two\n", 8, "^ x"),
122 ("one\ntwo\nthree\n", 0, "G x"),
123 ("one\ntwo\nthree\n", 10, "g g x"),
124 ("one\ntwo\nthree\n", 0, "2 G x"),
125 ("one two three\n", 0, "e x"),
126 ("one two three\n", 0, "w w b x"),
127 ("one.two three\n", 0, "w x"),
128 ("one.two three\n", 0, "W x"),
129 ("one.two three\n", 12, "B x"),
130 ("one two three\n", 0, "d e"),
131 ("one two three\n", 8, "d b"),
132 ("para one\nstill\n\npara two\n", 0, "} x"),
133 ("para one\nstill\n\npara two\n", 0, "d }"),
134 ("para one\nstill\n\npara two\n", 0, "d a p"),
135 ("para one\nstill\n\npara two\n", 0, "y i p G p"),
136 ("(a (b) c)\n", 0, "% x"),
137 ("one two\nthree four\n", 0, "j l l l k x"),
138 ("long line here\nab\nlonger line\n", 10, "j j x"),
139 ("one two three\n", 0, "s t h x"),
140 ("one two three\n", 12, "S t w x"),
141 ("ab ab ab ab\n", 0, "s a b ; ; x"),
142 ("ab ab ab ab\n", 0, "s a b ; ; , x"),
143 ("ab ab ab ab\n", 10, "S a b ; x"),
144 ("ab ab ab ab ab ab\n", 0, "2 s a b ; x"),
145 ("one tw tw tw\n", 0, "f w s t w ; x"),
146 ("one two three\n", 0, "\" a y w w \" a P"),
147 ("one two three\n", 0, "\" _ d w"),
148 (" one\n", 2, "o x ESC"),
149 ("- item\n", 3, "o x ESC"),
150 ("- [ ] item\n", 3, "o x ESC"),
151 ("1. item\n", 3, "o x ESC"),
152 ("text\n- item\n", 8, "O x ESC"),
153 ("- item\n more\n", 9, "o x ESC"),
154 ("* Heading\n", 3, "o x ESC"),
155 ("one two three\n", 0, "v e ~"),
156 ("one two three\n", 0, "v e U"),
157 ("one\ntwo\nthree\n", 0, "V j >"),
158 ("one\ntwo\nthree\n", 0, "v j J"),
159 ("one two three\n", 0, "y i w w v e p"),
160 ("a (b c) d\n", 0, "c i ( X ESC"),
161 ("one two\n", 4, "d a w"),
162 ("one two\n", 4, "d i w"),
163 ("one two three four\n", 0, "d 2 a w"),
164 ("one two three\n", 4, "c $ X ESC"),
165 ("one two three\n", 8, "d 0"),
166 (" one two\n", 8, "d ^"),
167 ("one\ntwo\nthree\n", 4, "d G"),
168 ("one\ntwo\nthree\n", 4, "d g g"),
169 ("one two three\n", 4, "y $ P"),
170 ("ab\n", 0, "x p"),
171 ("one\ntwo\n", 0, "d d p"),
172 ("abc\n", 1, "5 x"),
173 ("abcdef\n", 1, "3 r x"),
174 ("one two three four\n", 0, "x . ."),
175 ("one two three four five\n", 0, "d w 2 ."),
176 ("one two\n", 3, "c w X ESC"),
177 ("aXbXcXd\n", 0, "2 f X x"),
178 ("one two three\n", 0, "w v $ d"),
179 ("one\ntwo\nthree\n", 0, "j V d p"),
180 ("x\n\n\ny\n", 0, "d }"),
181 ("one two\n", 0, "e a X ESC b ."),
182 ("one\n\nthree\n", 0, "j o x ESC"),
183 ("one two\n", 0, "g U U"),
184 ("a b\n", 0, "w i x ESC h ."),
185 ("one two three\n", 4, "c c X ESC"),
186 (" one two\n", 4, "c c X ESC"),
187 ("one two\n", 0, "> >"),
188 (" one two\n", 0, "< <"),
189 ("one two\n", 0, "i a b DEL c ESC"),
190 ("one two\n", 0, "i x C-w y ESC"),
191 ("one two\n", 3, "a X ESC . ."),
192 ("one\ntwo\n", 0, "A ; ESC j ."),
193 ("one two three\n", 0, "3 l v l d"),
194 ("one two three\n", 8, "v b d"),
195 ("one two three\n", 0, "v 2 e c X ESC"),
196 // evil-snipe-override-mode: f, F, t and T snipe one character, ; and , repeat over
197 // the visible text, and the snipe's own keys repeat it right after.
198 ("abc abc\nxbz abc\n", 0, "f c ; ; x"),
199 ("abc abc\nxbz abc\n", 0, "f c f f x"),
200 ("abc abc\nxbz abc\n", 0, "f c f F x"),
201 ("abc abc abc\n", 10, "F b f x"),
202 ("abc abc abc abc\n", 10, "F b F x"),
203 ("abc abc\nxbz abc\n", 14, "T a ; x"),
204 ("abc abc\nxbz abc\n", 0, "t c t x"),
205 ("abc abc\nxbz abc\n", 0, "f c l f c x"),
206 ("aAa\n", 0, "f a x"),
207 ("aAa\n", 0, "f A x"),
208 ("abab\n", 0, "t b x"),
209 ("ab ab\n", 4, "T a x"),
210 ("abc\nxbz\n", 2, "t z x"),
211 ("a b c\n", 0, "f SPC x"),
212 ("a b\n", 0, "d f SPC"),
213 ("a b c\n", 0, "t SPC x"),
214 ("one two three\n", 0, "d t e"),
215 ("one two three\n", 0, "c f o X ESC"),
216 ("one two three\n", 12, "d F o"),
217 ("one two three\n", 12, "d T o"),
218 ("one two three\n", 0, "v f e d"),
219 ("one two three\n", 0, "v t e d"),
220 ("one two three\n", 0, "d f e ."),
221 ("a1a2a3a4a5a6\n", 0, "2 f a ; x"),
222 ("ab ab ab\n", 0, "s a b s S x"),
223 ("ab ab ab\n", 0, "s a b d ;"),
224 ("ab ab ab\n", 0, "v s a b d"),
225 ("ab x\ncd ab\nab\n", 5, "s a b ; , x"),
226 // A count after a snipe ends its repeat keys: the key snipes again with the count.
227 ("a1a2a3a4a5a6a7\n", 0, "f a 2 f a x"),
228 ("a1a2a3a4a5a6a7\n", 0, "f a f 2 f a x"),
229 ("a1a2a3a4a5a6a7\n", 0, "f a 2 ; x"),
230 ("a1a2a3a4a3\n", 0, "s a 2 2 s a 3 x"),
231 // evil-snipe-char-fold: a letter matches its variants, in either case unless typed
232 // as a capital.
233 ("x à b á c\n", 0, "f a ; x"),
234 ("x À b á c\n", 0, "f a x"),
235 ("x à b À c\n", 0, "f A x"),
236 ("x ü b ǜ c\n", 0, "f u ; x"),
237 ("x ⓐ a ª\n", 0, "f a ; ; x"),
238 ("x e\u{301}b\n", 0, "t b x"),
239 ("x e\u{301}b\n", 0, "t e x"),
240 ("x é b\n", 0, "s é SPC x"),
241 ("x é É\n", 0, "f É x"),
242 ("a\u{A0}b c\n", 0, "f SPC x"),
243 ("one fi fa\n", 0, "f f x"),
244 ]
245
246 /// Thirty lines of seven characters: line `i` starts at `7 * i`.
247 static let lines = (0..<30).map { String(format: "line%02d\n", $0) }.joined()
248
249 func driven(_ caret: Int, _ keys: String, halfPage: Int? = nil, visible: Range<Int>? = nil) -> Int {
250 var driver = VimDriver(Self.lines, caret: caret)
251 if let halfPage { driver.halfPage = halfPage }
252 driver.visible = visible
253 driver.type(Self.keys(keys))
254 return driver.caret
255 }
256
257 @Test func zzAndZqRunExCommands() {
258 var vim = Vim()
259 let context = Vim.Context(text: "a\n", caret: 0)
260 #expect(vim.feed(KeyChord("Z"), context) == [])
261 #expect(vim.feed(KeyChord("Z"), context) == [.ex("wq")])
262 _ = vim.feed(KeyChord("Z"), context)
263 #expect(vim.feed(KeyChord("Q"), context) == [.ex("q!")])
264 }
265
266 @Test func snipeRepeatsOverTheVisibleText() {
267 let text = "ab x\ncd ab\nab\n"
268 func caret(_ keys: String, visible: Range<Int>?) -> Int {
269 var driver = VimDriver(text, caret: 0)
270 driver.visible = visible
271 driver.type(Self.keys(keys))
272 return driver.caret
273 }
274 // The snipe itself keeps to the line; repeats search what the window shows.
275 #expect(caret("s a b ;", visible: nil) == 0)
276 #expect(caret("s a b ;", visible: 0..<14) == 8)
277 #expect(caret("s a b ; ;", visible: 0..<14) == 11)
278 #expect(caret("s a b ; ; ,", visible: 0..<14) == 8)
279 // After f, ; repeats the f.
280 #expect(caret("s a b f x ;", visible: 0..<14) == 3)
281 }
282
283 @Test func halfPageScrollsFollowTheWindow() {
284 #expect(driven(0, "C-d", halfPage: 5) == 35)
285 #expect(driven(70, "C-u", halfPage: 5) == 35)
286 #expect(driven(0, "C-d") == 105)
287 #expect(driven(0, "3 C-d", halfPage: 5) == 21)
288 }
289
290 @Test func windowLines() {
291 let shown = 70..<140
292 #expect(driven(0, "H", visible: shown) == 70)
293 #expect(driven(0, "3 H", visible: shown) == 84)
294 #expect(driven(0, "2 0 H", visible: shown) == 133)
295 #expect(driven(0, "L", visible: shown) == 133)
296 #expect(driven(0, "2 L", visible: shown) == 126)
297 #expect(driven(0, "M", visible: shown) == 98)
298 #expect(driven(3, "H") == 3)
299 // Linewise with an operator, and a jump.
300 var driver = VimDriver(Self.lines, caret: 70)
301 driver.visible = shown
302 driver.type(Self.keys("d L"))
303 #expect(driver.text == Self.lines.replacingOccurrences(of: (10..<20).map { String(format: "line%02d\n", $0) }.joined(), with: ""))
304 var vim = Vim()
305 let actions = vim.feed(KeyChord("L"), Vim.Context(text: Self.lines, caret: 70, visible: shown))
306 #expect(actions == [.jumped(from: 70), .move(caret: 133)])
307 }
308
309 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
310 func matchesEvil() throws {
311 let evil = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config/emacs/.local/straight/repos")
312 guard FileManager.default.fileExists(atPath: evil.appendingPathComponent("evil/evil.el").path) else { return }
313 var forms: [EmacsOracle.Case] = []
314 for (text, point, keys) in Self.cases {
315 forms.append(EmacsOracle.Case(text: text, point: point + 1, form: """
316 (progn
317 (setq evil-want-Y-yank-to-eol t evil-undo-system 'undo-redo evil-want-C-w-delete t evil-want-C-u-delete t evil-shift-width 8)
318 (add-to-list 'load-path "\(evil.path)/evil")
319 (add-to-list 'load-path "\(evil.path)/goto-chg")
320 (add-to-list 'load-path "\(evil.path)/evil-snipe")
321 (require 'evil)
322 (require 'evil-snipe)
323 (setq evil-snipe-smart-case t evil-snipe-scope 'line evil-snipe-repeat-scope 'visible evil-snipe-char-fold t)
324 (setq kill-ring nil)
325 (switch-to-buffer (current-buffer))
326 (evil-local-mode 1)
327 (evil-snipe-local-mode 1)
328 (evil-snipe-override-local-mode 1)
329 (evil-normal-state)
330 (execute-kbd-macro (kbd "\(keys.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"").replacingOccurrences(of: "ESC", with: "<escape>").replacingOccurrences(of: "DEL", with: "DEL"))"))
331 (let ((p (point)) (s evil-state))
332 (goto-char (point-max))
333 (insert (format "\\n@@STATE %s" s))
334 (goto-char p)))
335 """))
336 }
337 let results = try EmacsOracle.run(forms)
338 for ((text, point, keys), result) in zip(Self.cases, results) {
339 var driver = VimDriver(text, caret: point)
340 // A batch Emacs window shows the whole buffer.
341 driver.visible = 0..<text.utf16.count
342 driver.type(Self.keys(keys))
343 guard let marker = result.text.range(of: "\n@@STATE ") else {
344 Issue.record("\(keys): \(result.error)")
345 continue
346 }
347 let emacsText = String(result.text[..<marker.lowerBound])
348 let state = String(result.text[marker.upperBound...])
349 let ours = driver.vim.mode == .insert ? "insert" : driver.vim.mode == .normal ? "normal" : "visual"
350 let emacsCaret = EmacsOracle.offset(result.point, in: result.text)
351 #expect(driver.text == emacsText && driver.caret == emacsCaret && ours == state,
352 "\(keys.debugDescription) on \(text.debugDescription) at \(point)\nours: \(driver.text.debugDescription) @\(driver.caret) \(ours)\nevil: \(emacsText.debugDescription) @\(emacsCaret) \(state) \(result.error)")
353 }
354 }
355}