Repository navigation
Ctrl+P Opens The Notes Picker In Quick Capture - #944
Merged
Merged
Conversation
Quick capture renders as its own window root, so App.tsx and its capture-phase handler are never loaded there. Key handling was a plain window listener in the bubble phase, and codemirror-vim consumes Ctrl+P, Ctrl+N and Ctrl+W as normal-mode chords and stops propagation. Three of the four shortcuts the footer advertises were dead in exactly the mode they matter in. The main window already fixed this twice (ZenNotes#510, ZenNotes#579); neither fix reached this window. A capture-phase listener now claims those chords, and only while no overlay is open. Inside an overlay its input has focus and the palette's own isPalettePreviousKey/isPaletteNextKey branch navigates, so standing aside there is what keeps Ctrl+P navigating rather than closing. That is the main window's behaviour, and Ctrl+N always worked there because the overlay guard returns early before newNoteHere. Mod+W needed different treatment, because useCloseWindowShortcut is shared with the main window. It takes a capture flag that only this call site sets; the main window leaves it off, since it already resolves that chord itself and doing both would double-handle it. Three further defects in the same window, all found while testing the first: - The picker rows put the filename last behind the path, so the filename truncated to a stub while the path stayed readable. This is ZenNotes#635, whose fix landed in Open Buffers and never reached here. The row layout now mirrors BufferPalette, and the path column shows only the subpath, since the folder is already labelled on the left and the filename sits beside it. - Neither overlay scrolled the selection into view, so moving past the bottom of the list left the highlight off screen. Both now do, keyed off data-qc-idx to stay clear of SearchPalette's data-search-idx. - The footer is now state-aware, and the command overlay no longer claims Mod+N/Mod+P on rows whose chords move the selection instead. The navigation hint reads Ctrl, not Mod, and that is load-bearing rather than stylistic. isPaletteNextKey and isPalettePreviousKey match on ctrlKey and reject metaKey, so Cmd+N/Cmd+P navigate in no palette on any platform. Every other palette hardcodes the literal for that reason. An earlier draft used modKey here, which would have rendered a Cmd hint that does nothing on macOS. 11 tests added. They dispatch on document for the chords that open or create, so the capture path runs while the editor never sees the key, and on the focused input for navigation inside an overlay. Each puts Vim into normal mode first, since insert mode is where the bug does not reproduce.
adibhanna
added a commit
that referenced
this pull request
Oct 14, 2026
…mes your key Review follow-up to #944, kept apart so @uNyanda's commit stands as theirs. The picker's path column is right to left so its ellipsis lands on the left, and #944's comment says an LRE/PDF pair keeps the path itself left to right, as Open Buffers does (#635). The pair was missing from the code, so a subpath that starts with digits or punctuation came out reordered: a note in inbox/2026/Planning showed "Planning/2026". The path is now wrapped in U+202A and U+202C, the same string BufferPalette renders, and the test pins it. The footer's new close hint was written as Mod+W, but the key behind it is Close active tab, which follows a rebind and can be unbound (#893). A rebound key was advertised as Mod+W, and an unbound one as a key that does nothing. useCloseShortcutDisplay reads the binding the way useCloseWindowShortcut does, kept current through the same storage event, and the footer shows it, or leaves the hint out once it is unbound. The in-app help's Quick capture section gains Ctrl+N / Ctrl+P: inside the note picker and the command palette they move the selection, which the footer now says and the help did not. Checked: the app-core typecheck and its full suite (3,235 tests, 2 new), and the built app over CDP. With Vim on and the editor in normal mode, Ctrl+P opens the picker, Ctrl+N and Ctrl+P move within it, Ctrl+N outside it saves the draft to Quick Notes and starts a new one, the footer reads "⌘W close", and the 2026/Planning row reads in order.
Contributor
|
Thanks @uNyanda, merged into the I checked it in the built app with Vim on and the editor in normal mode. A follow-up commit, f8e7fe1, changes three things:
|
This was referenced Oct 14, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three of the four shortcuts the Quick capture window advertises in its footer do nothing when Vim mode is on and the editor holds focus:
Mod+EntersaveMod+NnewMod+PnotesMod+⇧PcmdMod+Wwas dead the same way and was not advertised at all. Insert mode is the only mode where any of them worked, because it is the one mode where Vim claims none of them.Fixes #942.
The Cause
Quick capture renders as its own window root, so
App.tsxis never loaded in that window. Its key handling was a plainwindowlistener in the bubble phase, and codemirror-vim consumesCtrl+P,Ctrl+NandCtrl+Was normal-mode chords and stops propagation, so the listener never saw them.The main window already solved this twice. #510 moved its palette shortcut into a capture-phase handler so
Ctrl+Pwins over Vim's cursor-up. #579 recorded that a shortcut "remapped onto an editor chord (Ctrl+P, Ctrl+E, …), the bubble-phase handler ran only after CodeMirror had already consumed the key, so the shortcuts went dead exactly when the editor held focus." Quick capture is the one window with no capture-phase handler at all.The Change
A capture-phase listener handles the three chords that collide, and only while no overlay is open. Those branches then come out of the bubble handler, so no chord is handled in two places.
Mod+Wneeded different treatment, becauseuseCloseWindowShortcutis shared with the main window. It now takes acaptureflag that only the Quick capture call site sets. The main window leaves it off, since it already resolves that chord in its own capture-phase handler.The footer now tracks state: the three chord hints when nothing is open, and
↑↓ Ctrl+N/P moveplusesc closewhile a palette has focus.Advertising
Mod+W closein the no-overlay state is a deliberate choice. Escape is the shortcut the window already documents for closing and it still works, but in insert mode Escape exits insert mode first and only a second Escape closes the window, so closing costs two presses and the first appears to do nothing useful.Mod+Wcloses in one press, in every mode including insert. It was unadvertised, and in normal mode broken, so the one chord that closes without the double press was both hidden and half broken.It is advertised only when no palette is open because it does not work when one is.
useCloseWindowShortcutis givenoverlayOpenas its blocker, soMod+Wis inert while the picker or command palette has focus, which is the same rule the main window follows. That is why the palette branch showsesc closerather than the close chord:Escapedismisses the palette and the close chord would not.The command palette's rows no longer claim
Mod+NandMod+P, because inside that palette those chords navigate the list rather than running the command.The navigation hint reads
Ctrl, notMod, and that is not a choice.isPaletteNextKeyandisPalettePreviousKeymatch onevent.ctrlKey && !event.metaKey, soCmd+N/Cmd+Pnever navigate in any palette on any platform. Every other palette already hardcodes the literalCtrl+N/P movefor exactly this reason, and Quick capture now does the same. An earlier draft of this footer usedmodKey, which would have rendered⌘N/P moveon macOS and reintroduced the exact defect #939 was filed about.A Second Defect In The Same Window
Three separate problems lived in this one window. All three are in the PR because all three were found while testing the first.
The picker rows put the filename last, behind the path. The filename truncated to a stub while the path stayed readable, which inverts the priority: the user is picking by filename, and the path only exists to separate notes that share one.
This is #635, which is closed. That fix landed in Open Buffers and never reached Quick capture's picker, which carries the same row layout.
The fix mirrors the one already in
BufferPaletterather than inventing a second shape:flex-1column, so it absorbs only what is left and collapses first.dir="rtl"moves the ellipsis to the left edge, so a path that no longer fits keeps its distinguishing tail.text-rightholds it against the row's right edge, which is whereml-autoused to put it.The column also stops repeating what the row already shows. The folder is on the left as an uppercase label and the filename sits beside it, so only the subpath within the folder is rendered. A note at
inbox/ZenNotes/Enhancements/Calendar Panel Tasks Placement.mdshowsZenNotes/Enhancements, not the full path.Neither overlay scrolled the selection into view. Moving the highlight past the bottom of the list left it out of sight, with no way to tell which row was selected.
SearchPalette,CommandPaletteandBufferPaletteall scroll the active row already, so this was an omission specific to Quick Capture rather than a missing idea. Both overlays now carry the same effect, keyed off adata-qc-idxattribute on each row.Why Not The Other Way
There were two ways to resolve the overlap between
Ctrl+NandCtrl+Pas Quick Capture shortcuts and as palette navigation keys.Option A: let the shortcuts win everywhere. Keep
Mod+Nstarting a new note andMod+Popening the picker even while a palette is open, and narrow palette navigation toCtrl+JandCtrl+K. The footer would advertise↑↓ Ctrl+J/K move.Option B: the palette owns the keyboard while it is open.
Ctrl+NandCtrl+Pstart a new note and open the picker only when nothing is open. Inside a palette they navigate. The footer advertises↑↓ Ctrl+N/P move. This is what is implemented.Option B is the main window's existing behaviour, and that is the reason for choosing it.
The only chord that actually misbehaved there was
Ctrl+P, which toggled the palette rather than navigating. That is #939, fixed by the branch this sits on top of.Ctrl+Nalways navigated correctly, because the overlay guard inApp.tsxreturns early when a palette is open, so thenewNoteHerehandler behind it never fires. Nothing aboutCtrl+Nneeded resolving in the main window, and the same guard shape is what this PR gives Quick Capture.Option A was considered and rejected on two grounds.
It would narrow navigation for no reason. With
Ctrl+Nalready correct everywhere, taking it away inside a palette is a regression dressed as a fix.It creates dead keys. Once the capture handler stands aside while an overlay is open and the bubble branches are gone,
Ctrl+NandCtrl+Pdo nothing there. Under Option A that is the goal, but it has to be arrived at deliberately, and the failure mode is the reverse one: narrowing navigation toCtrl+JandCtrl+Kwithout also re-enabling the shortcuts leaves two inert keys in every palette.The trade is that
Ctrl+Nno longer starts a new note from inside the command palette, where it did before in Quick Capture specifically. It is the same trade the main window makes, and it is why the gate on "no overlay open" is the load-bearing line in the capture handler.Tests
QuickCaptureApp.test.tsgains 11 tests, all passing. The file is now 25 tests, 24 passing, and the one failure iscancels a Vim character selection, a 5000 ms timeout that is pre-existing flake on this machine: it fails identically on a stashed clean tree.The existing #765
Escapecases and bothCmd+Wclose-shortcut cases still pass, which is the regression risk.Escapedeliberately stays in the bubble phase because its branch readse.defaultPreventedon the grounds that CodeMirror has already run, so it is the chord most at risk from the listener moving phase.Reaching the phase under test
The chords are claimed in a capture-phase listener on
window, because codemirror-vim consumesCtrl+P,Ctrl+NandCtrl+Was normal-mode chords and stops propagation. A test that dispatches on the editor therefore proves nothing, since the editor eats the key before any handler sees it.Two dispatch targets are needed, and the distinction is the whole point:
onWindow(...)dispatches ondocument. The capture path still starts atwindow, so the listener runs, while the editor never sees the key. This is how the chords that open or create are tested.press(input, ...)dispatches on the focused overlay input, where a real keystroke lands once an overlay is open. This is how navigation inside an overlay is tested.Every chord test also puts Vim into normal mode first, via one
Escape, because insert mode is the mode where the bug does not reproduce.What each test asserts
The chords now fire in normal mode. Four tests, one per chord.
opens the note picker on Mod+P in normal mode, where Vim would take itstarts a new note on Mod+N in normal modecreateNotewas called and no overlay opened, which is what Vim's cursor-down used to do instead.hides the window on Mod+W in normal mode, keeping the draftwindowClosecalled exactly once. This is thecaptureflag on the shared hook.opens the command palette on Mod+Shift+P in normal modeRun a command…placeholder rather than the picker's, so the two overlays cannot be confused.Navigation wins once an overlay has focus. Three tests, which is the other half of the fix: the capture listener has to stand aside, or the chord re-enters it and toggles the overlay shut.
moves down rather than toggling once the picker has focusCtrl+Nlands on row 1. The picker is still open and still has 2 rows, so it navigated rather than closing.holds at the top row on Ctrl+P instead of closing the pickerCtrl+Pfrom the top row clamps and leaves the picker open. This is the direct regression test for the reported symptom. Then a separate step moves down and back up to proveCtrl+Pstill navigates when there is somewhere to go, so the clamp cannot be passing merely because the chord does nothing.stops moving at the last row instead of wrapping or closingCtrl+Npast the end holds on the last row, rather than wrapping to the top or closing.The footer tells the truth. Two tests.
swaps the footer hints while an overlay is opennotesandclose; with the picker open it showsmoveandesc close, and no longer advertisesneworcmd, which do nothing there. Also assertsCtrl+N/Pis present and⌘N/Pis absent, which pins the Ctrl-only rule described above.drops the two command hints that no longer run inside the command paletteSave and start a new noteandOpen another note…are still listed but carry noMod+N/Mod+Phint, andMod+Enterkeeps its↩because it is not a navigation chord.The two defects from the same window. Two tests.
shows only the subpath beside the filenameinbox/ZenNotes/Enhancements/Calendar Panel Tasks Placement.mdthe row renders the title andZenNotes/Enhancements. Assertsinboxappears exactly once, so the folder label is not repeated in the path column, and that no cell ends in.md, so the filename is not either.scrolls the active row into view in both overlaysCtrl+NcallsscrollIntoView({ block: 'nearest' })in the picker, then again in the command palette after dismissing the first. Both overlays get their own check because they are separate components with separate effects.Notes on the test environment
Three things were needed that the file did not have, and each is worth knowing before adding the next test here.
Element.prototype.scrollIntoViewhas to be stubbed. jsdom has no layout, so it is not implemented, and the new scroll effect calls it on mount. Without the stub the failure surfaces as an unrelated-lookingTypeErrorinside a React passive-effect commit hook.Notes are seeded by re-running
definePropertyonwindow, not by assignment.beforeEachinstallswindow.zenwithoutwritable: true, sowindow.zen = ...throws.activeRow()matches on the exact class tokenbg-paper-200viaclassList, not on a substring. The inactive row carrieshover:bg-paper-200/60, which contains the active row's class name as a substring, so a naiveclassName.includespicks the wrong row and makes every navigation assertion pass or fail for the wrong reason.Known Gaps
Ctrl+NandCtrl+Wat the keyboard level is still only covered by hand. The manual pass is the only thing that exercises the real chord path.Cmd+NandCmd+Pdo not navigate inside any palette.palette-nav.tsmatchesctrlKeyand rejectsmetaKey, so on macOS the chords a user would reach for are inert inside every palette in the app. That is pre-existing and app-wide, and out of scope here, but this PR does make Quick capture's footer state the rule rather than hide it.Ctrl+Pcursor-up is unavailable in Quick capture once this lands. That matches the main window, where the app shortcut already wins, but it is a change for anyone relying on the Vim binding in that window.Mod+EnterandMod+Shift+Pmoved to capture phase for consistency rather than need. They work today. Leaving one chord in the bubble handler while its neighbours are in the capture handler is the arrangement that produced this bug.Review Focus
The
captureflag onuseCloseWindowShortcutis the part worth a careful look. It is the only change touching shared code, and getting it wrong would either double-handle the chord in the main window or leaveMod+Wbroken there.Second is the gate on
overlayRef.current !== 'none'in the capture handler. It is the single line that keeps Option B honest, and the three navigation tests are what hold it in place.