From bf3c1f547aec7f9c547ab8103a216dbc0e482bb8 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 5 Sep 2026 06:48:22 +0200 Subject: [PATCH] fix: legible caret and working :q! CodeMirror injects its own `.cm-cursor` rule after our stylesheet, so the equally specific override lost and the caret painted black on a dark background. Qualify the selector, same as `.cm-activeLine`. `:q!` reached the command table as the literal name `quit!`, which nothing matched. Commands now carry an optional `bang` variant, and `runCommand` splits a trailing `!` off any head before looking it up. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01476ZKdB4k3rPDwvVhXcnDg --- web/src/app.css | 13 +++++++++++-- web/src/lib/Editor.svelte | 7 ++++--- web/src/lib/Help.svelte | 4 ++++ web/src/lib/commands.ts | 30 +++++++++++++++++++----------- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/web/src/app.css b/web/src/app.css index ee67f8b..d695d0d 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -198,11 +198,20 @@ input::placeholder { color: var(--fg-faint); } .cm-editor .cm-activeLine { background: color-mix(in srgb, var(--fg) 4%, transparent); } .cm-selectionBackground, .cm-content ::selection { background: var(--select) !important; } -/* Insert mode: a 2px bar reads far better than the browser's hairline caret. */ -.cm-cursor, .cm-cursor-primary { +/* + * Insert mode: a 2px bar reads far better than the browser's hairline caret. + * CodeMirror injects `.cm-cursor { border-left: 1.2px solid black }` after this + * stylesheet, so — like `.cm-activeLine` above — an equally specific selector + * loses and the caret renders black on a dark background. Qualify it to win. + */ +.cm-editor .cm-cursor, +.cm-editor .cm-cursor-primary, +.cm-editor .cm-dropCursor { border-left: 2px solid var(--accent); border-right: 0; + margin-left: -1px; } +.cm-editor .cm-cursor-secondary { border-left-color: var(--accent-dim); } /* * Normal mode: the vim plugin paints the character *inside* the block cursor, diff --git a/web/src/lib/Editor.svelte b/web/src/lib/Editor.svelte index cdf05d3..def954e 100644 --- a/web/src/lib/Editor.svelte +++ b/web/src/lib/Editor.svelte @@ -45,11 +45,12 @@ for (const spec of commands) { const names = [spec.name, ...(spec.aliases ?? [])] for (const name of names) { - if (name.includes('!')) continue Vim.defineEx(name, name, (_cm: unknown, params: { argString?: string }) => { const arg = (params.argString ?? '').trim() - // vim parses `:q!` as command `q` with the bang left in the arguments. - void runCommand(arg === '!' ? `${spec.name}!` : `${spec.name} ${arg}`) + // vim parses `:q!` as command `q` with the bang left in the arguments, + // so put it back on the name before handing the line over. + const bang = arg.startsWith('!') + void runCommand(`${spec.name}${bang ? '!' : ''} ${bang ? arg.slice(1).trim() : arg}`) }) } } diff --git a/web/src/lib/Help.svelte b/web/src/lib/Help.svelte index b6941e1..cf78d6e 100644 --- a/web/src/lib/Help.svelte +++ b/web/src/lib/Help.svelte @@ -46,6 +46,10 @@ {#if spec.args} {spec.args}{/if}
{spec.help}
+ {#if spec.bang} +
:{spec.name}!{#if spec.aliases?.length} ({spec.aliases.map((a) => `${a}!`).join(' ')}){/if}
+
{spec.bang.help}
+ {/if} {/each} diff --git a/web/src/lib/commands.ts b/web/src/lib/commands.ts index 2319c65..d9c0993 100644 --- a/web/src/lib/commands.ts +++ b/web/src/lib/commands.ts @@ -16,6 +16,8 @@ export interface CommandSpec { args?: string help: string run: (arg: string) => void | Promise + /** The forcing variant, `:name!` — vim hands the bang over as an argument. */ + bang?: { help: string; run: (arg: string) => void | Promise } } const requireOpen = () => { @@ -59,16 +61,15 @@ export const commands: CommandSpec[] = [ } hooks.focusList() }, - }, - { - name: 'q!', - help: 'discard changes and close the editor', - run: async () => { - const open = diary.open - diary.editing = false - diary.dirty = false - if (open) await diary.guard(() => diary.openEntry(open.id)) - hooks.focusList() + bang: { + help: 'discard changes and close the editor', + run: async () => { + const open = diary.open + diary.editing = false + diary.dirty = false + if (open) await diary.guard(() => diary.openEntry(open.id)) + hooks.focusList() + }, }, }, { @@ -244,11 +245,18 @@ export async function runCommand(line: string): Promise { return } - const spec = lookup.get(head) + // `:q!`, `:quit!` — the bang is part of the command name, not an argument. + const forced = head.endsWith('!') + const spec = lookup.get(forced ? head.slice(0, -1) : head) if (!spec) { diary.say(`E492: not an editor command: ${head}`, 'error') return } + if (forced) { + if (!spec.bang) return diary.say(`E477: no ! allowed: ${head}`, 'error') + await spec.bang.run(arg) + return + } await spec.run(arg) }