ZenNotes 2.27.0: your tabs come back before the vault scan
On a big vault, launching ZenNotes showed the window chrome and then a blank workspace for as long as the full note listing took: the saved tabs could not paint until every note on disk had been walked. The workspace snapshot is a few hundred bytes; the index is the expensive part. They are decoupled now: your tabs and the active note appear immediately, and the vault scan fills the note list in behind them. Also in this release: accepting a
{{variable}}in the template editor no longer collides with auto-pair brackets, so{{cursor}}stays{{cursor}}instead of growing a second pair of closers. Installers for macOS, Windows, and Linux (x64 + arm64) are attached below.
✨ New
-
Jump straight to a tab:
{count}gtand Alt+1 through Alt+9.gt/gTcould only cycle, so reaching a specific tab meant hammering the same key past every stop (#497, requested by @uNyanda). The vim half now follows vim's own contract:2gtgoes to tab 2,5gtto tab 5, a count past the end lands on the last tab, plaingtstill cycles, and{count}gTstays relative (that many tabs back). The non-vim half is nine rebindable shortcuts, Alt+1 through Alt+9 (Ctrl+1 through Ctrl+9 on macOS, where Option types characters on many layouts and the Cmd digits already mean sidebar, connections, and pane modes). Tab numbers count across panes in the same ordergtcycles, so cycling and direct jumps always agree about which tab is number 3. Direct selection targets open tabs only: no recent-notes fallback, "tab 3" is an open tab or nothing. The release review shielded the shortcuts from every keyboard reality it could find: they stand down while a modal, palette, or menu is open, they yield to any combination you had already rebound to another action before these defaults shipped, Alt plus numpad digits stay reserved for Windows Alt-code character entry, and digit bindings also match the physical digit row, so Alt+1 fires on layouts like French AZERTY where the digit keys type punctuation. One macOS heads-up: with multiple Spaces, Mission Control claims Ctrl+digit for Switch to Desktop before the app ever sees the key; rebind under Settings → Keymaps or free the shortcut in System Settings.How to test locally: open three or more tabs. Vim on:
2gtlands on tab 2,9gton the last tab,2gTtwo back. Any mode: Alt+2 (Ctrl+2 on a Mac) jumps to tab 2. All nine bindings appear under Settings → Keymaps as "Go to tab N".
🐛 Fixes
-
Saved tabs paint before the vault scan finishes. Opening a vault with many notes left the tab strip and editor blank for seconds while the app walked the whole vault, because startup awaited the full note index before restoring the saved workspace, and the restore checked every saved tab against that index before it would show anything (#564, reported by @junereycasuga, who traced the ordering in
store.tsthemselves; the fix follows the shape they proposed).The order is reversed now, in startup and in every switch-vault flow: the saved workspace is restored first and trusted, then the index refresh runs behind it. Trusting the snapshot is safe because everything it could get wrong already has a corrector. The active tab's content is read straight from disk, so a saved tab whose note is gone fails that read and is dropped on the spot. Background tabs whose notes vanished while the app was closed are pruned the moment the real listing arrives, by the same reconciliation that has guarded mid-session refreshes since #384. Until note metadata lands, a tab title falls back to its filename, then upgrades in place. The release review added the belt to those suspenders: the moment the first listing lands, every restored tab is checked against it once, so a snapshot synced from another machine can no longer promote a never-verified tab to active and leave a ghost with a blank editor alive for the session.
How to test locally: open a vault with a few thousand notes, open several tabs, quit, relaunch. Before: window chrome appears, then the tab strip stays empty until the scan completes (the bigger the vault, the longer the blank). After: your tabs and the active note are there immediately, and the sidebar note list streams in as the scan finishes. To see the pruning, delete one of the background tabs' files while the app is closed: the tab shows briefly on relaunch and disappears when the index arrives.
-
Template variables stop doubling their closing braces. With auto-pair brackets on, accepting a variable from the template editor's
{{popup produced four closers:{{cursor}}}}(#566, reported by @ryanthara, who pinned it to the exact feature combination). Typing{{auto-pairs to{{}}with the caret in the middle, which is right on its own; the completion then replaced only the text up to the caret with the full{{cursor}}, so the auto-paired closers survived after it. Typing the braces by hand was never affected (a typed}steps over an auto-paired one); only the popup left extras behind.The completion's inserted text carries its own
}}, so accepting it now swallows up to two closing braces already sitting at the caret. That cap is deliberate: it is exactly what the insert provides, so any braces beyond the pair are left alone. A review catch tightened it further: the braces are only consumed when they can belong to the variable's own{{. If an earlier{{on the line is still open (prose documenting Handlebars or Jinja syntax), the closers after the caret belong to it, are literal note content, and stay. The same change fixes a cousin that never needed auto-pair at all: editing an existing variable in place ({{date}}cut down to{{ti}}, then accepting{{time}}) used to leave the old pair's braces behind too.How to test locally: with Settings → Editor → Auto-pair brackets on, open Settings → Templates → New template, type
{{curand accept the completion. Before:{{cursor}}}}. After:{{cursor}}. Then with auto-pair off, make{{date}}, deletedate, typeti, accept{{time}}: also clean now. -
Vim's pending
fmotion owns the next key over themmenu. In visual mode,f mopened the editor context menu instead of extending the selection to the nextm, and the orphaned motion stayed pending: after closing the menu, the next key typed was swallowed as theftarget and the selection jumped somewhere unasked (#568, reported by @uNyanda, whose leak observation pointed at the real layer). Themshortcut lives in a window-level capture listener that runs before CodeMirror ever sees the event, so vim never got the chance to consume the key as an operand.The shortcut now reads the vim plugin's own pending state and yields whenever a sequence is in flight: after
f/t/rawaiting their character, or with any buffered prefix (a count like2, agprefix, a register).v f mfinds them,d f mand2 f mkeep their operands, a plainmin visual mode still opens the menu, and the dedicated context-menu key is untouched. The stale-pending leak is gone structurally, since the menu can no longer open mid-sequence.How to test locally: with vim mode on, open a note with a line like
alpha m bravo, press0 v f m. Before: the context menu opens and the selection stays put. After: the selection extends through themof "alpha m", no menu. Escape, then0 v m: the menu opens as always. -
:wasaves everything instead of closing everything. The:wa/:wallex commands were registered as plain aliases of:qall, on the theory that autosave leaves nothing to write, so "write all" was read as a no-op and the close-everything behavior leaked in (#569, reported by @simonlearnscoding, whose muscle memory is the correct spec). In vim,:wais a save command: write all changed buffers, close nothing. Typing it reflexively after an edit demolished the whole tab layout. Nothing was ever lost (closing flushes dirty buffers), but the workspace was.:wa/:wallnow write every unsaved note across all panes and touch no tab. The quit-and-write family keeps closing, exactly as vim defines it::xa/:xallstay, and:wqa/:wqallis newly registered so the common spelling of "save everything and quit" works too.:qaand friends still close everything,:wstays write-only, and:q/:wqstill close just the active note. No toggle: vim semantics are the only behavior now.How to test locally: with vim mode on, open a few tabs, edit a note, type
:wa. Before: every tab closes. After: nothing closes and the edit is on disk.:wqaor:xastill close everything when that is what you asked for. -
Image size hints actually resize the image.
![[assets/pic.jpg|100x50]]and the markdown spellingrendered full-width with the hint leaking into the alt text, in the editor and the reading view alike, even though the help promised size hints "same syntax as images" (#570, reported by @corpserot, who tried both spellings and quoted the help back at us). Only Excalidraw embeds ever honored them.Images now share the drawings' hint parser. Both spellings work in Edit, Preview, and PDF export:
|300sets width with height following the aspect ratio,|600x400sets both, captions keep their text (shows "cognitive web"), and pipes inside captions survive. A hint wider than the pane is still capped by the layout. Review refinements: a zero dimension is not a resize (|0x300stays a caption instead of eating the label and distorting the image), a purely numeric alt in the markdown spelling stays your caption (is a year, not a 2024px width; writeto size it), and an image that scrolls away and back no longer resurrects a size hint you already edited out.How to test locally: drop an image in
assets/, add![[assets/pic.jpg|100x50]]andto a note, check Edit and Preview. Before: two full-width images with the hint as alt text. After: both at 100x50 with clean captions.
🧰 For contributors
- All eight
refreshVaultIndexes+restoreWorkspaceForVaultcall sites (init plus the seven vault open/switch/connect flows) now go through oneopenVaultWorkspaceorchestrator instore.ts. It restores, refreshes, and then does the two things that genuinely need the index: folding freshly discovered folder rows into the startup-collapsed set, and scheduling the background-tab content warm-up. If you add a new way to enter a vault, call the orchestrator, not the pieces. sanitizeWorkspaceLayoutandinitialWorkspaceRestoreContentPathsno longer take anexistingPathsset: at restore time there is deliberately no index to check against. Existence is enforced where it can actually be observed: the eager disk read for active tabs, a one-time strict check inopenVaultWorkspacethe moment the first listing lands (this is where ghost tabs from stale synced snapshots die; dirty tabs survive it, and an empty listing skips it because it is indistinguishable from a failed one), andrefreshNotespruning from then on, whose selectedPath exemption now also requires the note to be loaded or dirty. Resist re-adding an index check to restore itself; it is the exact wait #564 was about.- The startup folder-collapse rule runs in two stages now. Restore decides
quick:andinbox:(the only rows that exist before the index); the orchestrator appends keys for folders the index discovered, minus the active note's ancestors, and leaves the two early keys alone in case the user toggled them during the gap.computeStartupCollapsedFoldersitself is unchanged. workspaceRestoredflips true beforenotesis populated now. Everything reading it was audited (persistWorkspace, pending open-note requests, the ready perf mark) and behaves correctly, but new code gated on it must not assume the notes index is loaded.- The template variable apply lives in
templateVariableApplySpec(cm-template-variables.ts), a pure transaction builder in thecm-auto-pairsstyle, so the closer-swallowing edge cases are unit-tested without a mounted editor. If another completion source ever inserts text that ends in a delimiter the editor auto-pairs, it has the same leftover problem and should apply through the same pattern. - The ex-command surface is documented in two mirrored places (in-app
help.tsand the website docs table) and both listed:wain the close-everything row; #569 split that row. If an ex command's behavior changes, both surfaces change in the same commit or they drift. - Keymap entries live in THREE mirrored places:
keymaps.ts(normative), the slim@shared/keymaps-catalog(whose mirror test is what catches an omission), and the help/website shortcut tables. #497's ninetabs.selectNentries touched all of them.getBufferSelectTargetsharesopenTabOrderwith the cycler on purpose: if tab numbering and cycling ever came from different lists,3gtand pressinggtthree times could land on different tabs. - CM-Vim hands actions their count as
actionArgs.repeatwithrepeatIsExplicitdistinguishing a typed1from no count at all. ThenextBufferaction uses that to give1gtabsolute-jump semantics while a baregtcycles; any future count-aware action should read the same flag rather than comparing repeat to 1. - Setting width/height ATTRIBUTES on an embed image is not enough: the embed class stretches images to
width: 100%, and presentational attributes lose to any CSS rule, so the #570 hint also lands as inline style. If a rendered size ever looks wrong despite correct HTML, measuregetBoundingClientRectin the running app before trusting the markup.splitEmbedLabel(excalidraw-preview.ts) is the one label-splitting rule; the reading view applies it in a singleremarkImageSizeHintspass because both embed spellings arrive there as image nodes. isVimAwaitingArgument(view)(vim-nav.ts) is the one place that answers "is vim mid-sequence and entitled to the next key". Any app-level single-key shortcut that can fire while the editor is focused in vim mode should consult it, the way both VimNav menu sites and the EditorPanemhandler now do. (#568 briefly shipped a duplicate namedvimAwaitsNextKey; the release review folded it back in, and the real-editor coverage lives invim-nav-awaiting-argument.test.ts. There is deliberately one name for this predicate.) The window keydown listener in VimNav runs with capture, before every CodeMirror handler: a shortcut added there intercepts keys vim never sees, which is exactly how #568 happened.