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 | 32 | case setClipboard(String) |
| 33 | 33 | /// Ask for a search pattern (`/`, `?`) or an ex command (`:`). |
| 34 | 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 | 37 | case message(String) |
| 36 | 38 | /// `m`: a mark at this offset, for the editor to keep through edits. |
| 37 | 39 | case setMark(Character, Int) |
| @@ -463,8 +465,8 @@ public struct Vim: Sendable { |
| 463 | 465 | case ("Z", []): |
| 464 | 466 | guard rest.count >= 2 else { return .incomplete } |
| 465 | 467 | switch rest[rest.startIndex + 1].key { |
| 466 | | case "Z": return .done([.command("app.save")]) |
| 467 | | case "Q": return .done([.message("ZQ isn't available; use the window's close button")]) |
| 468 | case "Z": return .done([.ex("wq")]) |
| 469 | case "Q": return .done([.ex("q!")]) |
| 468 | 470 | default: return .invalid |
| 469 | 471 | } |
| 470 | 472 | default: |
Sources/OrgEditorAppKit/Modal.swift
+2
| @@ -170,6 +170,8 @@ extension OrgEditor { |
| 170 | 170 | NSPasteboard.general.setString(text, forType: .string) |
| 171 | 171 | case .prompt(let kind): |
| 172 | 172 | prompt(kind) |
| 173 | case .ex(let command): |
| 174 | ex(command) |
| 173 | 175 | case .message(let text): |
| 174 | 176 | onMessage?(text) |
| 175 | 177 | case .setMark(let name, let offset): |
Tests/OrgCoreTests/VimTests.swift
+9
| @@ -196,6 +196,15 @@ struct VimTests { |
| 196 | 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 | 208 | @Test func halfPageScrollsFollowTheWindow() { |
| 200 | 209 | #expect(driven(0, "C-d", halfPage: 5) == 35) |
| 201 | 210 | #expect(driven(70, "C-u", halfPage: 5) == 35) |
Tests/OrgEditorAppKitTests/ModalTests.swift
+11
| @@ -72,6 +72,17 @@ struct ModalTests { |
| 72 | 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 | 86 | @Test func windowLinesInTheEditor() { |
| 76 | 87 | let h = doom("one\ntwo\nthree\nfour\nfive\n") |
| 77 | 88 | h.caret(at: 0) |