krz/orgstar

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

Tests/OrgCoreTests/VimExtrasTests.swift

15f6b0709d88971fb62ed432c3e5b8032643670a
orgstar/Tests/OrgCoreTests/VimExtrasTests.swift history · blame · raw

245 lines · 12588 bytes

  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}