From 33224327fb940d103d0b7a5d0dc8ff914c80ccde Mon Sep 17 00:00:00 2001 From: Ethan Graf Date: Mon, 3 Aug 2026 21:57:40 -0400 Subject: [PATCH] Let `#` accept the highlighted note and continue into headings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing `#` part-way through a note name left the literal typed text as the target — `[[Dai#` resolves to nothing, so the heading popup never appeared and you had to go back and finish the name by hand. `#` now accepts whatever the popup has highlighted and moves straight on, so a few letters then `#` gets you to the heading list. It deliberately does not close the link: the `]]` is added when the heading is accepted, so `[[Targ` + `#` + pick lands as `[[Target#Middle Section]]` in one pass. Bound above `completionKeymap` so it sees the key first, and it returns false whenever no option is highlighted — outside the popup, `#` is ordinary text. The transaction is flagged `input.complete`, so the heading popup is requested explicitly rather than relying on activate-on-typing. Co-Authored-By: Claude Opus 5 --- .../automerge/livePreview/wikiLinkComplete.ts | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/editors/automerge/livePreview/wikiLinkComplete.ts b/src/editors/automerge/livePreview/wikiLinkComplete.ts index 74c5a6e..bb3a8db 100644 --- a/src/editors/automerge/livePreview/wikiLinkComplete.ts +++ b/src/editors/automerge/livePreview/wikiLinkComplete.ts @@ -13,10 +13,12 @@ * editing is untouched the rest of the time. */ import { Prec, type Extension } from '@codemirror/state'; -import { EditorView, keymap } from '@codemirror/view'; +import { EditorView, keymap, type Command } from '@codemirror/view'; import { autocompletion, completionKeymap, + selectedCompletion, + startCompletion, type Completion, type CompletionContext, type CompletionResult, @@ -166,6 +168,46 @@ function renderHeadingLevel(completion: Completion): Node | null { return el; } +/** + * Typing `#` inside the note-name popup accepts whatever is highlighted and + * moves straight on to choosing a heading. + * + * Without this, `[[Dai#` leaves the literal text you typed as the target, which + * usually resolves to nothing — so the heading popup never appears and you have + * to go back and finish the name by hand. Accepting the highlighted option + * first means you can type a few letters and keep going. + * + * Deliberately does NOT close the link: the `]]` is added when the heading is + * accepted, so the caret stays inside `[[Name#` ready for the next popup. + */ +const acceptTargetAndStartAnchor: Command = (view) => { + const { state } = view; + const selected = selectedCompletion(state); + if (!selected) return false; + + const pos = state.selection.main.head; + const line = state.doc.lineAt(pos); + const before = line.text.slice(0, pos - line.from); + const open = before.lastIndexOf('[['); + if (open < 0) return false; + + const typed = before.slice(open + 2); + // Only in the name part; past a `#` or `|` this key is ordinary text. + if (!STILL_A_TARGET.test(typed)) return false; + + const from = line.from + open + 2; + const insert = `${selected.label}#`; + view.dispatch({ + changes: { from, to: pos, insert }, + selection: { anchor: from + insert.length }, + userEvent: 'input.complete', + }); + // The transaction is flagged as a completion, so ask for the heading popup + // explicitly rather than relying on activate-on-typing. + startCompletion(view); + return true; +}; + export const wikiLinkCompletion: Extension = [ autocompletion({ override: [wikiLinkCompletionSource], @@ -176,5 +218,12 @@ export const wikiLinkCompletion: Extension = [ defaultKeymap: false, icons: false, }), - Prec.highest(keymap.of(completionKeymap)), + // Above completionKeymap so it sees `#` before the popup does; it falls + // through to ordinary typing whenever no option is highlighted. + Prec.highest( + keymap.of([ + { key: '#', run: acceptTargetAndStartAnchor }, + ...completionKeymap, + ]), + ), ]; -- 2.51.2