krz/orgstar

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

Commit 441daa361a

441daa361a949bb87a5b9785a776ead68e716f8f

parent: 4245fe5852

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 03:45 UTC

Make ZZ :wq and ZQ :q!

ZZ saves and closes the window, ZQ closes it, both through the ex
commands.

Layout: unified · split

Sources/OrgCore/Keymap/Vim.swift +4 −2
@@ -32,6 +32,8 @@ public struct Vim: Sendable {
32 case setClipboard(String) 32 case setClipboard(String)
33 /// Ask for a search pattern (`/`, `?`) or an ex command (`:`). 33 /// Ask for a search pattern (`/`, `?`) or an ex command (`:`).
34 case prompt(String) 34 case prompt(String)
35 /// An ex command to run, as if typed after `:` (`ZZ` is `:wq`, `ZQ` is `:q!`).
36 case ex(String)
35 case message(String) 37 case message(String)
36 /// `m`: a mark at this offset, for the editor to keep through edits. 38 /// `m`: a mark at this offset, for the editor to keep through edits.
37 case setMark(Character, Int) 39 case setMark(Character, Int)
@@ -463,8 +465,8 @@ public struct Vim: Sendable {
463 case ("Z", []): 465 case ("Z", []):
464 guard rest.count >= 2 else { return .incomplete } 466 guard rest.count >= 2 else { return .incomplete }
465 switch rest[rest.startIndex + 1].key { 467 switch rest[rest.startIndex + 1].key {
466 case "Z": return .done([.command("app.save")]) 468 case "Z": return .done([.ex("wq")])
467 case "Q": return .done([.message("ZQ isn't available; use the window's close button")]) 469 case "Q": return .done([.ex("q!")])
468 default: return .invalid 470 default: return .invalid
469 } 471 }
470 default: 472 default:
Sources/OrgEditorAppKit/Modal.swift +2
@@ -170,6 +170,8 @@ extension OrgEditor {
170 NSPasteboard.general.setString(text, forType: .string) 170 NSPasteboard.general.setString(text, forType: .string)
171 case .prompt(let kind): 171 case .prompt(let kind):
172 prompt(kind) 172 prompt(kind)
173 case .ex(let command):
174 ex(command)
173 case .message(let text): 175 case .message(let text):
174 onMessage?(text) 176 onMessage?(text)
175 case .setMark(let name, let offset): 177 case .setMark(let name, let offset):
Tests/OrgCoreTests/VimTests.swift +9
@@ -196,6 +196,15 @@ struct VimTests {
196 return driver.caret 196 return driver.caret
197 } 197 }
198 198
199 @Test func zzAndZqRunExCommands() {
200 var vim = Vim()
201 let context = Vim.Context(text: "a\n", caret: 0)
202 #expect(vim.feed(KeyChord("Z"), context) == [])
203 #expect(vim.feed(KeyChord("Z"), context) == [.ex("wq")])
204 _ = vim.feed(KeyChord("Z"), context)
205 #expect(vim.feed(KeyChord("Q"), context) == [.ex("q!")])
206 }
207
199 @Test func halfPageScrollsFollowTheWindow() { 208 @Test func halfPageScrollsFollowTheWindow() {
200 #expect(driven(0, "C-d", halfPage: 5) == 35) 209 #expect(driven(0, "C-d", halfPage: 5) == 35)
201 #expect(driven(70, "C-u", halfPage: 5) == 35) 210 #expect(driven(70, "C-u", halfPage: 5) == 35)
Tests/OrgEditorAppKitTests/ModalTests.swift +11
@@ -72,6 +72,17 @@ struct ModalTests {
72 #expect(h.editor.vim.mode == .normal) 72 #expect(h.editor.vim.mode == .normal)
73 } 73 }
74 74
75 @Test func zzSavesAndClosesAsWq() {
76 let h = doom("text\n")
77 var app: [String] = []
78 h.editor.onAppCommand = { app.append($0) }
79 h.caret(at: 0)
80 type(h, "Z Q")
81 #expect(app.isEmpty)
82 type(h, "Z Z")
83 #expect(app == ["app.save"])
84 }
85
75 @Test func windowLinesInTheEditor() { 86 @Test func windowLinesInTheEditor() {
76 let h = doom("one\ntwo\nthree\nfour\nfive\n") 87 let h = doom("one\ntwo\nthree\nfour\nfive\n")
77 h.caret(at: 0) 88 h.caret(at: 0)