krz/orgstar

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

Tests/OrgCoreTests/VimTests.swift

778058aafc7ddacee4a613b1da87c60aa0c8e113
orgstar/Tests/OrgCoreTests/VimTests.swift history · blame · raw

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}