| Commit message (Collapse) | Author | Age |
| |\
| |
| |
| |
| |
| | |
Merge: audit top-five cuts + tag sweeps + Config/DumpConfig (on the audit bug fixes C9, C2).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
workspace tag reads Dump Themes Config Debug Exit
Config (SPC f c) opens init, found as the settings report finds it
(opts.startup_config_path: $XDG_CONFIG_HOME/pardes/init or
~/.config/pardes/init, and the macOS and Windows paths config.md gives). A
pane that already holds it is gone to instead; a file on disk is looked at;
with none, an empty pane is named for it, and Save makes its directory and
any above it first (File.make_dirs, fs.makeDirs) before writing. Without a
configuration directory it says so and opens nothing.
The report Config used to open, the startup path and every live setting, is
DumpConfig, in a +DumpConfig pane. No alias keeps the old meaning.
The workspace tag's default words put Themes where NextColor was, NextColor
staying a builtin, and Config beside it. A tag the user edited is kept as it
was: a dump stores only an edited one (topbar_custom), and a restore puts
back only that.
theme.snap types NextColor onto the workspace tag to click it, and tagnav's
walk back to Dump takes five long-word steps. The rest of the re-recorded
goldens differ in the workspace words and their widths only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |/
|
|
|
|
|
|
| |
other language does: the in-process ZLS backend goes
lsp_zls.zig (1,522 lines), the zls dependency, build.zig's zls wiring and the import graph it baked into the options only for it, and main.zig's reference go. lsp_client's table gains a zls row (PARDES_LSP_ZIG overrides it); Hover, Rename, Diagnostics, WsSymbols, Lspinfo and gd answer through it (checked with lspprobe and the snapshots against zls 0.16.1-dev). What changes for a user: zls must be on PATH; a completion's rows are zls's candidates at the cursor, where the linked analyser gave each one's declaration; document symbols carry no signature. The Zig snapshots (lsp, lspcomplete, lspcompletemoved, lspdebug, lsprelpath) run the real zls now: lspdebug reads the client's explain and report, lsprelpath spells gr's rows where it spelled a completion's, and their goldens are recorded again (stable over three runs). Lspinfo still opens with its backend line. The ReleaseFast tty binary is 105.35 MB to 96.28 MB, the Debug one 296.5 MB to 245.9 MB. docs/lsp.md, web.md, design.typ and the README say so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Kill quit the editor, which in acme is Exit; acme's Kill stops the
commands it started. Exit now quits as acme's does (exec.c, rowclean):
it refuses once, naming each pane with unsaved text, and quits when
asked again with nothing edited since (a small scratch is not asked
about). Kill, bare or with names, stops the commands pardes typed into a
terminal (an exec, a middle click, a pty/run) while their shell's marks
say they run, by SIGTERM to the terminal's foreground job, never to the
shell (acme posts the kill note, which terminates). Both are session
builtins; the topbar's Kill becomes Exit, same width, and every golden's
topbar row changed by exactly that word (checked line by line); the
builtins script scrolls one more row for the index's new line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A message, a leader chord and a prompt used to share one row of body text at
the bottom of a pane, wearing the tagline font and nothing else about a
tagline. Now each one is a TagLayer of its own, emitted through the same
renderHeaderLayer the pane and column tags go through, so it gets the tagline
height, the small-font metrics, the band offset and the border for free --
none of which a body-grid row can have by changing its font role. The text is
right aligned. The prompt stays on the canonical grid because it owns a
cursor, and a cursor has to sit on a real cell.
The body starts BELOW the bands rather than under them, the way tree-sitter
context rows already worked. Pane.body_offset is how many rows they took and
Pardes.bodyTop(pane, rect) is the one place that answers "where does the body
begin" -- replacing fifteen copies of `if (tag_bottom) r.y else r.y + BOX_H`
spread across the paint, hit-test, scroll, PDF and image paths, which is what
let the bands and the text under them come adrift. Every notice is painted on
the grid as well, because the grid is what a terminal client draws and a band
it cannot see is a message it never gets; the GUI skips grid cells a tag layer
covers, so nothing is drawn twice.
Three bugs the bands exposed, fixed here:
- a prompt band flush with the right edge put its caret one column past the
pane, which the detached wire refuses -- so every frame was dropped for as
long as the prompt was open. The band now reserves that column.
- a click on a band mapped to Sel row 0, which is the TAG row: clicking
chrome expanded a word out of the tagline and ran it as a builtin.
- a watched file reloading under the editor changed the core without going
through update, so needs_frame was never set and the reload was never
drawn. Pardes.invalidate() is the name for that, and the file and theme
reloads call it.
A session can now drive its own 9P namespace instead of being refused one:
ownMountSuffix answers what a path names inside this editors own tree and
resolve, readLimit and write serve it from memory rather than making the
syscall that never returns. The match is anchored to whole components under
the registrys 9p/pardes/<name>, because a bare /pardes/<name> anywhere in a
string would claim ~/src/pardes/<name>/README -- and, before write learned the
same trick, write the trees bytes over the real file. readFileLimit and
writeFile refuse instead, having no core to answer from.
A toggle setting SETS when given `on` or `off` and only flips when it is bare,
so the report LocationsConfig prints can be fed back as configuration and mean
what it says.
Snapshots: 97/98, from 0/98. The goldens were several commits stale and 17
scripts had stopped running; `config <line>` is a new script command that
appends to the per-script startup config, so a script that clicks body
coordinates pins `Verbose off` instead of counting the rows an announcement
moves. nested-optout is left failing on purpose: two levels of nesting prepend
vaxis F3 codepoints to typed lines, which is a real bug and is written down in
docs/divergences.md with a repro.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
| |
|
|
| |
Empty marker on the last pre-Reload change. Keep the Reload experiment on reload (3801914), its first change on reload-start (200a1fc), and the unfinished performance investigation on reload-perf-wip.
|
| |
|
|
|
|
| |
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples.
Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The seam grows a second backend: src/lsp/lsp_client.zig speaks JSON-RPC to
child language servers — rust-analyzer, clangd, gopls, tsserver, pyright are
rows in a spec table — while the in-process ZLS analyser keeps .zig. One
reader thread per server owns the socket, routes responses to a mailbox
under the conn mutex (monotonic condvar), answers server-to-client requests,
feeds the diagnostics store, and narrates $/progress and state changes
through a status sink both native shells post to the transient message row:
"rust-analyzer: cargo check 88% 955/1083" lands where a save narrates, with
the same clock. Chatty progress is throttled and deduplicated; settled
states always land, which is also what makes the goldens deterministic.
Nothing wedges and nothing healthy dies: waits are deadline-bounded, a
timeout cancels and returns no rows, three consecutive timeouts restart the
server ONLY while it is idle (an indexing server is narrating its own
excuse), spawn and handshake failures back off 10s to 2min, a crash shortly
after ready counts as a failure, and only a missing binary disables a spec.
PARDES_LSP_{RS,C,GO,TS,PY} override binaries; empty disables; the snapshot
harness pins RS to test/lspmock.zig and empties the rest.
Mutating answers really mutate now: the @put record beside rename @edit
carries per-range text, so = applies the formatter (both backends) and a
same-file WorkspaceEdit rename applies atomically, one undo step, narrated
("renamed 2 range(s)"); a multi-file rename previews as rows instead of
half-applying. Malformed responses fail closed: coordinates validated not
clamped, one bad TextEdit poisons the whole edit set, poison frames kill
the connection instead of buffering forever, decoded control bytes reject a
uri, hierarchy items too deep to reserialize are skipped.
Four kinds helix does not have, on SPC l: c/C incoming/outgoing calls (rows
are call sites), t/T super/subtypes. Pull diagnostics (3.17) preferred when
advertised. Help gains a language-keys footer for the motions no builtin
row could carry; lsp.rel and look.grep now share one path-shortening rule.
zig build lspprobe drives the seam from the CLI (comma-separated kinds share
one server); measured against a 1083-crate workspace warm: gd 26ms, gr 213
rows 165ms, incoming calls 212 sites 197ms, document symbols 670 rows 347ms.
docs/lsp.md tells the whole story; lsp-evaluation.md gets an addendum.
|
| |
|
|
| |
stops rewriting the suite
|
| |
|
|
| |
takes a path argument
|
| |
|
|
| |
optional methods
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Esc ran Toggleterm, which hopped between the newest DOC and the newest
TERMINAL. That distinction never earned its keep. It made Esc unpredictable —
which of three panes you landed on depended on their kinds, not on where you
had been — and it could not alternate between two files at all, which is the
case you hit most. Editing two files, Esc did nothing.
The replacement already existed. Last (SPC j j) is "the pane you were in
before this one, whichever it was": it walks the jump stack for the newest
entry naming a different pane and restores its line and column. So Esc, and
Shift-Esc in tty, now run Last, and Toggleterm is deleted rather than renamed
— a third implementation of "go to the other pane" was the thing to avoid.
SPC w t goes with it; the w group is the four directional moves, and the jump
group already had SPC j j.
Held down, Esc alternates. Two files, a file and its shell, a file and a
+Search — all the same, because Last has no notion of kind to get wrong.
This depends on the swap in the same series: Last reads the stack backwards,
and until hopping stopped appending, the pane you came from could fall off it.
windownav.snap needed only its keys and prose changed — its golden did not
move at all, which is the useful evidence here: for the one scenario the old
builtin handled well, Last produces an identical focus sequence.
Coverage for what it did not handle is new: a unit test opens a second FILE by
looking its name and asserts Esc alternates between two panes of the SAME
kind, which is the case that used to be a no-op.
Docs follow: tutor.txt, docs/helix-keys.md, docs/design.typ, and the builtin
index goldens, which are now one row shorter.
75/75 snapshots, both unit suites, and the macOS ABI build all pass.
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
| |
config.tag_normal became a space when the mode moved into the layout box,
and 66 goldens still spelled the old bullet — the whole suite was red on
one character. Regenerated serially, and every file's diff is that
character and nothing else: replacing the bullets in the OLD goldens with
spaces is byte-identical to the new ones, so nothing else moved under
cover of the churn. The one bullet left in tutor.golden is the tutor's own
sentence about the badge, which the app still draws.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
file:LINE:COL-ENDLINE:ENDCOL, with the two short forms people actually type
reading naturally: file:412:9-21 on one line, file:412-418 whole ones. Ends are
inclusive. A path feature, not a search feature — a ranged path typed in a tag
or middle-clicked out of a shell's output selects just the same; search is only
its first consumer.
The dash is the fussy part. `-` was already a file char, so a ranged word
survives click expansion whole, but a range needs a number on BOTH sides or
my-file:10, build-2 and 2026-07-30 would stop being paths. Table-driven test in
look.zig for exactly that.
Selecting goes through the cellRange/setPaneRange pair the multi-cursor work
left, and hxOff clamps both ends, so a stale range selects what still exists
rather than crashing or reaching past EOF — pinned with an 8:6-400:9 range in a
nine-line file.
Producers: / search, Grep, and five LSP sites through a new spanRow — goto,
references, rename tokens and both symbol lists were throwing away real
protocol ranges at path:line:col. Left alone deliberately: Find rows are bare
paths with nothing to span, a jump is a spot not a span, and the diagnostic and
format paths only ever have a point, where half a range would be worse than
none.
One knock-on worth knowing: n now leaves an EXPLICIT selection, so a topbar
execute chords it. grep.snap's no-match step was silently becoming
`Grep TARGET`; it runs from the leader path now, which never chords, and the
dedicated chord steps stayed where they were.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The primary cursor stays exactly where it was — cur_row/cur_col plus vsel — and
sels[] holds helix's OTHER ranges. That split is why nothing moved at one
cursor: with nsel == 0 not one line of the existing motion, operator, render or
mouse code takes a different branch, which is what protects 800 differential
cases and 67 goldens.
paneRanges/setPaneRanges are the whole list; setPaneRanges IS helix's
Selection::new (min width 1, sorted, overlaps merged, primary follows its range
through a merge). An ordinary key runs the single-selection handler once per
range, visited last-first so an edit never disturbs a range still waiting, and
each finished pass is remembered as a distance from the END of the text, which
an earlier edit cannot move — helix's change mapping without a change map.
pushUndo fires once per keystroke, yanks accumulate, and a builtin acts from
the primary and stops the replay, which also closes the use-after-free window
if it frees the pane.
s and S reuse the / prompt wholesale rather than growing a second one: the
pattern is typed into the tag tail, and every keystroke re-runs the match from
the selection the prompt opened on, so the preview is live and Esc is just the
empty pattern. mvzr does runtime patterns — a bytecode VM in a fixed-size
struct with no allocator — with 64 ops and 8 char classes per pattern, no
case-insensitive flag (helix's smart case is done by folding a scratch copy),
no captures, no multi-line anchors. The last two are the two waivers.
Ctrl-c is a whole-list key and not a per-cursor replay, because helix decides
comment-vs-uncomment ONCE for the whole selection; replaying it would take that
decision n times. Comment tokens are a table in config.zig keyed on the same
extension syntax.zig picks grammars by.
Found and fixed a pre-existing single-cursor bug on the way: la<bs><esc> left
the cursor one cell before where the append began. helix's restore_cursor can
never walk past the origin; ours backed up unconditionally. hxdiff was green
before AND after — the old one-selection contract could not see it.
hxdiff 360 -> 481 cases, hxparity 440 -> 561, all goldens from real helix; the
harness contract now reports every range and its primary, omitted when there is
one, so 359 of the 360 old goldens are byte-identical. The one that moved is
o-count: helix's 2o really does leave two cursors and could not say so before.
|
|
|
THE SEAM TAKES A WRITER. `lsp.query`'s `out` is a `*std.Io.Writer`, not a
`*std.ArrayList(u8)`. The shell owns the buffer behind it (an
`Io.Writer.Allocating`), so a backend never allocates the result, never frees
it, and cannot get the allocator wrong — the invalid-free class of bug has
nowhere left to live. It also deleted a parameter from five functions: they
only ever took a `gpa` to allocate rows, and 0.16's unused-parameter error
found every one. `lsp.row()` lost its allocator argument too.
Since a Writer cannot rewind or be counted, `query` renders into a scratch
Allocating first: the log wants an exact row count, and `explain` throws the
rows away and prints narration in their place.
INTROSPECTION. `SPC l i` (Lspinfo) and `SPC l w` (Lspwhy), in the `l` group
that now holds every language command (see below).
They exist because of the seam's own contract: a backend never fails loudly,
which is right for an editor, but it makes a broken backend and a correct one
that found nothing look identical from the outside. Every query now leaves a
record — kind, file, offset, duration, row count, and THE ERROR `run` returned,
which `catch {}` swallowed and which was visible nowhere. Lspinfo prints those,
plus which ZLS is compiled in, which zig lib dir and whether it actually opens
(the usual cause of "gd does nothing in std"), and what the backend answers
versus refuses. It answers from ANY pane, including one with no file, because
it is about the backend — which matters precisely when the pane you are sitting
in is the problem; both shells now send status for a file-less pane.
Lspwhy narrates the REAL resolution path. The trace is threaded through `goto`
itself, so what it prints is the position context the analyser returned and the
branch that actually stopped. A debug view that re-derives the logic beside it
is one that can disagree with it.
CTRL-CLICK IS gd. Mouse gained a `ctrl` field, set by both shells (SDL asked
directly via GetModState rather than read off key-event bookkeeping, which a
click with no prior keypress would miss). The flag rides the drag rather than
firing on the press: a click does not place the modal cursor until RELEASE, so
a query asked at press time would answer about wherever the cursor previously
sat. A ctrl-DRAG still selects.
The snapshot DSL gained a `ctrl-` button prefix (SGR bit 4, what a terminal
sends and what vaxis decodes). test/snapshots/lspdebug.snap covers all three,
including a PLAIN click in the same spot that must NOT jump — without it the
test would pass on a bug that made every click a goto. Durations cannot live in
a golden, so PARDES_LSP_NOTIME (set by the harness, like PARDES_DUMP) omits
them.
58 snapshot scripts, hxdiff 360, hxparity 440, unit 46, gui build: all green.
lspbench: 17/17, 0 false claims.
THE WHOLE LANGUAGE GROUP LIVES UNDER SPC l.
pardes keeps its own leader letters back. `SPC d` is Del again, `SPC k` is
Kill, `SPC s d`/`SPC s r` are Dump/Restore and `SPC h t` is Tutor — exactly
where they were before the language work touched them.
The previous pass put the LSP commands on helix's bare `<space>` letters and
moved pardes's builtins out of the way (Kill k->q, Del d->wc, Dump/Restore
s?->f?, Tutor ht->T). That was the wrong trade. Those five are the most-pressed
keys in the editor and predate the language work; an LSP command is something
you reach for deliberately and can afford one keystroke more.
So every LSP command keeps HELIX'S OWN LETTER and gains the `l` prefix:
`<space>k` -> `SPC l k` (hover), `<space>d` -> `SPC l d` (diagnostics),
r/a/h/s/S/D likewise. Nothing to re-learn but the prefix, and `Lspinfo`/
`Lspwhy` were already there.
THE GOTOS ARE UNTOUCHED. `gd` `gD` `gy` `gi` `gr`, `]d`/`[d`, `]D`/`[D`, `=`
and ctrl-click all stay exactly as helix has them — they never collided with
anything, so there was never a reason to move them, and they are the ones you
actually press mid-edit.
leader.snap is restored to the pre-LSP script (its `key q` unmapped-key step
works again now that Kill is back on `k`) plus one new step for `SPC l ?`. Its
`SPC ?` root listing had to stop waiting on Restore: the full list grew to 33
rows and row 21 falls off the pane, so it watches an early row instead.
DEPENDENCY IMPORTS NOW RESOLVE. `gd` on `@import("vaxis")` opens vaxis's root
file; before, it silently did nothing while `std` worked perfectly.
The asymmetry was not a wiring mistake. ZLS's uriFromImportStr answers exactly
three ways: a relative `.zig`/`.zon` path from disk, `std` from `zig_lib_dir`
(one directory, which we supply), and EVERY OTHER NAME only by running
`zig build --build-runner` to discover the module graph. That last branch needs
`zig_exe_path`, which this backend sets to null on purpose — so every
dependency import returned `.none`. Confirmed twice over: in ZLS's source, and
by `SPC l w` on the import string, which printed the STOP line naming exactly
that branch. (The introspection builtin diagnosing its own backend on its first
real outing is a decent argument for having built it.)
We never needed a compiler for this: build.zig IS the module graph. It folds
`root_mod.import_table` into a name -> root-source-file table at configure
time and passes it as a build option; the backend consults it precisely where
ZLS gave up. Correct by construction — a dependency added or renamed in
build.zig cannot forget to update it — and it costs no subprocess, no build
step and no runtime work. `SPC l i` now lists the table, since "is this name
even importable" is the first question when a jump does nothing.
Two limits, both stated in the code: a module whose root is a GENERATED file
is skipped (it has no path until make() runs), and a file inside a dependency
importing that dependency's OWN internal module name is still a miss — that
would mean running its build.zig.
TRAP: the table is folded out of root_mod.import_table, so `addOptions` had to
move BELOW every `addImport` call. Attached where it was, the table is empty.
TOPBAR GAINS `Help`, WHICH IS WHY `SPC ?` LOOKED BROKEN.
A bare `pardes` boots straight into tty mode (main.zig: `args.len == 1`), where
every printable key belongs to the shell — so SPC never reaches the leader, and
`SPC ?`, the one thing that would tell you the leader exists, is exactly the
thing you cannot press. Ctrl-b first and it all works; nothing was broken. But
"the help is unreachable until you already know the escape hatch" is a bad
answer, and there was no mouse route either: Help was the one builtin missing
from the bar.
Row 0 is not a pane, so a middle-click there is dispatched before any pane's
mode is consulted — the word works in tty mode, which is the only reason it
earns the width. APPENDED, not inserted, so every existing topbar word keeps
its column and no golden's click coordinates move. test/snapshots/ttyhelp.snap
pins it from a bare boot: click Help, get the list, shell still TTY at its
prompt, then Ctrl-b + SPC ? for the keyboard route.
All 58 goldens carry row 0, so all 58 moved. Verified mechanically that the
only changes are the row-0 text and the row-0 style run (0-47 -> 0-52), plus:
dump/restore record the topbar inside their .zon, and tagnav's `$`+Enter now
executes `Help` rather than `Grep` because the bar's last word changed — still
exactly what that step's comment claims it tests.
THE DEPENDENCY FIX HAS A CEILING, NOW STATED. The module map is consulted from
OUR goto handler, not from inside ZLS, so the analyser still cannot type the
`vaxis` const: `gd` on `@import("vaxis")` opens the file, `gd` on `vaxis.init`
finds nothing. That is now spelled out at the top of lsp_zls.zig and on
moduleRoot rather than left implied, and `SPC l w` detects the case by name —
if the left side of a failed field access is a known dependency it says so,
instead of the generic "could not resolve". Lifting it means giving ZLS a real
BuildConfig, either by letting it run the build runner (a subprocess, and with
no cross-query cache that is once per keypress) or by synthesizing one into
BuildFile.impl. Both are real work and neither is smuggled in.
Also fixed while there: the field-access miss was only explained when ZLS
returned null, but it returns an EMPTY SLICE when it typed the left side and
found no such member. Both are "gd did nothing" from the outside; both are
explained now.
|