docs(v0.6): specify the snippet library and $/> palette split
Reshape v0.6 snippets from a hard-coded table into a git-synced, Zed-compatible JSON library (.typoena.snippets.json) with an inline Tab path and a $ palette launcher. Add the file-format reference and document the Cmd-P/>/$ verb split and the just init catalog.
This commit is contained in:
144
docs/typoena-snippets.md
Normal file
144
docs/typoena-snippets.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# `.typoena.snippets.json` — snippet library
|
||||
|
||||
> The git-tracked file that holds your trigger-driven text expansions for
|
||||
> Markdown authoring. Hand-editable (and Zed-compatible, so you can paste your
|
||||
> existing snippets straight in), synced across devices like your notes. Landed
|
||||
> in **v0.6** (see [`macroplan.md`](macroplan.md)). The editing surfaces — inline
|
||||
> Tab-expansion and the `$` palette — are specified in
|
||||
> [`v0.6-markdown.md`](v0.6-markdown.md).
|
||||
>
|
||||
> **Three files, three concerns, don't confuse them.** `.typoena.snippets.json`
|
||||
> is *content* (your templates). [`.typoena.toml`](typoena-toml.md) is *behaviour*
|
||||
> (auto-save, gutter). `/sd/typoena.conf` is *secrets* (Wi-Fi, PAT), gitignored
|
||||
> and never committed. The first two live in the repo and sync; the third is
|
||||
> per-device.
|
||||
|
||||
## Location
|
||||
|
||||
```
|
||||
/sd/repo/.typoena.snippets.json
|
||||
```
|
||||
|
||||
It sits in the Tracked repo beside [`.typoena.toml`](typoena-toml.md), so it is
|
||||
**committed and pushed** like any note and **syncs to every device** that clones
|
||||
the repo. Your snippet library follows you. It is read **once at boot**; a
|
||||
**missing, empty, or malformed file is fine** — you simply have no snippets, and
|
||||
the editor runs unchanged.
|
||||
|
||||
## Format
|
||||
|
||||
Deliberately **Zed's snippet JSON shape**, so the contents of a Zed
|
||||
`snippets/markdown.json` paste in unmodified:
|
||||
|
||||
```json
|
||||
{
|
||||
"Markdown link": {
|
||||
"prefix": "link",
|
||||
"body": "[$1]($2)$0",
|
||||
"description": "Inline link"
|
||||
},
|
||||
"Book notes": {
|
||||
"prefix": "fiche",
|
||||
"body": ["# $1", "", "## $2 — $3", "", "## What the book is about", ""],
|
||||
"description": "Fiche de lecture"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- The top-level key is the **display name** (what the `$` palette shows).
|
||||
- `prefix` — the word that triggers inline Tab-expansion.
|
||||
- `body` — a **string**, or an **array of lines** joined with `\n` (Zed's form;
|
||||
it sidesteps embedded-newline escaping and reads cleanly for multi-line
|
||||
templates).
|
||||
- `description` — optional but recommended: the `$` palette fuzzy-matches it and
|
||||
shows it, so it is how you find a snippet you don't remember the prefix for.
|
||||
|
||||
### Tab stops
|
||||
|
||||
A body is literal text plus numbered stops:
|
||||
|
||||
- `$1 … $n` — empty stops the caret visits in order.
|
||||
- `$0` — the final resting place (defaults to the end of the insertion if absent).
|
||||
- `${n:label}` — **accepted, but the label is stripped** to a bare `$n`. The
|
||||
editor has no selection/overtype model, so a label would just be text to
|
||||
delete; on a device with no completion popup it could never be shown as a
|
||||
prompt anyway. The **headings and structure carry the template** — the labels
|
||||
were only hints. This is what lets a Zed file with `${1:Titre}` load as-is.
|
||||
- **No dynamic or computed values** (no `date`, no `clipboard`). There is no RTC
|
||||
— the wall clock is valid only after Wi-Fi + SNTP, so a `date` snippet would
|
||||
stamp 1970 on a cold boot. A stop is empty or it is literal; nothing else.
|
||||
|
||||
## The two surfaces
|
||||
|
||||
Every snippet works both ways — there is **no hidden two-tier rule** where some
|
||||
are "inline only" and some are "palette only". Inline Tab is the fast path you
|
||||
reach for once a prefix is in muscle memory; the `$` palette is discovery.
|
||||
|
||||
### Inline Tab-expansion (Insert mode)
|
||||
|
||||
Type a prefix, press **Tab**. If the word immediately before the caret matches a
|
||||
snippet prefix, it expands; otherwise Tab inserts spaces as it does today. (Tab
|
||||
arrives as an ordinary character, so this is a check inside the Insert-mode
|
||||
handler, alongside the existing list-continuation transform.)
|
||||
|
||||
On a **typing pause** — the same throttle as the word-count / cursor refresh, so
|
||||
never a per-keystroke e-ink flash — if the word before the caret is a prefix, the
|
||||
right side panel shows a quiet hint (`↹ fiche de lecture`). The panel is ~17
|
||||
columns, so the hint is the **snippet name / first line**, not the whole body;
|
||||
the full preview is what the `$` palette is for.
|
||||
|
||||
### `$` palette (browse + insert)
|
||||
|
||||
Open the palette (`Cmd-P`) and type **`$`** — the same sigil mechanism as `>` for
|
||||
commands. The query after the `$` fuzzy-matches name, prefix, and description;
|
||||
`Ctrl-N`/`Ctrl-P` move the selection; **Enter inserts the body at the caret** and
|
||||
starts the tab-stop session (dropping you into Insert at `$1`). The empty-palette
|
||||
placeholder legends the sigils: `Go to file · > settings · $ snippets`.
|
||||
|
||||
## The tab-stop session
|
||||
|
||||
Identical whether the snippet was expanded inline or inserted from the palette:
|
||||
|
||||
- After insertion the caret lands on **`$1`** (or the end, if the body has no
|
||||
stops), in **Insert** mode.
|
||||
- **Tab advances** to the next stop, **forward only** (no Shift-Tab). The last
|
||||
Tab lands on `$0` / the end and ends the session.
|
||||
- Pending stop offsets sit **after the caret** and shift with the edits you make
|
||||
at each stop, so typing at `$1` keeps `$2 … $n` correctly placed.
|
||||
- The session **auto-aborts** on Esc, a mode change, or a motion that leaves the
|
||||
stop range — after which the buffer is just text and Tab inserts spaces again.
|
||||
|
||||
## Parsing
|
||||
|
||||
The parse lives in the host-testable `editor` crate (`Snippets::parse`), using
|
||||
`serde_json` — JSON string escapes (`\n`, `\"`, `\uXXXX`) are a foot-gun to
|
||||
hand-roll, and `serde_json` is battle-tested; the editor crate is `std`, so it
|
||||
compiles for xtensa via esp-idf. This is the **one new dependency** the feature
|
||||
adds. The firmware reads the file at boot and hands the parsed list to
|
||||
`Editor::set_snippets`, mirroring how `.typoena.toml` is read and applied via
|
||||
`set_prefs`. A parse error is **non-fatal**: log it and boot with no snippets,
|
||||
rather than refusing to start over a stray comma.
|
||||
|
||||
## Editing it
|
||||
|
||||
- **On your computer (the normal path).** It's plain JSON in your notes repo —
|
||||
edit it in your real editor, copy entries over from Zed, commit, and it reaches
|
||||
the device on the next clone/sync. This is deliberately where the heavy editing
|
||||
happens; the appliance is for writing, not for maintaining a JSON library.
|
||||
- **First-time setup.** [`just init`](../firmware/README.md#provisioning-an-sd-card)
|
||||
seeds this file from a curated catalog — you pick which snippet groups you
|
||||
want and it writes the selected subset into `repo/.typoena.snippets.json`
|
||||
(committed on the device's first `:sync`). See
|
||||
[`v0.6-markdown.md`](v0.6-markdown.md) for the catalog.
|
||||
- **On-device hand-edit — deferred.** The palette hides dotfiles, and `:e` was
|
||||
dropped in v0.6, so there is no in-editor path to this file yet. When one is
|
||||
wanted it returns as a discoverable `> edit snippets` command that opens the
|
||||
file directly, rather than resurrecting a general `:e`.
|
||||
|
||||
## See also
|
||||
|
||||
- [`v0.6-markdown.md`](v0.6-markdown.md) — the editing surfaces, the `$`/`>`
|
||||
palette model, and the setup-recipe snippet catalog.
|
||||
- [`typoena-toml.md`](typoena-toml.md) — the sibling prefs file this is kept
|
||||
separate from, and the `>` command palette snippets share the surface with.
|
||||
- [`macroplan.md`](macroplan.md) — v0.6 scope.
|
||||
@@ -3,36 +3,93 @@
|
||||
> Part of the [Typoena macro plan](macroplan.md). Requirements and targets:
|
||||
> [qfd.md](qfd.md). Load-bearing decisions: [adr.md](adr.md).
|
||||
|
||||
**Status:** render affordances done early; the snippet
|
||||
engine remains (snippets are net-new scope, added 2026-07-08).
|
||||
**Status:** render affordances done early; the snippet engine is the remaining
|
||||
work (snippets are net-new scope, added 2026-07-08; reshaped 2026-07-12 from a
|
||||
hard-coded table into a git-synced, Zed-compatible library with a `$` palette
|
||||
launcher — see [`typoena-snippets.md`](typoena-snippets.md) for the file format).
|
||||
|
||||
- [x] Heading lines bolded in render (faux-bold double-strike)
|
||||
- [x] List continuation on Enter inside `- ` / `1. ` (with empty-item exit)
|
||||
- [x] Soft-wrap at word boundaries
|
||||
- [ ] **Snippets** — trigger-driven text expansion for Markdown authoring
|
||||
(Zed-inspired, but no completion popup: e-ink's ~630 ms refresh rules out
|
||||
a live filtering menu, and it fights the distraction-free premise). Shape,
|
||||
mirroring the existing `list_marker` insert-transform:
|
||||
- [ ] Tab in Insert mode triggers expansion: if the word immediately before
|
||||
the caret matches a snippet prefix, expand it; otherwise insert spaces
|
||||
as today (`expand_snippet(word) -> Option<(body, stops)>`, alongside
|
||||
`list_marker`).
|
||||
- [ ] A snippet body is literal text plus numbered empty tab stops `$1 … $n`
|
||||
and a final `$0`. There is no placeholder text (`${1:label}`) — the
|
||||
editor has no selection/overtype model, so a placeholder would just be
|
||||
text to delete. There are no dynamic or computed values either (e.g. no
|
||||
`date` — there's no RTC; the wall clock is valid only after Wi-Fi+SNTP,
|
||||
so it'd stamp 1970 on a cold boot).
|
||||
- [ ] After expansion the caret lands on `$1`; Tab advances to the next stop,
|
||||
forward only (no Shift-Tab). Stored stop offsets shift with edits at the
|
||||
caret (all pending stops are always after it). The session auto-aborts
|
||||
on Esc, a mode change, or a motion that leaves the stops.
|
||||
- [ ] On a typing pause (same throttle as the insert cursor / word-count
|
||||
refresh — the panel never repaints per keystroke), if the word before
|
||||
the caret is a snippet prefix, the side panel shows the hint (the target
|
||||
expansion). Quiet while typing; the hint appears on pause.
|
||||
- [ ] The snippet table is hard-coded in the binary to start; a git-syncable
|
||||
file on SD (`/sd/repo/.snippets`) is a later option, deferred while SD
|
||||
is still blocked.
|
||||
- [ ] Starter set: link `[$1]($2)$0`, image `$0`, fenced code block,
|
||||
etc.
|
||||
## Snippets
|
||||
|
||||
Trigger-driven text expansion for Markdown authoring (Zed-inspired, but **no
|
||||
completion popup**: e-ink's ~630 ms refresh rules out a live filtering menu, and
|
||||
it fights the distraction-free premise). The library is a git-synced,
|
||||
Zed-compatible JSON file — full file-format reference in
|
||||
[`typoena-snippets.md`](typoena-snippets.md). This section is the *editor*
|
||||
behaviour.
|
||||
|
||||
- [x] **The tab-stop engine** — the shared core both surfaces drive. A body is
|
||||
literal text plus numbered stops `$1 … $n` and a final `$0`; on insertion
|
||||
the caret lands on `$1` (or the end), in Insert. Tab advances to the next
|
||||
stop, **forward only** (no Shift-Tab); pending stops sit after the caret
|
||||
and shift with edits there. The session auto-aborts on Esc, a mode change,
|
||||
or a motion that leaves the stops. `${n:label}` parses to a bare `$n` (the
|
||||
label is stripped — no selection model to fill); no dynamic values (no RTC,
|
||||
so no `date`).
|
||||
- [x] **Inline Tab-expansion (Insert mode).** If the word immediately before the
|
||||
caret matches a snippet prefix, Tab expands it and starts the tab-stop
|
||||
session; otherwise Tab inserts spaces as today. A check in the Insert
|
||||
handler alongside the existing `list_marker` transform
|
||||
(`expand_snippet(word) -> Option<Snippet>`). Tab already arrives as
|
||||
`Key::Char('\t')`, so no new key event.
|
||||
- [ ] **Hint-on-pause.** On the typing pause (same throttle as the word-count /
|
||||
cursor refresh — never a per-keystroke repaint), if the word before the
|
||||
caret is a prefix, the right side panel shows a quiet hint. The panel is
|
||||
~17 cols, so the hint is the snippet **name / first line**, not the whole
|
||||
body — the full preview is the `$` palette's job.
|
||||
- [x] **`$` palette (browse + insert).** `Cmd-P` then `$` switches the palette to
|
||||
the snippet list (the same sigil mechanism as `>`). Fuzzy-matches name /
|
||||
prefix / description; Enter inserts the body at the caret and starts the
|
||||
tab-stop session. Lists **all** snippets — the fuzzy filter handles clutter,
|
||||
so there's no hidden "inline-only vs palette-only" split. Rows read
|
||||
`Name [prefix]`, so browsing also teaches the inline trigger.
|
||||
- [ ] **Boot wiring.** The host reads `/sd/repo/.typoena.snippets.json` at boot
|
||||
and calls `Editor::set_snippets` (mirroring `set_prefs`); a missing or
|
||||
malformed file is non-fatal (no snippets, editor runs). Parse lives in the
|
||||
host-testable `editor` crate via `serde_json` — the one new dependency.
|
||||
|
||||
## The palette, generalised (`Cmd-P` · `>` · `$`)
|
||||
|
||||
v0.5 shipped `Cmd-P` = files and `>` = a five-entry settings list (`save_on_idle`,
|
||||
`format_on_save`, `line_numbers` toggles + `theme`/`auto_sync` rotations). v0.6
|
||||
makes the sigils a clean split by verb, and the empty-palette placeholder legends:
|
||||
`Go to file · > settings · $ snippets`.
|
||||
|
||||
- **bare `Cmd-P`** → *navigate*: go to file (unchanged).
|
||||
- **`>`** → *act on the editor* — the command palette. The pref toggles are just
|
||||
its stateful entries, not a special section. Dispatch differs per entry: a
|
||||
**toggle** flips and the list **stays open** (as today); a **one-shot** (e.g.
|
||||
`format`, `publish`) runs and **closes**; a **parameterised** command morphs
|
||||
the palette into a second **input step** (select `New file` → the box becomes a
|
||||
filename prompt → Enter creates it, scope read from a `repo/`/`local/` prefix as
|
||||
`:enew` does today). This retires `:e` (bare `Cmd-P` covers file-opening;
|
||||
dotfiles get a dedicated `> edit …` command if/when wanted).
|
||||
- **`$`** → *insert content* — the snippet launcher above.
|
||||
|
||||
`format` / `publish` as `>` entries are a small opportunistic add (they prove `>`
|
||||
is a real action registry, not a settings box); the headline is snippets.
|
||||
|
||||
## First-time setup — snippet catalog (`just init`)
|
||||
|
||||
[`just init`](../firmware/README.md#provisioning-an-sd-card) gains a step that
|
||||
seeds the two git-tracked config files into `repo/` (so they commit + sync on the
|
||||
device's first `:sync`): a starter [`.typoena.toml`](typoena-toml.md) (the four
|
||||
keys at their defaults, or confirmed at a prompt) and a
|
||||
[`.typoena.snippets.json`](typoena-snippets.md) chosen from a **curated catalog**
|
||||
checked into the repo. The catalog is grouped and opt-in — you pick the groups
|
||||
you want rather than getting all of one writer's personal templates. Not every
|
||||
Zed snippet is worth proposing: the Slidev/blog-pipeline ones (`@[youtube]`,
|
||||
`<v-clicks>`, frontmatter, mermaid) and render-dependent or hyper-specific ones
|
||||
are left out of the menu, since a distraction-free prose appliance can't render
|
||||
them and you'd never miss them. **Proposed groups (pending sign-off):**
|
||||
|
||||
- [ ] **Symbols** (inline, keyboard can't type them): `fleche`→`→`,
|
||||
`different`→`≠`, `fois`→`×`, `median`→`·`, `degre`→`°`, `euro`→`€`,
|
||||
`edanso`→`œ` (dead keys in v0.2.5 cover accents, *not* these).
|
||||
- [ ] **Structure**: `todo`→`- [ ] `, `link`→`[$1]($2)$0`, `img`→`$0`
|
||||
(net-new — obvious for Markdown), `table`, `code` (fenced block).
|
||||
- [ ] **Prose / PKM templates** (`${n:label}` stripped to `$n`): `fiche`
|
||||
(book notes), `reference`/`refangl` (reference block), `biais`,
|
||||
`capture`, `standard`, `5w1h`.
|
||||
|
||||
Reference in New Issue
Block a user