krz/orgstar

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

Tests/OrgEditorAppKitTests/KeyTests.swift

2fa201330f61c801f777baa3f2943d49d8758cda
orgstar/Tests/OrgEditorAppKitTests/KeyTests.swift history · blame · raw

162 lines · 6372 bytes

  1import AppKit
  2import OrgCore
  3import Testing
  4@testable import OrgApp
  5@testable import OrgEditorAppKit
  6
  7@MainActor
  8struct KeyTests {
  9    static let codes: [Character: UInt16] = ["a": 0, "s": 1, "f": 3, "g": 5, "z": 6, "x": 7, "c": 8, "w": 13, "q": 12, "t": 17]
 10
 11    /// Presses a key. `control` sends the control character, as macOS does.
 12    func press(_ h: Harness, _ key: Character, control: Bool = false, option: UInt = 0, typed: String? = nil) {
 13        var flags: NSEvent.ModifierFlags = []
 14        if control { flags.insert(.control) }
 15        if option != 0 { flags = NSEvent.ModifierFlags(rawValue: flags.rawValue | NSEvent.ModifierFlags.option.rawValue | option) }
 16        let ignoring = String(key)
 17        let characters = typed ?? (control ? String(UnicodeScalar(UInt8(key.asciiValue! & 0x1f))) : ignoring)
 18        send(h, characters: characters, ignoring: ignoring, code: Self.codes[key] ?? 0, flags: flags)
 19    }
 20
 21    func send(_ h: Harness, characters: String, ignoring: String, code: UInt16, flags: NSEvent.ModifierFlags = []) {
 22        let event = NSEvent.keyEvent(
 23            with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 0, windowNumber: h.window.windowNumber,
 24            context: nil, characters: characters, charactersIgnoringModifiers: ignoring, isARepeat: false, keyCode: code
 25        )!
 26        h.textView.keyDown(with: event)
 27    }
 28
 29    @Test func prefixSequenceRunsAnOrgCommand() {
 30        let h = Harness("* a\n")
 31        var pending: [String?] = []
 32        var next: [(key: String, title: String)] = []
 33        h.editor.onKeysPending = { keys, continuations in
 34            pending.append(keys)
 35            if keys != nil { next = continuations }
 36        }
 37        h.caret(at: 1)
 38        press(h, "c", control: true)
 39        #expect(pending == ["C-c"])
 40        #expect(next.contains { $0.key == "C-t" && $0.title == "Cycle TODO State" })
 41        press(h, "t", control: true)
 42        #expect(h.string == "* TODO a\n")
 43        #expect(pending == ["C-c", nil])
 44        h.checkInSync()
 45    }
 46
 47    @Test func undefinedSequenceReportsAMessage() {
 48        let h = Harness("text\n")
 49        var messages: [String] = []
 50        h.editor.onMessage = { messages.append($0) }
 51        press(h, "c", control: true)
 52        press(h, "z", control: true)
 53        #expect(messages == ["C-c C-z is undefined"])
 54        press(h, "x", control: true)
 55        press(h, "g", control: true)
 56        #expect(messages.last == "Quit")
 57        #expect(h.string == "text\n")
 58    }
 59
 60    @Test func tabFoldsOnHeadingsAndIndentsElsewhere() {
 61        let h = Harness(outline)
 62        h.caret(at: 1)
 63        send(h, characters: "\t", ignoring: "\t", code: 48)
 64        #expect(h.visibleLines == ["* a\n", "* c\n", "text\n"])
 65        h.caret(at: h.offset(of: "text"))
 66        send(h, characters: "\t", ignoring: "\t", code: 48)
 67        #expect(h.string.hasSuffix("\ttext\n"))
 68    }
 69
 70    @Test func markExtendsMovementAndKillTakesTheRegion() {
 71        let h = Harness("abc\n")
 72        h.caret(at: 0)
 73        send(h, characters: "\0", ignoring: " ", code: 49, flags: .control)
 74        #expect(h.editor.markActive)
 75        press(h, "f", control: true)
 76        press(h, "f", control: true)
 77        #expect(h.textView.selectedRange() == NSRange(location: 0, length: 2))
 78        press(h, "w", control: true)
 79        #expect(h.string == "c\n")
 80        #expect(!h.editor.markActive)
 81        h.checkInSync()
 82    }
 83
 84    @Test func leftOptionIsMetaRightOptionTypes() {
 85        let h = Harness("one two\n")
 86        h.caret(at: 0)
 87        press(h, "f", option: KeyEvents.leftOption, typed: "ƒ")
 88        #expect(h.caret == 3)
 89        press(h, "f", option: KeyEvents.rightOption, typed: "ƒ")
 90        #expect(h.string == "oneƒ two\n")
 91    }
 92
 93    @Test func appCommandsGoToTheApp() {
 94        let h = Harness("text\n")
 95        var commands: [String] = []
 96        h.editor.onAppCommand = { commands.append($0) }
 97        press(h, "x", control: true)
 98        press(h, "s", control: true)
 99        #expect(commands == ["app.save"])
100    }
101
102    @Test func userKeymapOverridesThePreset() throws {
103        let h = Harness("* a\n")
104        var problems: [String] = []
105        let user = try Keymap(toml: "[[bind]]\nkeys = \"C-c C-t\"\ncommand = \"org.priority.up\"\n", problems: &problems)
106        h.editor.keymap = .layered([KeymapPreset.emacs.keymap, user])
107        h.caret(at: 1)
108        press(h, "c", control: true)
109        press(h, "t", control: true)
110        #expect(h.string == "* [#B] a\n")
111    }
112
113    @Test func promptsAskAndRunAgainWithTheAnswer() {
114        let h = Harness("* a\n")
115        var asked: [Prompt] = []
116        var reply: ((String?) -> Void)?
117        h.editor.onPrompt = { prompt, answer in
118            asked.append(prompt)
119            reply = answer
120        }
121        h.caret(at: 1)
122        press(h, "c", control: true)
123        press(h, "q", control: true)
124        #expect(asked.map(\.key) == ["tags"])
125        reply?("x")
126        #expect(h.string.hasPrefix("* a ") && h.string.hasSuffix(":x:\n"))
127        h.checkInSync()
128    }
129
130    @Test func everyPresetCommandExists() {
131        for preset in KeymapPreset.allCases {
132            for binding in preset.keymap.bindings {
133                #expect(EditorCommands.command(binding.command) != nil, "\(preset): \(binding.command)")
134            }
135        }
136    }
137
138    @Test func everyEmacsImportCommandExists() {
139        for (emacs, command) in EmacsImport.commands {
140            #expect(EditorCommands.command(command) != nil, "\(emacs): \(command)")
141        }
142    }
143}
144
145@MainActor
146struct ListKeyTests {
147    @Test func metaReturnInsertsAnItemInAListAndAHeadingElsewhere() {
148        let h = Harness("* a\n- x\n")
149        let event = { (flags: NSEvent.ModifierFlags) in
150            NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 0, windowNumber: h.window.windowNumber,
151                             context: nil, characters: "\r", charactersIgnoringModifiers: "\r", isARepeat: false, keyCode: 36)!
152        }
153        let meta = NSEvent.ModifierFlags(rawValue: NSEvent.ModifierFlags.option.rawValue | KeyEvents.leftOption)
154        h.caret(at: 7)
155        h.textView.keyDown(with: event(meta))
156        #expect(h.string == "* a\n- x\n- \n")
157        h.caret(at: 3)
158        h.textView.keyDown(with: event(meta))
159        #expect(h.string.hasPrefix("* a\n* \n"))
160        h.checkInSync()
161    }
162}