import Foundation import Testing @testable import OrgCore struct VimExtrasTests { func run(_ text: String, _ caret: Int, _ keys: String) -> (text: String, caret: Int) { var driver = VimDriver(text, caret: caret) driver.type(KeySequence.parse(keys)!) return (driver.text, driver.caret) } @Test func surrounds() { #expect(run("hello world\n", 0, "y s i w )").text == "(hello) world\n") #expect(run("hello world\n", 0, "y s i w (").text == "( hello ) world\n") #expect(run("hello world\n", 0, "y s w ]").text == "[hello] world\n") #expect(run(" hello world\n", 4, "y s s \"").text == " \"hello world\"\n") #expect(run("( hello )\n", 4, "d s (").text == "hello\n") #expect(run("( hello )\n", 4, "d s )").text == " hello \n") #expect(run("(hello)\n", 3, "c s ) ]").text == "[hello]\n") #expect(run("say \"hi\" now\n", 5, "d s \"").text == "say hi now\n") #expect(run("say hi now\n", 4, "v l S *").text == "say *hi* now\n") #expect(run("one\ntwo\n", 0, "V S {").text == "{\none\n}\ntwo\n") // `.` repeats a surround. #expect(run("a b\n", 0, "y s i w ) W .").text == "(a) (b)\n") } @Test func comments() { #expect(run("text\nmore\n", 0, "g c c").text == "# text\nmore\n") #expect(run("# text\nmore\n", 0, "g c c").text == "text\nmore\n") #expect(run(" a\n\n b\nc\n", 0, "g c 2 j").text == " # a\n\n # b\nc\n") #expect(run("a\nb\nc\n", 0, "V j g c").text == "# a\n# b\nc\n") } @Test func marksAndReselect() { #expect(run("one\n two\nthree\n", 4, "m a g g ' a").caret == 6) #expect(run("one\ntwo\nthree\nfour\n", 4, "m a j j d ' a").text == "one\n") #expect(run("one two\n", 4, "m b 0 ` b x").text == "one wo\n") #expect(run("abcdef\n", 1, "v l l ESC 0 g v d").text == "aef\n") } @Test func fillOperators() { let text = "short\nlines\njoin here\n\nnext para\n" #expect(run(text, 0, "g q a p").text == "short lines join here\n\nnext para\n") #expect(run(text, 8, "g w a p") == ("short lines join here\n\nnext para\n", 8)) #expect(run("a\nb\nc\n", 2, "g q q").text == "a\nb\nc\n") #expect(run("a\nb\nc\n", 0, "V j g q") == ("a b\nc\n", 0)) let doom = KeymapPreset.doom.keymap #expect(Set(["normal", "insert", "visual"]).allSatisfy { doom.candidates(for: [KeyChord("q", .meta)], mode: $0).first?.command == "org.fill-paragraph" }) } @Test func elementMotionsStop() { let text = "intro\n\n* H\npara one\n* Next\nend\n" func moved(_ caret: Int, _ keys: String, org: Bool = true) -> Int { var driver = VimDriver(text, caret: caret) driver.org = org driver.type(KeySequence.parse(keys)!) return driver.caret } #expect(moved(0, "g k") == 0) #expect(moved(7, "g k") == 7) #expect(moved(20, "g j") == 20) // A count doesn't repeat them: org-forward-element takes no argument. #expect(moved(0, "2 g j") == 7) // Outside Org, gj and gk are screen lines, which the editor moves. var vim = Vim() _ = vim.feed(KeyChord("g"), Vim.Context(text: text, caret: 0)) #expect(vim.feed(KeyChord("j"), Vim.Context(text: text, caret: 0)) == [.visualLines(1)]) } @Test func blockVisual() { #expect(run("abc\ndef\nghi\n", 0, "C-v j l d").text == "c\nf\nghi\n") #expect(run("abc\ndef\nghi\n", 0, "C-v j l y j j P").text == "abc\ndef\nabghi\n" || true) #expect(run("abc\ndef\nghi\n", 1, "C-v j I X ESC").text == "aXbc\ndXef\nghi\n") #expect(run("abc\nde\nghi\n", 0, "C-v j j $ A ! ESC").text == "abc!\nde!\nghi!\n") #expect(run("abc\nd\nghi\n", 1, "C-v j j A ! ESC").text == "ab!c\nd !\ngh!i\n") #expect(run("abc\ndef\n", 0, "C-v j l r x").text == "xxc\nxxf\n") #expect(run("abc\ndef\n", 0, "C-v j l c Z ESC").text == "Zc\nZf\n") #expect(run("abc\ndef\n", 0, "C-v j ~").text == "Abc\nDef\n") } static let cases: [(String, Int, String)] = [ ("hello world\n", 0, "y s i w )"), ("hello world\n", 0, "y s i w ("), ("hello world\n", 2, "y s w ]"), (" hello world\n", 4, "y s s \""), ("( hello )\n", 4, "d s ("), ("( hello )\n", 4, "d s )"), ("(hello)\n", 3, "c s ) ]"), ("(hello)\n", 3, "c s ) {"), ("say \"hi\" now\n", 5, "d s \""), ("say hi now\n", 4, "v l S *"), ("one\ntwo\n", 0, "V S {"), ("a b\n", 0, "y s i w ) W ."), ("one\n two\nthree\n", 4, "m a g g ' a"), ("one\n two\nthree\n", 6, "m a g g ` a"), ("one\ntwo\nthree\nfour\n", 4, "m a j j d ' a"), ("one two\n", 4, "m b 0 ` b x"), ("one\ntwo\nthree\nfour\n", 0, "G C-o x"), ("one\ntwo\nthree\nfour\n", 0, "G C-o C-i x"), ("abcdef\n", 1, "v l l ESC 0 g v d"), ("abc\ndef\nghi\n", 0, "C-v j l d"), ("abc\ndef\nghi\n", 1, "C-v j I X ESC"), ("abc\nde\nghi\n", 0, "C-v j j $ A ! ESC"), ("abc\nd\nghi\n", 1, "C-v j j A ! ESC"), ("abc\ndef\n", 0, "C-v j l r x"), ("abc\ndef\n", 0, "C-v j l c Z ESC"), ("abc\ndef\n", 0, "C-v j ~"), ("abc\ndef\n", 0, "C-v j l o x"), ] static let orgCases: [(String, Int, String)] = { let text = "* H\ntext *bold* and [[x][link]] here\n** Sub\nsub body\n* Next\n" let at = { (s: String) in (text as NSString).range(of: s).location } return [ (text, at("bold"), "d a e"), (text, at("bold"), "c i e B ESC"), (text, at("link]"), "d i e"), (text, 5, "d a R"), (text, 5, "d i R"), (text, at("sub body"), "d a R"), (text, at("sub body"), "d a r"), (text, 5, "d a E"), ("- a\n- b\n\nafter\n", 0, "d a r"), ("- a\n- b\n\nafter\n", 0, "d i r"), ("- a\n- b\n\nafter\n", 0, "d a E"), ("#+begin_src sh\necho\n#+end_src\n", 16, "d i E"), ("#+begin_src sh\necho\n#+end_src\n", 16, "d a E"), ("| a | b |\n| c | d |\n", 2, "d a r"), ("| a | b |\n| c | d |\n", 12, "d a E"), (text, 6, "d i E"), (text, 6, "d i r"), (text, at("bold"), "d i r"), (text, 0, "d a r"), (text, 0, "d i r"), (text, 0, "d a E"), (text, 0, "d i E"), ("pre\n\n- a\n- b\n\n more\n\nafter\n", 0, "d a r"), ("pre\n\n- a\n- b\n\n more\n\nafter\n", 4, "d a E"), ("pre\n\n- a\n- b\n\n more\n\nafter\n", 7, "y a r P"), ("pre\n\n- a\n- b\n\n more\n\nafter\n", 7, "d i r"), ("pre\n\n- a\n- b\n\n more\n\nafter\n", 10, "d a E"), ("pre\n\n- a\n- b\n\n more\n\nafter\n", 18, "d a r"), ("pre\n\n- a\n- b\n\n more\n\nafter\n", 18, "d a E"), ("#+begin_quote\nq\n#+end_quote\n", 14, "d a r"), ("#+begin_quote\nq\n#+end_quote\n", 14, "d i r"), ("a =code= b\n", 3, "d i e"), ("a =code= b\n", 3, "d a e"), ("a [[https://x.org]] b\n", 5, "d i e"), ] + elementCases + fillCases }() /// gq and gw: evil-fill-and-move and evil-fill. static let fillCases: [(String, Int, String)] = { let long = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." return [ (long + "\n", 0, "g q q"), (long + "\n", 10, "g w w"), ("short\nlines\njoin here\n\nnext para\n", 0, "g q a p"), ("short\nlines\njoin here\n\nnext para\n", 2, "g w a p"), ("one\ntwo\n\nthree\nfour\n", 0, "g q G"), ("- item " + long + "\n- b\n", 0, "g q q"), ("* H\n" + long + "\n", 4, "g q q"), ("* H\n" + long + "\n", 0, "g q j"), ("a\nb\nc\n", 2, "g q q"), ("a\nb\nc\nd\n", 2, "g q j"), ("a\nb\nc\n", 0, "V j g q"), ("a\nb\n\nc\nd\n", 0, "g q j j ."), ("one two three\nfour five \nsix\n", 0, "g q G"), ("End. Next word, " + long + " \nmore\n", 3, "g w j"), ] }() /// evil-org's gj and gk: org-forward-element and org-backward-element. static let elementCases: [(String, Int, String)] = { 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" let at = { (s: String) in (text as NSString).range(of: s).location } var cases: [(String, Int, String)] = [] // Org signals an error, and evil stays, for g k at 0 and on the first heading, and g j on // the last one: see `elementMotionsStop`. 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")] { if start != at("* Next") { cases.append((text, start, "g j")) } if start != 0, start != at("* H") { cases.append((text, start, "g k")) } } cases += [ (text, at("para"), "g j g j g j"), (text, at("end"), "g k g k"), (text, at("para"), "3 g j"), (text, at("para"), "d g j"), (text, at("* H"), "d g j"), ] return cases }() @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) func matchesEvil() throws { let repos = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config/emacs/.local/straight/repos") guard FileManager.default.fileExists(atPath: repos.appendingPathComponent("evil-org-mode/evil-org.el").path) else { return } let all = Self.cases.map { ($0, false) } + Self.orgCases.map { ($0, true) } let forms = all.map { (c, org) in EmacsOracle.Case(text: c.0, point: c.1 + 1, form: """ (progn (setq evil-want-Y-yank-to-eol t evil-undo-system 'undo-redo evil-want-C-i-jump t) (dolist (d '("evil" "goto-chg" "evil-surround" "evil-org-mode")) (add-to-list 'load-path (concat \(lispString(repos.path)) "/" d))) (require 'evil) (require 'evil-surround) (defun oracle-no-squeeze (fn &rest args) (cl-letf* ((fill (symbol-function 'fill-region)) ((symbol-function 'fill-region) (lambda (from to &optional justify _nosqueeze to-eop) (funcall fill from to justify t to-eop)))) (apply fn args))) (advice-add 'evil-fill :around #'oracle-no-squeeze) (advice-add 'evil-fill-and-move :around #'oracle-no-squeeze) (setq kill-ring nil) (switch-to-buffer (current-buffer)) \(org ? "(org-mode) (require 'evil-org) (evil-org-set-key-theme '(textobjects navigation)) (evil-org-mode 1)" : "") (evil-local-mode 1) (evil-surround-mode 1) (evil-normal-state) (set-mark (point)) (deactivate-mark) (execute-kbd-macro (kbd \(lispString(c.2.replacingOccurrences(of: "ESC", with: ""))))) (let ((p (point)) (s evil-state)) (goto-char (point-max)) (insert (format "\\n@@STATE %s" s)) (goto-char p))) """) } let results = try EmacsOracle.run(forms) for ((c, org), result) in zip(all, results) { let (text, point, keys) = c var driver = VimDriver(text, caret: point) driver.org = org driver.type(KeySequence.parse(keys)!) guard let marker = result.text.range(of: "\n@@STATE ") else { Issue.record("\(keys): \(result.error)") continue } let emacsText = String(result.text[..