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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01476ZKdB4k3rPDwvVhXcnDg
This commit is contained in:
nvrlandClaude Opus 5 committed 2026-09-05 06:48:22 +02:00
1 parent e0565ea1b4
commit bf3c1f547a
4 files changed
+31 -9

No files matched your search

+11 -2
View File
@@ -198,11 +198,20 @@ input::placeholder { color: var(--fg-faint); }
.cm-editor .cm-activeLine { background: color-mix(in srgb, var(--fg) 4%, transparent); } .cm-editor .cm-activeLine { background: color-mix(in srgb, var(--fg) 4%, transparent); }
.cm-selectionBackground, .cm-content ::selection { background: var(--select) !important; } .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-left: 2px solid var(--accent);
border-right: 0; 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, * Normal mode: the vim plugin paints the character *inside* the block cursor,
+4 -3
View File
@@ -45,11 +45,12 @@
for (const spec of commands) { for (const spec of commands) {
const names = [spec.name, ...(spec.aliases ?? [])] const names = [spec.name, ...(spec.aliases ?? [])]
for (const name of names) { for (const name of names) {
if (name.includes('!')) continue
Vim.defineEx(name, name, (_cm: unknown, params: { argString?: string }) => { Vim.defineEx(name, name, (_cm: unknown, params: { argString?: string }) => {
const arg = (params.argString ?? '').trim() const arg = (params.argString ?? '').trim()
// vim parses `:q!` as command `q` with the bang left in the arguments. // vim parses `:q!` as command `q` with the bang left in the arguments,
void runCommand(arg === '!' ? `${spec.name}!` : `${spec.name} ${arg}`) // 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}`)
}) })
} }
} }
+4
View File
@@ -46,6 +46,10 @@
{#if spec.args}<span class="faint"> {spec.args}</span>{/if} {#if spec.args}<span class="faint"> {spec.args}</span>{/if}
</dt> </dt>
<dd>{spec.help}</dd> <dd>{spec.help}</dd>
{#if spec.bang}
<dt>:{spec.name}!{#if spec.aliases?.length}<span class="faint"> ({spec.aliases.map((a) => `${a}!`).join(' ')})</span>{/if}</dt>
<dd>{spec.bang.help}</dd>
{/if}
{/each} {/each}
</dl> </dl>
</section> </section>
+12 -4
View File
@@ -16,6 +16,8 @@ export interface CommandSpec {
args?: string args?: string
help: string help: string
run: (arg: string) => void | Promise<void> run: (arg: string) => void | Promise<void>
/** The forcing variant, `:name!` — vim hands the bang over as an argument. */
bang?: { help: string; run: (arg: string) => void | Promise<void> }
} }
const requireOpen = () => { const requireOpen = () => {
@@ -59,9 +61,7 @@ export const commands: CommandSpec[] = [
} }
hooks.focusList() hooks.focusList()
}, },
}, bang: {
{
name: 'q!',
help: 'discard changes and close the editor', help: 'discard changes and close the editor',
run: async () => { run: async () => {
const open = diary.open const open = diary.open
@@ -71,6 +71,7 @@ export const commands: CommandSpec[] = [
hooks.focusList() hooks.focusList()
}, },
}, },
},
{ {
name: 'new', name: 'new',
aliases: ['n', 'o'], aliases: ['n', 'o'],
@@ -244,11 +245,18 @@ export async function runCommand(line: string): Promise<void> {
return 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) { if (!spec) {
diary.say(`E492: not an editor command: ${head}`, 'error') diary.say(`E492: not an editor command: ${head}`, 'error')
return return
} }
if (forced) {
if (!spec.bang) return diary.say(`E477: no ! allowed: ${head}`, 'error')
await spec.bang.run(arg)
return
}
await spec.run(arg) await spec.run(arg)
} }