Skip to content

ZenNotes 2.41.0

Latest

Choose a tag to compare

@github-actions github-actions released this 01 Sep 16:28
· 1 commit to main since this release

ZenNotes 2.41.0: code stays code, tables learn yank and paste, and a web-exported note carries its title

Three fixes. In Vim mode, y and p now work inside WYSIWYG table cells, through the same registers as the rest of the note, so text moves freely between a cell and the body in either direction (#706, reported by @skuong). Auto-close Markdown no longer fires inside code blocks, so typing == in code stops wrapping itself into a highlight pair (#718, reported by @MatthewHunt84). And exporting a note to PDF from the self-hosted web app no longer drops its title: the web export window is now connected to the same title rule the desktop app has used since 2.20.0.

🐛 Fixes

  • Auto-close Markdown keeps its hands off code. (#718, reported by @MatthewHunt84) With Auto-close Markdown on, typing == and then a space wrapped the pair into a ==highlight== snippet, everywhere, including inside a fenced code block, where a == b is just a comparison. The workaround was turning the whole feature off, which threw away auto-closing **, fences, and math blocks in prose to stop one wrong trigger in code. Now the snippet engine asks the syntax tree where it is before firing: inside a fenced code block or an inline code span, no inline pair fires (==, **, ~~, a backtick, [[, %% all stay literal), and a $$ or nested fence marker typed inside someone else's code block no longer expands into a math block on Enter. Everything in prose is untouched, including the one case that looks like code but is not: typing ``` on a fresh line still expands into a fence with Enter, because that line's own unclosed fence is exactly what the snippet exists for. This is not a new setting, on purpose: markdown formatting means nothing inside code, so there is no situation where firing there is right. How to test locally: npm run dev, make a note with a fenced code block, type `a == b` inside it followed by a space. Before: the moment you hit space, `==` wrapped into a highlight pair around the cursor. After: it stays `a == b`, and the same `==` followed by space in a prose paragraph still wraps like it always did.

  • Yank and paste work inside table cells. (#706, reported by @skuong) With Vim mode on, a WYSIWYG table cell runs its own modal normal/insert mode, and that mini-Vim knew how to delete (x, dd, diw) but not how to yank or paste: y and p were swallowed as stray keys, and the one yank that existed (visual mode y) wrote straight to the system clipboard, where no p could reach it. Now the cell cursor speaks the rest of the note's language. y works as an operator (yy or Y for the cell, yw, y$, yiw, ya" and friends), p and P paste around the block cursor, visual selections yank and paste too (v l y, and visual p replaces the selection, swapping the replaced text into the register like Vim does). All of it goes through the same unnamed Vim register as the main editor, which is the point: yank a word in the note body and paste it into a cell, or yy a cell and p it onto a line of prose, in either direction. Deletes feed the register too, so dd in one cell and p in another moves the text, and with "Sync clipboard with Vim registers" turned on, table yanks and pastes ride the system clipboard exactly like editor yanks. A multi-line register flattens to a single line on paste into a cell (interior breaks become spaces) so a row's markdown can never be broken by what you paste. How to test locally: npm run dev, vim mode on, make a note with a table. Click a cell, press yy, click another cell, press p: the text lands there. Then press Escape to the note body, put the cursor on a word, yiw, click back into a cell, p. Before: nothing at all happened on any of those keys. After: they behave like Vim.

  • A note exported from the web build carries its title. Export a note to PDF from the self-hosted web app and the title was missing whenever the body did not already open with an H1, so a frontmatter-titled note (or any note that leans on its filename for a title) exported as a page that begins on its first paragraph. Exporting the same note from the desktop app was fine, which is what makes this worth calling a bug rather than a choice: the two builds disagreed about the same note. The rule itself already existed and was already shared. withExportTitle resolves what an export should be called (a body H1 wins outright, then frontmatter title:, then the note's filename) and inserts it as a leading heading only when the body does not state one, so the title flows through the ordinary rendering pipeline and picks up the note's own typography. It was wired into the desktop PDF window, the Word export and the copy-for-email path when it was written, and its header comment says it covers every export format built on it. The web export window predates that work by three months and was missed. This connects it to the same helper, with no second definition of what an export title is, and nothing changes for a note that already opens with its own H1. The browser's print dialog seeds the saved file's name from document.title, so that is resolved through the same helper too: a note that names itself in frontmatter no longer saves itself under its filename. How to test locally: run npm run dev:web with the Go server on :7878, open a note whose body has no # heading (a frontmatter-titled note is the clearest case), and export it as a PDF. Before: the PDF opens on the first paragraph and the title is nowhere on the page. After: the note's title is the first line, and the print dialog offers that title as the filename.

🧰 For contributors

  • The #718 guard lives in cm-markdown-snippets.ts (isInsideCode), checked at trigger time on a settled state, never inside the StateField update where the syntax tree can be a keystroke stale. The fence-opener exemption matters: the moment ``` is typed it parses as an unclosed FencedCode, so a naive inside-code check would kill the Enter-to-close feature itself.

  • Two harness lessons paid for during live validation, for anyone driving the app over CDP: Input.dispatchKeyEvent only synthesizes text when the event is fully specified (key, code, both virtual key codes, text AND unmodifiedText), and it needs the window to hold OS key-window status, so a busy machine gives nondeterministic dead keys. And an empty isolated ZENNOTES_CONFIG_DIR gets auto-seeded with a default config.toml that races the renderer's first read: localStorage vimMode: false can lose to the file's vim-on default. Write [vim]\nenabled = false into the config dir up front.

  • The table's modal Vim lives in packages/app-core/src/lib/cm-table.ts, not in codemirror-vim: cells are contenteditable fields inside an atomic block widget, so the plugin's keymap never sees them. The register is the one bridge: Vim.getRegisterController() is shared module state, pushText('"', …) writes the same unnamed register object the editor's p reads (registers['"'] === unnamedRegister in the vim dist), and cm-vim-clipboard's pushText patch means table yanks inherit the clipboard mirroring setting for free. One deliberate behavior change: visual-mode y in a cell used to write the system clipboard unconditionally; it now respects the "Sync clipboard with Vim registers" setting like every other yank in the app.

  • The gap dates to 4214c59 ("the note's title travels"), which introduced packages/shared-domain/src/export-title.ts and applied it to apps/desktop/src/renderer/export-window.tsx, note-docx.ts and note-email-html.ts. apps/web/src/export-window.tsx had existed since a4c1d18 (April) and was not touched. The two export windows are near-copies of each other, which is exactly the shape that lets a change land in one and not the other; worth a look whenever an export behaviour is added.

  • Found while investigating PR #715, which proposed running rewriteWikilinkImageEmbeds in both PDF export windows to fix ![[image.png]] embeds rendering as raw text. That bug does not reproduce: both export windows render through Preview, and the markdown pipeline's remarkWikilinks (packages/app-core/src/lib/markdown.ts) turns an image wikilink into an image node before anything downstream sees it, so there is no wikilink syntax left to rewrite. Verified live by loading index.html?exportNote=<path>, the same entry exportNotePdf uses, against a note with ![[image.png]] and ![[subfolder/team photo.png|Team photo]]: both resolved to zen-asset:// and loaded. Applying the rewrite on top is a no-op in most cases and lossy in one, since it consumes the label that remarkWikilinks otherwise keeps as the alt-text fallback (![[image.png]] renders alt="image.png" today, alt="" with the rewrite). The PR was right that the web export window was missing something the other exports had; it named the wrong helper.

  • Verification: npm run typecheck clean, npm run test:run green (3,993 tests). The desktop export path was driven live over CDP. The web change mirrors it at the code level and has not been run through the full web stack.


Local-first and keyboard-first, as always.