Skip to content

Helix keybindings - #13

Draft
enkodr wants to merge 20 commits into
ZenNotes:mainfrom
enkodr:helix
Draft

enkodr wants to merge 20 commits into
ZenNotes:mainfrom
enkodr:helix

Conversation

@enkodr

@enkodr enkodr commented Oct 20, 2026 •

Copy link
Copy Markdown

I've been using this branch with helix bindings that I feel other people might benefit from it.

The vim engine leaves gn/gp with the engine (a no-op) instead of
switching tabs; hints only advertise them under helix motions.
@enkodr enkodr changed the title Helix Helix keybindings Oct 20, 2026
Document every [helix] key with a sample block, point [editor] motions
at the engine choice, and refresh the config table ([helix] replaces
the legacy [vim] row, which is still honored).
motions moves under [vim] next to the keys it governs, the settings
view edits [vim] again, and the starter config from zn config shows
the vim-shaped keys. A [helix] section from earlier helix-branch
builds is still honored when [vim] is absent.
startInsert now collapses the anchor, and VisualRange never reports a
selection in insert mode: the anchor stayed where insert began while
the cursor advanced with every typed character, so the host drew a
growing highlight around normal typing.
@enkodr
enkodr marked this pull request as draft October 21, 2026 11:14
enkodr and others added 13 commits October 21, 2026 13:08
… screen

Glamour styles each word, so a token like [[double brackets]] reaches
styleInline with escape codes inside it. The link style renders rune by
rune, splitting those codes into visible text; the row grew wider than
the pane, the terminal wrapped it and the whole display scrolled. Strip
the codes from the token before styling it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With soft wrap on and vim.wrapped_line_motions = "display" (the default),
a bare `$` stops at the end of the row the cursor is on, as the desktop
editor does (#536 there). That part is deliberate. But pressing `$` again
on that row found the same row and stayed put, so the true end of a long
line was out of reach without `A<Esc>` or switching to logical motions.
Reported on Discord by Scotty, who expected nvim's `$`, or at least a
second press that gets there.

Now a bare `$` that starts on the last character of a row that is not the
line's last goes on to the end of the logical line. The first press still
ends the visible row; `g$` stays the row motion and never runs past it,
and operators keep the row too, so `d$` still deletes only what the user
can see. A counted `$` and the "logical" setting behave as before.

Engine tests cover the first, second and third press, `g$`, `d$` and
`v$$`. The README's wrapped-line sentence and the manual's motion list say
how to reach the end. Live in zn tui on a 168-column line: `$` landed on
3:88, `$` again on 3:168, and `g$` on 3:88.
With auto pairs on, the editor gave every backtick a closing partner. The
first backtick paired, the second stepped over its partner, and the third
paired again, so ``` became ```` with the cursor before the fourth. Enter
then split that line into ``` and a stray backtick, and the fence snippet
that closes a code block on Enter never got its chance. Reported on
Discord by Scotty.

A backtick typed right after another backtick now grows the run instead
of opening a pair, so ``` (and ```go) stays as typed, and Enter adds the
closing fence below with the cursor inside the block, as in the desktop
editor. A single backtick still pairs, so `ls` types as before.

The desktop never pairs backticks; the terminal keeps its inline pairing
and only stops pairing inside a run.

Tests: engine cases for ```, ```go and an inline span, and a TUI test that
types ```go and Enter through the App's key path. Both failed before this
change (```go` and ````). Live in zn tui: ```go then Enter opened the
block with ``` below it, and `ls` still paired.
A database record is a note links can reach only once it has a page,
and `zn base add --page` made one only for a new row; an existing record
got its page when someone opened it, one at a time. A database converted
from a CSV had none, so none of its records could be linked.

`zn base pages <base>` gives every record that has no page its record
page, the desktop's "Create pages for all records" (ZenNotes 2.74.0):
the row's properties as frontmatter and its title as the heading, as
`zn base add --page` writes them. Doc.PlanRecordPages follows the app's
rules (planRecordPages in its shared-domain database-records.ts): a
record whose page is mapped and still there keeps it, one whose page was
deleted gets a new one, and one with an empty title is left out.
Ops.RecordPageExists returns a read error as an error rather than as a
missing page, so a failing server read stops the run instead of minting
a second page for a record that has one. The schema is written once,
and kept when a page fails partway, so the pages already written stay
linked to their rows. `--dry-run` lists what it would create, `--json`
reports the pages, and a loose .csv is refused with a pointer to
`zn base convert`. The help row also registers the command and its flags
for parsing and completion.

On a vault with Books.base holding only its data.csv: --dry-run listed
Dune, Kindred and Emma and wrote nothing; the run created the three
pages and mapped them in schema.json, relative to the folder as the app
keeps them; a second run found all three. The desktop app opened that
vault with links to the records live and counted every page as there.

Tests: the planner, a read error stopping it, the command's pages and
schema, a second run writing nothing, --dry-run, and a loose .csv.

go vet and go test ./... clean.
…t copied

A Discord user's workaround script wrote a page file per record into the
database folder without mapping it in schema.json. `zn base pages` took
those records for pageless and wrote `Dune 2.md` beside the file, and
opening a record in the TUI did the same, as it always had for an
unmapped file.

A record with no mapped page is now linked to a page file already there
under its name, in the database folder or its `pages/` subfolder, under
each name a page gets: the app's note-title rules, the slash-only rule
zn and the server path use, and the title as it is.
Doc.RecordPageCandidates gives the same names as the app's
recordPageCandidates (a test pins them to the app's case), and
Doc.RecordPageToAdopt picks the first that exists and no row maps.
PlanRecordPages lists those records under Adopt, `zn base pages` maps
them without writing and reports them as `linked` (and under --dry-run),
and the TUI's open links the file before it would create a page. A file
another row maps, or an earlier record with the same title took, is
never taken twice.

Tests: the candidate names, files in the folder and in pages/ linked,
one mapped by another row left alone, twins sharing no file, the command
linking a script's page as it is, and the TUI open linking instead of
writing `Dune 2.md` (which fails without this change).

go vet and go test ./... clean.
The rebase left TestTypedBacktickFenceClosesOnEnter using vim.R, vim.KeyEnter
and vim.Pos while the file only imported the helix package, so go test
failed to build the tui tests.
The vim engine aliases Pos, Options, Register, Hooks, Key and the key
constructors out of this package, and its alias comments say so. The
helix definitions now carry the matching note from their side.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants