krz/orgstar

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

Tests/OrgCoreTests/ElectricPairTests.swift

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

97 lines · 5008 bytes

 1import Foundation
 2import Testing
 3@testable import OrgCore
 4
 5/// Keys typed with `electric-pair-mode` in an Org buffer, against Emacs.
 6struct ElectricPairTests {
 7    struct Typing {
 8        let text: String
 9        let point: Int
10        var mark: Int?
11        let keys: [String]
12    }
13
14    static let cases: [Typing] = [
15        Typing(text: "", point: 0, keys: ["("]),
16        Typing(text: "foo", point: 3, keys: ["(", "x", ")"]),
17        Typing(text: ")", point: 0, keys: ["("]),
18        Typing(text: "(", point: 1, keys: [")"]),
19        Typing(text: "", point: 0, keys: ["\"", "a", "\""]),
20        Typing(text: "a \"b", point: 4, keys: ["\""]),
21        Typing(text: "(foo )", point: 4, keys: [")"]),
22        Typing(text: "x", point: 1, keys: ["<", ">"]),
23        Typing(text: "", point: 0, keys: ["[", "[", "a", "]", "]"]),
24        Typing(text: "{}", point: 1, keys: ["}"]),
25        Typing(text: "(())", point: 2, keys: [")", ")"]),
26        Typing(text: "[(])", point: 2, keys: [")"]),
27        Typing(text: "()", point: 1, keys: ["DEL"]),
28        Typing(text: "(x)", point: 2, keys: ["DEL"]),
29        Typing(text: "\"\"", point: 1, keys: ["DEL"]),
30        Typing(text: "()", point: 1, keys: ["RET"]),
31        Typing(text: "word", point: 4, mark: 0, keys: ["("]),
32        Typing(text: "word", point: 4, mark: 0, keys: [")"]),
33        Typing(text: "word", point: 4, mark: 0, keys: ["\""]),
34        Typing(text: "say ", point: 4, keys: ["“", "hi", "”"]),
35        Typing(text: "\"a b\"", point: 2, keys: ["("]),
36        Typing(text: "\"a b\"", point: 2, keys: ["<"]),
37        Typing(text: "* Heading\nit", point: 12, keys: ["'", "s"]),
38        Typing(text: "a (b", point: 4, keys: [")"]),
39        Typing(text: "a b)", point: 2, keys: ["("]),
40        Typing(text: "(a\n )", point: 2, keys: [")"]),
41        Typing(text: "word", point: 0, keys: ["\""]),
42        Typing(text: "say \"x\" and ", point: 12, keys: ["\"", "y", "\""]),
43        Typing(text: "- item (see [[x]]", point: 17, keys: [")"]),
44        Typing(text: "<2026-10-06 Tue>", point: 16, keys: ["DEL"]),
45        Typing(text: "f(\"(\")", point: 4, keys: [")"]),
46    ]
47
48    /// The keys typed into our side: `ElectricPairKey` where it applies, otherwise as the
49    /// text view types them.
50    static func ours(_ c: Typing) -> (text: String, point: Int) {
51        var text = c.text
52        var selection = (c.mark ?? c.point)..<c.point
53        if let mark = c.mark, mark > c.point { selection = c.point..<mark }
54        var options = EditingOptions.org
55        options.electricPair = true
56        let units = c.keys.flatMap { key in key == "DEL" || key == "RET" ? [key] : key.map { String($0) } }
57        for key in units {
58            let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [selection], options: options)
59            let command = ElectricPairKey(key: key)
60            if command.applies(in: context), case .commit(let result) = command.run(in: context) {
61                for edit in result.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { text = edit.apply(to: text) }
62                selection = result.selection!.first!
63                continue
64            }
65            let ns = text as NSString
66            let caret = selection.upperBound
67            switch key {
68            case "DEL":
69                text = ns.replacingCharacters(in: NSRange(location: caret - 1, length: 1), with: "")
70                selection = caret - 1..<caret - 1
71            default:
72                let typed = key == "RET" ? "\n" : key
73                text = ns.replacingCharacters(in: NSRange(location: selection.lowerBound, length: selection.count), with: typed)
74                selection = selection.lowerBound + 1..<selection.lowerBound + 1
75            }
76        }
77        return (text, selection.upperBound)
78    }
79
80    @Test func typesLikeEmacs() throws {
81        let cases = Self.cases.map { c in
82            let keys = c.keys.map { key in
83                key == "DEL" || key == "RET" ? "(execute-kbd-macro (kbd \"\(key)\"))"
84                    : key.map { "(execute-kbd-macro \(lispString(String($0))))" }.joined(separator: " ")
85            }.joined(separator: " ")
86            let region = c.mark.map { "(set-mark \(EmacsOracle.point($0, in: c.text))) (activate-mark)" } ?? ""
87            return EmacsOracle.Case(text: c.text, point: EmacsOracle.point(c.point, in: c.text), form: """
88                (progn (switch-to-buffer (current-buffer)) (electric-pair-local-mode 1) (transient-mark-mode 1) \(region) \(keys))
89                """)
90        }
91        for (c, theirs) in zip(Self.cases, try EmacsOracle.run(cases)) {
92            let ours = Self.ours(c)
93            #expect(ours.text == theirs.text && EmacsOracle.point(ours.point, in: ours.text) == theirs.point,
94                    "\(c.text.debugDescription) at \(c.point) keys \(c.keys)\nours:  \(ours.text.debugDescription) \(ours.point)\nemacs: \(theirs.text.debugDescription) \(theirs.point) \(theirs.error)")
95        }
96    }
97}