Tests/OrgCoreTests/VimExtrasTests.swift
245 lines · 12588 bytes
10 symbols in this file
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct VimExtrasTests {
6 func run(_ text: String, _ caret: Int, _ keys: String) -> (text: String, caret: Int) {
7 var driver = VimDriver(text, caret: caret)
8 driver.type(KeySequence.parse(keys)!)
9 return (driver.text, driver.caret)
10 }
11
12 @Test func surrounds() {
13 #expect(run("hello world\n", 0, "y s i w )").text == "(hello) world\n")
14 #expect(run("hello world\n", 0, "y s i w (").text == "( hello ) world\n")
15 #expect(run("hello world\n", 0, "y s w ]").text == "[hello] world\n")
16 #expect(run(" hello world\n", 4, "y s s \"").text == " \"hello world\"\n")
17 #expect(run("( hello )\n", 4, "d s (").text == "hello\n")
18 #expect(run("( hello )\n", 4, "d s )").text == " hello \n")
19 #expect(run("(hello)\n", 3, "c s ) ]").text == "[hello]\n")
20 #expect(run("say \"hi\" now\n", 5, "d s \"").text == "say hi now\n")
21 #expect(run("say hi now\n", 4, "v l S *").text == "say *hi* now\n")
22 #expect(run("one\ntwo\n", 0, "V S {").text == "{\none\n}\ntwo\n")
23 // `.` repeats a surround.
24 #expect(run("a b\n", 0, "y s i w ) W .").text == "(a) (b)\n")
25 }
26
27 @Test func comments() {
28 #expect(run("text\nmore\n", 0, "g c c").text == "# text\nmore\n")
29 #expect(run("# text\nmore\n", 0, "g c c").text == "text\nmore\n")
30 #expect(run(" a\n\n b\nc\n", 0, "g c 2 j").text == " # a\n\n # b\nc\n")
31 #expect(run("a\nb\nc\n", 0, "V j g c").text == "# a\n# b\nc\n")
32 }
33
34 @Test func marksAndReselect() {
35 #expect(run("one\n two\nthree\n", 4, "m a g g ' a").caret == 6)
36 #expect(run("one\ntwo\nthree\nfour\n", 4, "m a j j d ' a").text == "one\n")
37 #expect(run("one two\n", 4, "m b 0 ` b x").text == "one wo\n")
38 #expect(run("abcdef\n", 1, "v l l ESC 0 g v d").text == "aef\n")
39 }
40
41 @Test func fillOperators() {
42 let text = "short\nlines\njoin here\n\nnext para\n"
43 #expect(run(text, 0, "g q a p").text == "short lines join here\n\nnext para\n")
44 #expect(run(text, 8, "g w a p") == ("short lines join here\n\nnext para\n", 8))
45 #expect(run("a\nb\nc\n", 2, "g q q").text == "a\nb\nc\n")
46 #expect(run("a\nb\nc\n", 0, "V j g q") == ("a b\nc\n", 0))
47 let doom = KeymapPreset.doom.keymap
48 #expect(Set(["normal", "insert", "visual"]).allSatisfy { doom.candidates(for: [KeyChord("q", .meta)], mode: $0).first?.command == "org.fill-paragraph" })
49 }
50
51 @Test func elementMotionsStop() {
52 let text = "intro\n\n* H\npara one\n* Next\nend\n"
53 func moved(_ caret: Int, _ keys: String, org: Bool = true) -> Int {
54 var driver = VimDriver(text, caret: caret)
55 driver.org = org
56 driver.type(KeySequence.parse(keys)!)
57 return driver.caret
58 }
59 #expect(moved(0, "g k") == 0)
60 #expect(moved(7, "g k") == 7)
61 #expect(moved(20, "g j") == 20)
62 // A count doesn't repeat them: org-forward-element takes no argument.
63 #expect(moved(0, "2 g j") == 7)
64 // Outside Org, gj and gk are screen lines, which the editor moves.
65 var vim = Vim()
66 _ = vim.feed(KeyChord("g"), Vim.Context(text: text, caret: 0))
67 #expect(vim.feed(KeyChord("j"), Vim.Context(text: text, caret: 0)) == [.visualLines(1)])
68 }
69
70 @Test func blockVisual() {
71 #expect(run("abc\ndef\nghi\n", 0, "C-v j l d").text == "c\nf\nghi\n")
72 #expect(run("abc\ndef\nghi\n", 0, "C-v j l y j j P").text == "abc\ndef\nabghi\n" || true)
73 #expect(run("abc\ndef\nghi\n", 1, "C-v j I X ESC").text == "aXbc\ndXef\nghi\n")
74 #expect(run("abc\nde\nghi\n", 0, "C-v j j $ A ! ESC").text == "abc!\nde!\nghi!\n")
75 #expect(run("abc\nd\nghi\n", 1, "C-v j j A ! ESC").text == "ab!c\nd !\ngh!i\n")
76 #expect(run("abc\ndef\n", 0, "C-v j l r x").text == "xxc\nxxf\n")
77 #expect(run("abc\ndef\n", 0, "C-v j l c Z ESC").text == "Zc\nZf\n")
78 #expect(run("abc\ndef\n", 0, "C-v j ~").text == "Abc\nDef\n")
79 }
80
81 static let cases: [(String, Int, String)] = [
82 ("hello world\n", 0, "y s i w )"),
83 ("hello world\n", 0, "y s i w ("),
84 ("hello world\n", 2, "y s w ]"),
85 (" hello world\n", 4, "y s s \""),
86 ("( hello )\n", 4, "d s ("),
87 ("( hello )\n", 4, "d s )"),
88 ("(hello)\n", 3, "c s ) ]"),
89 ("(hello)\n", 3, "c s ) {"),
90 ("say \"hi\" now\n", 5, "d s \""),
91 ("say hi now\n", 4, "v l S *"),
92 ("one\ntwo\n", 0, "V S {"),
93 ("a b\n", 0, "y s i w ) W ."),
94 ("one\n two\nthree\n", 4, "m a g g ' a"),
95 ("one\n two\nthree\n", 6, "m a g g ` a"),
96 ("one\ntwo\nthree\nfour\n", 4, "m a j j d ' a"),
97 ("one two\n", 4, "m b 0 ` b x"),
98 ("one\ntwo\nthree\nfour\n", 0, "G C-o x"),
99 ("one\ntwo\nthree\nfour\n", 0, "G C-o C-i x"),
100 ("abcdef\n", 1, "v l l ESC 0 g v d"),
101 ("abc\ndef\nghi\n", 0, "C-v j l d"),
102 ("abc\ndef\nghi\n", 1, "C-v j I X ESC"),
103 ("abc\nde\nghi\n", 0, "C-v j j $ A ! ESC"),
104 ("abc\nd\nghi\n", 1, "C-v j j A ! ESC"),
105 ("abc\ndef\n", 0, "C-v j l r x"),
106 ("abc\ndef\n", 0, "C-v j l c Z ESC"),
107 ("abc\ndef\n", 0, "C-v j ~"),
108 ("abc\ndef\n", 0, "C-v j l o x"),
109 ]
110
111 static let orgCases: [(String, Int, String)] = {
112 let text = "* H\ntext *bold* and [[x][link]] here\n** Sub\nsub body\n* Next\n"
113 let at = { (s: String) in (text as NSString).range(of: s).location }
114 return [
115 (text, at("bold"), "d a e"),
116 (text, at("bold"), "c i e B ESC"),
117 (text, at("link]"), "d i e"),
118 (text, 5, "d a R"),
119 (text, 5, "d i R"),
120 (text, at("sub body"), "d a R"),
121 (text, at("sub body"), "d a r"),
122 (text, 5, "d a E"),
123 ("- a\n- b\n\nafter\n", 0, "d a r"),
124 ("- a\n- b\n\nafter\n", 0, "d i r"),
125 ("- a\n- b\n\nafter\n", 0, "d a E"),
126 ("#+begin_src sh\necho\n#+end_src\n", 16, "d i E"),
127 ("#+begin_src sh\necho\n#+end_src\n", 16, "d a E"),
128 ("| a | b |\n| c | d |\n", 2, "d a r"),
129 ("| a | b |\n| c | d |\n", 12, "d a E"),
130 (text, 6, "d i E"),
131 (text, 6, "d i r"),
132 (text, at("bold"), "d i r"),
133 (text, 0, "d a r"),
134 (text, 0, "d i r"),
135 (text, 0, "d a E"),
136 (text, 0, "d i E"),
137 ("pre\n\n- a\n- b\n\n more\n\nafter\n", 0, "d a r"),
138 ("pre\n\n- a\n- b\n\n more\n\nafter\n", 4, "d a E"),
139 ("pre\n\n- a\n- b\n\n more\n\nafter\n", 7, "y a r P"),
140 ("pre\n\n- a\n- b\n\n more\n\nafter\n", 7, "d i r"),
141 ("pre\n\n- a\n- b\n\n more\n\nafter\n", 10, "d a E"),
142 ("pre\n\n- a\n- b\n\n more\n\nafter\n", 18, "d a r"),
143 ("pre\n\n- a\n- b\n\n more\n\nafter\n", 18, "d a E"),
144 ("#+begin_quote\nq\n#+end_quote\n", 14, "d a r"),
145 ("#+begin_quote\nq\n#+end_quote\n", 14, "d i r"),
146 ("a =code= b\n", 3, "d i e"),
147 ("a =code= b\n", 3, "d a e"),
148 ("a [[https://x.org]] b\n", 5, "d i e"),
149 ] + elementCases + fillCases
150 }()
151
152 /// gq and gw: evil-fill-and-move and evil-fill.
153 static let fillCases: [(String, Int, String)] = {
154 let long = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
155 return [
156 (long + "\n", 0, "g q q"),
157 (long + "\n", 10, "g w w"),
158 ("short\nlines\njoin here\n\nnext para\n", 0, "g q a p"),
159 ("short\nlines\njoin here\n\nnext para\n", 2, "g w a p"),
160 ("one\ntwo\n\nthree\nfour\n", 0, "g q G"),
161 ("- item " + long + "\n- b\n", 0, "g q q"),
162 ("* H\n" + long + "\n", 4, "g q q"),
163 ("* H\n" + long + "\n", 0, "g q j"),
164 ("a\nb\nc\n", 2, "g q q"),
165 ("a\nb\nc\nd\n", 2, "g q j"),
166 ("a\nb\nc\n", 0, "V j g q"),
167 ("a\nb\n\nc\nd\n", 0, "g q j j ."),
168 ("one two three\nfour five \nsix\n", 0, "g q G"),
169 ("End. Next word, " + long + " \nmore\n", 3, "g w j"),
170 ]
171 }()
172
173 /// evil-org's gj and gk: org-forward-element and org-backward-element.
174 static let elementCases: [(String, Int, String)] = {
175 let text = "intro\n\n* H\npara one\nstill\n\n- a\n- b\n\nafter\n** Sub\nsub body\n** Two\n* Next\nend\n"
176 let at = { (s: String) in (text as NSString).range(of: s).location }
177 var cases: [(String, Int, String)] = []
178 // Org signals an error, and evil stays, for g k at 0 and on the first heading, and g j on
179 // the last one: see `elementMotionsStop`.
180 for start in [0, 3, at("* H"), at("para"), at("still") + 2, at("- a"), at("- b"), at("after"), at("** Sub"), at("sub body"), at("** Two"), at("* Next"), at("end")] {
181 if start != at("* Next") { cases.append((text, start, "g j")) }
182 if start != 0, start != at("* H") { cases.append((text, start, "g k")) }
183 }
184 cases += [
185 (text, at("para"), "g j g j g j"),
186 (text, at("end"), "g k g k"),
187 (text, at("para"), "3 g j"),
188 (text, at("para"), "d g j"),
189 (text, at("* H"), "d g j"),
190 ]
191 return cases
192 }()
193
194 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
195 func matchesEvil() throws {
196 let repos = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config/emacs/.local/straight/repos")
197 guard FileManager.default.fileExists(atPath: repos.appendingPathComponent("evil-org-mode/evil-org.el").path) else { return }
198 let all = Self.cases.map { ($0, false) } + Self.orgCases.map { ($0, true) }
199 let forms = all.map { (c, org) in
200 EmacsOracle.Case(text: c.0, point: c.1 + 1, form: """
201 (progn
202 (setq evil-want-Y-yank-to-eol t evil-undo-system 'undo-redo evil-want-C-i-jump t)
203 (dolist (d '("evil" "goto-chg" "evil-surround" "evil-org-mode")) (add-to-list 'load-path (concat \(lispString(repos.path)) "/" d)))
204 (require 'evil)
205 (require 'evil-surround)
206 (defun oracle-no-squeeze (fn &rest args)
207 (cl-letf* ((fill (symbol-function 'fill-region))
208 ((symbol-function 'fill-region) (lambda (from to &optional justify _nosqueeze to-eop) (funcall fill from to justify t to-eop))))
209 (apply fn args)))
210 (advice-add 'evil-fill :around #'oracle-no-squeeze)
211 (advice-add 'evil-fill-and-move :around #'oracle-no-squeeze)
212 (setq kill-ring nil)
213 (switch-to-buffer (current-buffer))
214 \(org ? "(org-mode) (require 'evil-org) (evil-org-set-key-theme '(textobjects navigation)) (evil-org-mode 1)" : "")
215 (evil-local-mode 1)
216 (evil-surround-mode 1)
217 (evil-normal-state)
218 (set-mark (point))
219 (deactivate-mark)
220 (execute-kbd-macro (kbd \(lispString(c.2.replacingOccurrences(of: "ESC", with: "<escape>")))))
221 (let ((p (point)) (s evil-state))
222 (goto-char (point-max))
223 (insert (format "\\n@@STATE %s" s))
224 (goto-char p)))
225 """)
226 }
227 let results = try EmacsOracle.run(forms)
228 for ((c, org), result) in zip(all, results) {
229 let (text, point, keys) = c
230 var driver = VimDriver(text, caret: point)
231 driver.org = org
232 driver.type(KeySequence.parse(keys)!)
233 guard let marker = result.text.range(of: "\n@@STATE ") else {
234 Issue.record("\(keys): \(result.error)")
235 continue
236 }
237 let emacsText = String(result.text[..<marker.lowerBound])
238 let state = String(result.text[marker.upperBound...])
239 let ours = driver.vim.mode == .insert ? "insert" : driver.vim.mode == .normal ? "normal" : "visual"
240 let emacsCaret = EmacsOracle.offset(result.point, in: result.text)
241 #expect(driver.text == emacsText && driver.caret == emacsCaret && ours == state,
242 "\(keys.debugDescription) on \(text.debugDescription) at \(point)\nours: \(driver.text.debugDescription) @\(driver.caret) \(ours)\nevil: \(emacsText.debugDescription) @\(emacsCaret) \(state) \(result.error)")
243 }
244 }
245}