krz/orgstar

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

Tests/OrgCoreTests/VimTests.swift

191ddb506f3a91b93c755ba6e8123a4e8ac4f90d
orgstar/Tests/OrgCoreTests/VimTests.swift history · blame · raw

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}