Tests/OrgCoreTests/VimTests.swift
337 lines · 15290 bytes
11 symbols in this file
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 ]
227
228 /// Thirty lines of seven characters: line `i` starts at `7 * i`.
229 static let lines = (0..<30).map { String(format: "line%02d\n", $0) }.joined()
230
231 func driven(_ caret: Int, _ keys: String, halfPage: Int? = nil, visible: Range<Int>? = nil) -> Int {
232 var driver = VimDriver(Self.lines, caret: caret)
233 if let halfPage { driver.halfPage = halfPage }
234 driver.visible = visible
235 driver.type(Self.keys(keys))
236 return driver.caret
237 }
238
239 @Test func zzAndZqRunExCommands() {
240 var vim = Vim()
241 let context = Vim.Context(text: "a\n", caret: 0)
242 #expect(vim.feed(KeyChord("Z"), context) == [])
243 #expect(vim.feed(KeyChord("Z"), context) == [.ex("wq")])
244 _ = vim.feed(KeyChord("Z"), context)
245 #expect(vim.feed(KeyChord("Q"), context) == [.ex("q!")])
246 }
247
248 @Test func snipeRepeatsOverTheVisibleText() {
249 let text = "ab x\ncd ab\nab\n"
250 func caret(_ keys: String, visible: Range<Int>?) -> Int {
251 var driver = VimDriver(text, caret: 0)
252 driver.visible = visible
253 driver.type(Self.keys(keys))
254 return driver.caret
255 }
256 // The snipe itself keeps to the line; repeats search what the window shows.
257 #expect(caret("s a b ;", visible: nil) == 0)
258 #expect(caret("s a b ;", visible: 0..<14) == 8)
259 #expect(caret("s a b ; ;", visible: 0..<14) == 11)
260 #expect(caret("s a b ; ; ,", visible: 0..<14) == 8)
261 // After f, ; repeats the f.
262 #expect(caret("s a b f x ;", visible: 0..<14) == 3)
263 }
264
265 @Test func halfPageScrollsFollowTheWindow() {
266 #expect(driven(0, "C-d", halfPage: 5) == 35)
267 #expect(driven(70, "C-u", halfPage: 5) == 35)
268 #expect(driven(0, "C-d") == 105)
269 #expect(driven(0, "3 C-d", halfPage: 5) == 21)
270 }
271
272 @Test func windowLines() {
273 let shown = 70..<140
274 #expect(driven(0, "H", visible: shown) == 70)
275 #expect(driven(0, "3 H", visible: shown) == 84)
276 #expect(driven(0, "2 0 H", visible: shown) == 133)
277 #expect(driven(0, "L", visible: shown) == 133)
278 #expect(driven(0, "2 L", visible: shown) == 126)
279 #expect(driven(0, "M", visible: shown) == 98)
280 #expect(driven(3, "H") == 3)
281 // Linewise with an operator, and a jump.
282 var driver = VimDriver(Self.lines, caret: 70)
283 driver.visible = shown
284 driver.type(Self.keys("d L"))
285 #expect(driver.text == Self.lines.replacingOccurrences(of: (10..<20).map { String(format: "line%02d\n", $0) }.joined(), with: ""))
286 var vim = Vim()
287 let actions = vim.feed(KeyChord("L"), Vim.Context(text: Self.lines, caret: 70, visible: shown))
288 #expect(actions == [.jumped(from: 70), .move(caret: 133)])
289 }
290
291 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
292 func matchesEvil() throws {
293 let evil = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config/emacs/.local/straight/repos")
294 guard FileManager.default.fileExists(atPath: evil.appendingPathComponent("evil/evil.el").path) else { return }
295 var forms: [EmacsOracle.Case] = []
296 for (text, point, keys) in Self.cases {
297 forms.append(EmacsOracle.Case(text: text, point: point + 1, form: """
298 (progn
299 (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)
300 (add-to-list 'load-path "\(evil.path)/evil")
301 (add-to-list 'load-path "\(evil.path)/goto-chg")
302 (add-to-list 'load-path "\(evil.path)/evil-snipe")
303 (require 'evil)
304 (require 'evil-snipe)
305 (setq evil-snipe-smart-case t evil-snipe-scope 'line evil-snipe-repeat-scope 'visible evil-snipe-char-fold t)
306 (setq kill-ring nil)
307 (switch-to-buffer (current-buffer))
308 (evil-local-mode 1)
309 (evil-snipe-local-mode 1)
310 (evil-snipe-override-local-mode 1)
311 (evil-normal-state)
312 (execute-kbd-macro (kbd "\(keys.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"").replacingOccurrences(of: "ESC", with: "<escape>").replacingOccurrences(of: "DEL", with: "DEL"))"))
313 (let ((p (point)) (s evil-state))
314 (goto-char (point-max))
315 (insert (format "\\n@@STATE %s" s))
316 (goto-char p)))
317 """))
318 }
319 let results = try EmacsOracle.run(forms)
320 for ((text, point, keys), result) in zip(Self.cases, results) {
321 var driver = VimDriver(text, caret: point)
322 // A batch Emacs window shows the whole buffer.
323 driver.visible = 0..<text.utf16.count
324 driver.type(Self.keys(keys))
325 guard let marker = result.text.range(of: "\n@@STATE ") else {
326 Issue.record("\(keys): \(result.error)")
327 continue
328 }
329 let emacsText = String(result.text[..<marker.lowerBound])
330 let state = String(result.text[marker.upperBound...])
331 let ours = driver.vim.mode == .insert ? "insert" : driver.vim.mode == .normal ? "normal" : "visual"
332 let emacsCaret = EmacsOracle.offset(result.point, in: result.text)
333 #expect(driver.text == emacsText && driver.caret == emacsCaret && ours == state,
334 "\(keys.debugDescription) on \(text.debugDescription) at \(point)\nours: \(driver.text.debugDescription) @\(driver.caret) \(ours)\nevil: \(emacsText.debugDescription) @\(emacsCaret) \(state) \(result.error)")
335 }
336 }
337}