import AppKit import OrgCore import OrgPresentation import Testing @testable import OrgEditorAppKit /// The Doom preset in the editor: keys through the keymap and the modal engine. @MainActor struct ModalTests { func doom(_ text: String) -> Harness { let h = Harness(text) h.editor.keymap = KeymapPreset.doom.keymap h.editor.modal = true return h } /// Types `keys` as key events: letters, `ESC`, `RET`, `TAB`, `SPC`. func type(_ h: Harness, _ keys: String) { let codes: [String: UInt16] = ["ESC": 53, "RET": 36, "TAB": 48, "SPC": 49] for token in keys.split(separator: " ") { let key = String(token) if key.hasPrefix("C-") { let letter = String(key.dropFirst(2)) let named = codes[letter] let characters = named == nil ? letter : " " KeyTests().send(h, characters: characters, ignoring: characters, code: named ?? 0, flags: .control) } else if let code = codes[key] { let characters = key == "ESC" ? "\u{1b}" : key == "RET" ? "\r" : key == "TAB" ? "\t" : " " KeyTests().send(h, characters: characters, ignoring: characters, code: code) } else { let shifted = key.first!.isUppercase KeyTests().send(h, characters: key, ignoring: key, code: 0, flags: shifted ? .shift : []) } } } @Test func normalStateEditsAndInsertTypes() { let h = doom("hello world\nsecond\n") h.caret(at: 0) type(h, "d w") #expect(h.editor.document.text == "world\nsecond\n") type(h, "c i w") #expect(h.editor.vim.mode == .insert) h.textView.insertText("planet", replacementRange: h.textView.selectedRange()) type(h, "ESC") #expect(h.editor.document.text == "planet\nsecond\n") #expect(h.textView.selectedRange().location == 5) type(h, "j .") #expect(h.editor.document.text == "planet\nplanet\n") } @Test func undo() { let h = doom("hello world\n") h.caret(at: 0) type(h, "d w") #expect(h.editor.document.text == "world\n") type(h, "u") #expect(h.editor.document.text == "hello world\n") } @Test func leaderAndOrgKeys() { let h = doom("* TODO Task\n") var app: [String] = [] h.editor.onAppCommand = { app.append($0) } h.caret(at: 2) type(h, "SPC f s") #expect(app == ["app.save"]) type(h, "RET") #expect(h.editor.document.text == "* DONE Task\n") type(h, "SPC m t") #expect(h.editor.document.text == "* KILL Task\n" || h.editor.document.text.hasPrefix("* ")) type(h, "TAB") #expect(h.editor.vim.mode == .normal) } @Test func registersAndMacrosAreShared() { let a = doom("alpha beta\n") let b = doom("one\ntwo\n") a.caret(at: 0) type(a, "\" r y w") b.caret(at: 0) type(b, "\" r P") #expect(b.editor.document.text == "alpha one\ntwo\n") type(a, "q m A ! ESC q") type(b, "j @ m") #expect(b.editor.document.text == "alpha one\ntwo!\n") } @Test func uppercaseMarksAreGlobal() { let a = doom("first\n second\n") let b = doom("other\ntext\n") var jumps: [(OrgEditor, Int)] = [] b.editor.onJumpToEditor = { jumps.append(($0, $1)) } a.caret(at: 10) type(a, "m Q m q") b.caret(at: 0) type(b, "' Q") #expect(jumps.count == 1 && jumps.first?.0 === a.editor && jumps.first?.1 == 8) type(b, "` Q") #expect(jumps.last?.1 == 10) // Lowercase marks stay in their buffer. type(b, "j ' q") #expect(b.textView.selectedRange().location == 6) // In its own buffer the mark is an ordinary jump, and follows edits. type(a, "g g O") a.textView.insertText("new", replacementRange: a.textView.selectedRange()) type(a, "ESC ` Q") #expect(a.textView.selectedRange().location == 14) } @Test func foldKeysToggleOpenAndClose() { let h = doom("* A\nbody\n** B\nb body\n* C\n") h.caret(at: 0) type(h, "z c") #expect(h.editor.foldState.folds == [0]) type(h, "z o") #expect(h.editor.foldState.folds == [9]) type(h, "z o") #expect(h.editor.foldState.folds == [9]) type(h, "z a") #expect(h.editor.foldState.folds.contains(0)) type(h, "z a") #expect(h.editor.foldState.folds == [9]) // za and zo act on a heading line; zc closes the subtree around the caret. h.editor.setFolds(FoldState()) h.caret(at: 5) type(h, "z a") type(h, "z o") #expect(h.editor.foldState.folds.isEmpty) h.caret(at: 15) type(h, "z c") #expect(h.editor.foldState.folds == [9]) } @Test func fillOperatorsInTheEditor() { let h = doom("short\nlines\n\nnext\n") h.caret(at: 7) type(h, "g w a p") #expect(h.editor.document.text == "short lines\n\nnext\n") #expect(h.textView.selectedRange().location == 7) type(h, "u") #expect(h.editor.document.text == "short\nlines\n\nnext\n") } /// M-q in visual states fills the region's paragraphs and leaves visual state, as in /// Doom (checked against evil with `org-fill-paragraph`). @Test func fillParagraphFillsTheVisualRegion() { let h = doom("aaa\nbbb\n\nccc\nddd\n\neee\nfff\n") h.caret(at: 0) type(h, "v j j j") KeyTests().press(h, "q", option: KeyEvents.leftOption, typed: "œ") #expect(h.editor.document.text == "aaa bbb\n\nccc ddd\n\neee\nfff\n") #expect(h.textView.selectedRange() == NSRange(location: 9, length: 0)) #expect(h.editor.vim.mode == .normal) let lines = doom("aaa\nbbb\nccc\n\nx\ny\n") lines.caret(at: 8) type(lines, "v k") KeyTests().press(lines, "q", option: KeyEvents.leftOption, typed: "œ") #expect(lines.editor.document.text == "aaa bbb ccc\n\nx\ny\n") #expect(lines.textView.selectedRange() == NSRange(location: 4, length: 0)) #expect(lines.editor.vim.mode == .normal) } @Test func zzSavesAndClosesAsWq() { let h = doom("text\n") var app: [String] = [] h.editor.onAppCommand = { app.append($0) } h.caret(at: 0) type(h, "Z Q") #expect(app.isEmpty) type(h, "Z Z") #expect(app == ["app.save"]) } @Test func windowLinesInTheEditor() { let h = doom("one\ntwo\nthree\nfour\nfive\n") h.caret(at: 0) type(h, "L") #expect(h.textView.selectedRange().location == 19) type(h, "M") #expect(h.textView.selectedRange().location == 8) type(h, "H") #expect(h.textView.selectedRange().location == 0) } @Test func undefinedLeaderSequencesDoNothing() { let h = doom("* Task\nbody text\n") var messages: [String] = [] h.editor.onMessage = { messages.append($0) } h.caret(at: 0) type(h, "SPC m z") #expect(messages.last == "SPC m z is undefined") #expect(h.textView.selectedRange().location == 0) // No mark z was set: a jump to it fails and the caret stays. type(h, "j ' z") #expect(h.textView.selectedRange().location == 7) type(h, "C-c z") #expect(messages.last == "C-c z is undefined") #expect(h.editor.document.text == "* Task\nbody text\n") // Evil's own prefixes still reach it. type(h, "g g") #expect(h.textView.selectedRange().location == 0) } @Test func keyHintsFollowContext() { let h = doom("* a\n#+begin_src sh\necho\n#+end_src\n") var hints: [(key: String, title: String)] = [] h.editor.onKeysPending = { _, next in if !next.isEmpty { hints = next } } func hint() -> String? { hints.first { $0.key == "C-c" }?.title } h.caret(at: 1) type(h, "C-c") #expect(hint() != nil && hint() != "Run Source Block") type(h, "C-g") h.caret(at: 20) type(h, "C-c") #expect(hint() == "Run Source Block") type(h, "C-g") } @Test func visualSelection() { let h = doom("one two three\n") h.caret(at: 0) type(h, "v e") #expect(h.textView.selectedRange() == NSRange(location: 0, length: 3)) type(h, "d") #expect(h.editor.document.text == " two three\n") #expect(h.textView.selectedRange().length == 0) } func shiftTab(_ h: Harness) { KeyTests().send(h, characters: "\u{19}", ignoring: "\u{19}", code: 48, flags: .shift) } @Test func insertStateTabFollowsDoom() { let h = doom("* Top\n** Sub\n- one\n- item\n - child\nbody\n") h.caret(at: 8) type(h, "i") #expect(h.editor.vim.mode == .insert) shiftTab(h) #expect(h.editor.document.text.hasPrefix("* Top\n* Sub\n")) type(h, "TAB") #expect(h.editor.document.text.hasPrefix("* Top\n** Sub\n")) let item = (h.editor.document.text as NSString).range(of: "- item").location h.caret(at: item + 3) type(h, "TAB") #expect(h.editor.document.text.contains("- one\n - item\n - child\n")) #expect(h.editor.vim.mode == .insert) } @Test func shiftTabCyclesInNormalState() { let h = doom("* Top\nbody\n") h.caret(at: 7) let before = h.editor.document.text shiftTab(h) #expect(h.editor.document.text == before) #expect(h.editor.vim.mode == .normal) } @Test func reportsStateAndQuits() { let h = doom("text\n") var states: [String?] = [] var app: [String] = [] h.editor.onModeChange = { states.append($0) } h.editor.onAppCommand = { app.append($0) } h.caret(at: 0) type(h, "i") type(h, "ESC") type(h, "V") type(h, "ESC") type(h, "SPC q q") #expect(states == ["insert", "normal", "visual-line", "normal"]) #expect(app == ["app.quit"]) h.editor.modal = false #expect(states.last == .some(nil)) } @Test func macrosReplayKeys() { let h = doom("one two\nthree four\nfive six\nseven\n") h.caret(at: 0) type(h, "q a d w j q") #expect(h.editor.document.text == "two\nthree four\nfive six\nseven\n") type(h, "@ a") #expect(h.editor.document.text == "two\nfour\nfive six\nseven\n") type(h, "@ @") #expect(h.editor.document.text == "two\nfour\nsix\nseven\n") type(h, "g g q b A ! ESC j q 2 @ b") #expect(h.editor.document.text == "two!\nfour!\nsix!\nseven\n", "\(h.editor.document.text.debugDescription)") } @Test func jumpsAndMarks() { let h = doom("one\ntwo\nthree\nfour\n") h.caret(at: 4) type(h, "G") #expect(h.textView.selectedRange().location == 19) type(h, "C-o") #expect(h.textView.selectedRange().location == 4) type(h, "C-i") #expect(h.textView.selectedRange().location == 19) type(h, "g g m a G ' a") #expect(h.textView.selectedRange().location == 0) // Marks follow edits above them. type(h, "G m b g g O") h.textView.insertText("new", replacementRange: h.textView.selectedRange()) type(h, "ESC ` b") #expect(h.textView.selectedRange().location == 23) } } struct PresetCommandTests { /// Every command a preset binds is one the editor runs, and every mode is a state. @Test func presetsBindKnownCommands() throws { let known = Set(EditorCommands.all.map(\.id)) for preset in KeymapPreset.allCases { var problems: [String] = [] _ = try Keymap(toml: preset.toml, problems: &problems, commands: known) #expect(problems.isEmpty, "\(preset): \(problems)") } } }