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) |