| Commit message (Collapse) | Author | Age |
| ... | |
| | |
|
| | |
|
| |
|
|
| |
search/range look steps, goldens updated
|
| |
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
It never did — the effect arm was `.watch => {}` with a comment saying a shell
that never delivers the event simply never reloads. That was written as a
deliberate scope cut and it is the whole bug: the user runs pardes-gui.
The tty watcher was fine, and I proved all four suspicions false against a real
binary in a real pty with the write done by a stranger process and NO input
delivered afterwards: the loop does wake (postEvent signals an empty queue),
zig fmt's rename-over does fire MOVED_TO and is caught, three panes across two
directories all reload and closing one leaves its neighbour still following,
and the self-write hash guard does not swallow a real change. The live session
even had its inotify mark on src with the right mask and re-read config.zig
when I touched that directory.
Duplicated rather than shared with tty.zig, the way the two shells already each
own LspJob, forkShell and their pty readers. The headless PARDES_TEST_GRID path
deliberately passes -1: its contract is one frame per scripted input, and a
reload on its own clock would put an unasked-for frame in the stream.
filewatch.snap proved less than it looked. Its real blind spots were the
rename-over shape — the old script only truncated, so it saw CLOSE_WRITE and
never MOVED_TO — multiple directories, and the one that mattered: the suite
only ever runs the TTY binary, so it structurally cannot see a gui-only
regression. It now uses a new `run` directive whose writer is a child of the
RUNNER, so nothing it does reaches an app pty.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
228 in the ring: 3 pardes ships plus all 214 helix runtime themes and all 11
zed variants across its three families. No exclusions.
inherits is what this cost. 76 themes need it, so the reader is two passes now,
and the merge is WHOLE-KEY rather than field-wise because helix merges the
theme body at depth 1 — the three *_transparent themes clear their parent's
background with an empty table, and a field-wise merge would leave it painted.
I had that wrong until I read helix-view/src/theme.rs.
At 214 files the generator meets themes that are SPARSE rather than broken, and
the old fatal-on-anything-missing would have rejected them. Every fallback is
helix's own rule: no background at all means wear the terminal's, so bg and fg
go null (13 themes, correctly); the twelve RGB fields that cannot be null fall
back to ghostty's default palette entries 7 and 0, which is exactly what a
pardes terminal already paints an unstyled ANSI index as. fatal is kept for
what genuinely cannot be read.
Three real bugs surfaced only at scale: #ccc shorthand doubles the nibble
(helix's rule, 58 colours), a quoted inline-table key silently dropped a
selection colour, and four themes name a cursor background EQUAL to the page
because they are about to reverse that cell — taking it painted an invisible
move box. 0 degenerate themes out of 225 now: no fg==bg, no syntax colour on
its own background, no invisible box.
Collisions get an unconditional rule rather than a clever one: a zed variant is
always <name>_zed, because both projects ship gruvbox, ayu and one, and a
conditional tag would move a name when the other source changes. A comptime
assert holds it.
Cost: no-op build 0.257 -> 0.276s, a pardes.zig change 46.8 -> 47.5s, binary
+0.98%. The generator does 217 sources in 54ms.
NextColor stays and is no longer a way to REACH a theme — but the browse got
better, not worse: the generated half sorts by name, so its neighbours are that
theme's own family. theme.golden's fourth click moved from ayu_dark to acid,
which sorting 225 names does; themesel gained a tail capture proving row 228
renders.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An external update is pushed onto the undo stack exactly like an edit the user
typed, so unsaved work is one `u` away and pardes never has to merge anything.
That is the design, not an implementation detail: the whole feature is
pushUndo() then setContent().
One inotify instance in the tty shell, blocking in readVec through std.Io on a
concurrent task started beside the pty readers — after loop.start(), so the
forkpty ordering is untouched. It watches the containing DIRECTORY, because an
editor rewrites by rename-over and a watch on the file would follow the dead
inode, and it listens for CLOSE_WRITE rather than MODIFY, which is one event per
finished writer and most of the debounce for free.
Our own Save does not reach the undo stack: each watch keeps a hash of the
bytes last seen on disk and save_file restamps it. A hash rather than mtime and
size because the reload has to read the file anyway.
The core stays sans-IO — one watch effect out, one file_changed event in, and a
shell that cannot watch simply never sends the event, which is what the gui and
the web platform do. Linux only; fanotify is what the build system uses and is
rejected in a comment: it exists for thousands of directories across mounts,
and sixteen panes of inotify is a third of the code with no kernel floor.
New golden filewatch: edit without saving, overwrite from a shell in another
column, watch it reload, undo, get the unsaved edit back. None moved.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The pad faithfully turns a two-finger scroll's sideways drift into wheel_left
and wheel_right, so a plain scroll slid the view sideways underneath you. Every
vertical tick now re-arms a guard and every horizontal tick spends one instead
of scrolling, so horizontal has to EARN its way back by landing three ticks in
a row with no vertical among them.
Clock-free on purpose: the core is a state machine with no timestamp on a mouse
event, and faking one by counting renders would be worse than the counter. The
guard is only ever armed BY vertical scrolling, so a horizontal swipe from a
still view still moves on its first tick — only horizontal that interrupts
vertical has anything to prove.
A tilt wheel gets the same treatment, where recent-vertical is a much weaker
signal of accident. Deliberate: the only honest fix is a per-device flag out of
the shell, and that layer costs more than the three clicks it would save.
The rule is one pure function next to the number it reads, with an inline test
written to hold for any tuning of that number. unit-test grew a fourth binary
over the core module hxdiff already links. New golden wheeldrift; none moved.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
placeDoc picked its source by SLOT INDEX — it scanned panes from 0 and took the
first doc it found — so which column your file landed in depended on internal
id allocation, not on where you had been. It now walks the jump stack backwards
to the newest live doc pane.
No new state: jumps already IS the record of where the keyboard has been, and
Pane.serial is already the "this slot was reused" test. The slot-order scan
stays as the fallback, deliberately: a restored dump has one jump entry, and if
that pane is a shell the stack knows no doc — without the fallback the next
file would claim a second column with docs already open.
lookpanes moved: a file now lands under the doc that was actually last read
rather than under whichever doc happened to hold the lowest pane id. New
golden doccol pins the case the line exists for, and it fails at exactly one
capture with the walk disabled.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Not a second builtin: Help already rendered "every builtin with a leader path
and the key that runs it", and the ask is "every builtin and every shortcut" —
the same listing with three more rows and one more column. A new builtin would
have been a second renderer over a superset of the same rows, and the two would
drift the first time a column moved. The prefix filter is now the degenerate
case of the index rather than a separate mode: SPC h ? reads the same array
with prefix "h", and the path-less builtins fall out on their own because an
empty path starts with nothing.
The shortcut column is folded, never typed: chordName spells a chord from Key's
OWN declarations, so a new special key names itself and an unnamed
non-printable codepoint is a compile error instead of garbage; shortcuts()
walks the cmd columns of window_keys and jump_keys, look_cmd/exec_cmd with
their keys and buttons, and topbar_str. Retarget a binding in config.zig and
this re-renders.
So Look reads `enter, right-click`, Back reads `C-o`, Kill reads `topbar`, and
Theme reads nothing at all — which is the information.
Not a picker, deliberately: n/N stepping a list holding Kill, Del and Delcol
would end the session. The names are live text, so middle-click the one you
want.
Alt-n, Alt-c, the tty toggle and the cut/paste chords cannot appear — they are
inline handlers that reach no builtin, so there is no row to put them on. Make
one a struct with a cmd column beside its binding and it appears for free.
Three goldens moved, all of them the +Help buffer's contents.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Each theme is a .zig file of pure data and nothing enumerates them by hand —
fold() walks the container's declarations, so a theme is a file and that is the
whole registration. Field by field rather than a wholesale coercion, which
makes a missing field a compile error that names it.
tools/gen_themes.zig reads helix .toml and zed .json out of vendor/themes and
emits one .zig each; build.zig reads that directory, so adding a theme is
dropping a file in. Output goes to the build cache rather than the tree, so zig
owns the freshness check and a deleted source cannot leave a stale theme
behind. Vendored, not read from the genizah: the build stays offline. A source
it cannot map is a hard error naming the file and the key, never a silently
black-on-black theme.
ThemeSel is the clever half. Traits gained an `executes` column, so n/N over
that buffer hands the whole line to Exec instead of the leading word to Look —
both arms the ordinary builtin, so a stepped row does exactly what the matching
mouse button on it would. Stepping the list previews each theme live. The trait
is a pane property, not an output-pane branch, so any pane whose lines are
commands can opt in.
Goldens: leader gains SPC t t; theme's fourth NextColor no longer wraps to
helix because the ring is fifteen long. New: themesel.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One container, not two: focus_hist (a stack of pane ids rebuilt every sync) is
now jumps[] + a current pointer, and the +Jumps buffer is a RENDERING of that
array — nothing copies it, nothing shadows it. prevFocus, Toggleterm, Look's
directory order, Back/Forward, Last and Jumplist all walk the one list.
A pane id is reused, so a location that only remembered an id would retarget
after a respawn: panes now carry a monotonic serial and an entry whose slot
holds a different serial is dead. trackJump compacts those out and fixes the
pointer in the same pass.
The push rule lives in ONE place and says: a location is worth remembering
when you cannot see it any more — a different pane, or more than a bodyful of
rows away in the same one. So hjkl never grows the list and 100G, a search hit
and a goto-definition do.
Ctrl-o/Ctrl-i walk it, SPC j j toggles the last two, SPC j l lists them.
Ctrl-i IS Tab on a legacy host, where the binding simply never fires and
Tab-executes is untouched; kitty reports them apart. SPC j o/i work anywhere.
Two goldens moved, both the SPC ? Help listing gaining four rows.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Their only special feature is now the keys they are assigned. `Look main.zig`
typed in a tag is the same look a right click is; config.look_cmd/exec_cmd
point the two buttons and Enter/Tab at them, so retargeting Enter to Grep is
one line. actOnSelection — a hand-written cascade with the builtin dispatch
nested inside it — is gone, and the `button` parameter it threaded through five
KEYBOARD call sites went with it.
New syntax, spelled once in config.zig: @`ls -la` names a command to run
rather than a file to open. Word expansion takes the quoted run whole, the way
acme does for its own </|/> words, so a click inside one does not hand Look the
fragment `ls`.
And it nests: @`Look .` unwraps, re-enters the dispatcher, and looks at the
directory. Guarded at depth 8, which is unreachable today (every re-entry
strips a word or a delimiter pair, so the string strictly shrinks) and exists
for a future syntax that does not shrink.
leader_path is now optional: a builtin may have no SPC path when its shortcut
is a key and a mouse button. New golden cmdword; the other 59 unmoved.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Retargeting a key, a mouse chord or a spelling is now an edit to src/config.zig
and nothing else. Three parts in reading order: pardes's own bindings (where a
reader lands), the Look/Exec syntax, then the helix keymap under a banner
saying hxdiff/hxparity are differential suites against real helix, so a key
moved there is a divergence and not a tweak.
The bindings are data a comptime loop can walk, because the builtin index is
going to walk them.
Duplication this cut: is/isC/isA became one hit(key, chords) over ~132 call
sites, and a binding being a LIST collapses the letter-or-arrow chains;
swap_enter_tab is gone, replaced by look_key/exec_key that can point anywhere;
the four focus builtins' h/j/k/l lived hardcoded in two places and is now one
table.
No golden moved.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The seam's `query` now calls ZLS's analyser directly, on the worker thread,
in this process. There is no zls binary, no subprocess, no JSON-RPC, no
`initialize` handshake and no `Server` — `gd` is a function call whose answer
comes back as rows. ZLS's build.zig already publishes its guts as an
importable module (`b.modules.put("zls", ...)`), so this is a path dependency
on the local 0.16.x checkout plus one new file, `src/lsp_zls.zig`.
Construction is ZLS's own (tests/analysis_check.zig does exactly this):
InternPool.init -> DiagnosticsCollection -> DocumentStore struct literal ->
Analyser.init. `zig_exe_path` is null on purpose — shelling out to the
compiler is the thing this backend exists to avoid — and `zig_lib_dir` is
baked in from `b.graph.zig_lib_directory`, so `gd` on `std.mem.count` opens
the same mem.zig the compiler used, with ZIG_LIB_DIR overriding at runtime.
Offsets are `.@"utf-8"`, not ZLS's utf-16 default: `+Search` rows are byte
columns and we are not on a wire.
Seventeen probes, seventeen answering, no false claims, 5.9 MiB peak RSS.
The features that were already Server-free are calls (hover, document
symbols, code actions); the ones welded to `*Server` are reimplemented thin
on top of public primitives — goto is gotoHandler minus the protocol,
diagnostics is the in-process `std.zig.AstGen` branch of
getAstCheckDiagnostics, references is symbolReferences' algorithm from the
outside (offer every same-named identifier token back to the analyser and
keep the ones that resolve to the same decl, so a shadowed local is not a
false hit).
What it does not do, deliberately:
- Nothing is cached between queries. Each `query` builds a DocumentStore,
resolves imports and throws it away, because the arena dies on return and
`req.source` is a snapshot of a buffer the user is still typing into. So
cold IS warm — there is no index to warm up. It is also fast enough not to
need one: 124us for a local goto, 2.8ms into the stdlib, 8ms for
references over a 5000-line file. A cross-query cache is a real design
(a global, a mutex, an invalidation story), not a line of code, and it is
the obvious next step rather than something smuggled in here.
- References, rename and select-refs are THIS FILE only. Workspace-wide
means loading every project file into the store and running the analyser
over each; DocumentStore's own workspace iteration has the same limit
(it can only see handles already loaded). Workspace symbols and workspace
diagnostics DO walk the tree, because neither needs the analyser — a
parse and a tree walk each.
- Rename previews, format reports, code actions list. The seam hands back
rows, not edits, so there is no channel through which a backend could
rewrite the buffer. These answer the question the keypress asks and change
nothing.
- Without a zig binary, `@import("builtin")`, `@import("<pkg>")` and
`@cImport` resolve to nothing — silently, which is ZLS's behaviour, not a
bug introduced here. Relative imports and `std` work.
- Non-.zig files answer nothing. The core does not gate the keymap by file
type, so the gate is here: `gd` in a README must find nothing rather than
parse prose as Zig and confidently resolve a word out of it.
A whole-file report that ran and found nothing says so ("no diagnostics",
"already formatted", "no code actions") rather than returning zero rows,
because in this seam zero rows already means "no backend" — `lspResponse`
opens nothing for an empty answer, so silence cannot also mean "checked,
clean". Location queries keep the opposite rule: unresolvable is no rows.
test/snapshots/lsp.snap covers the round trip end to end — gd jumping on a
single result, SPC k opening +Hover, SPC s opening the +Search list that n
steps, and gd on a keyword answering nothing without opening anything. The
whole backend was also fuzzed at 20k queries over real, truncated and
byte-smashed sources across every kind; that found two crashes (a decl's
name token indexes its own file, not the requesting one, and is not
necessarily an identifier at all on a half-typed line) which are fixed.
emscripten does not get the backend: the web shell has no threads and
no-ops the lsp effect, so it keeps the empty one the base tree shipped.
DEPENDENCY: ZLS is FETCHED by the build system (build.zig.zon .url + .hash,
pinned to commit 3e0d0820 on the 0.16.x branch) rather than a path
dependency on the local genizah checkout, so it lands in zig-pkg/ like
every other dependency and the build is reproducible from the .zon alone.
Also passes -Dversion-string: ZLS's build.zig names itself by shelling out
to `git describe`, and a fetched package is an extracted tarball with no
.git, so every build printed a 'Failed to run git describe' warning. We
pin the commit, so we already know the answer.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The base every language backend plugs into. Three parts:
ASYNC. The core had no request/response shape - every effect was
fire-and-forget or instantaneous. A language query is the first thing
that answers later, so: Effect .lsp -> shell worker -> Event .lsp_resp.
tty.zig uses io.concurrent + the vaxis queue, gui.zig a detached thread
+ the mutex queue it already had for ptys; web no-ops it. The worker
never touches the core (path/source/arg are snapshotted into an LspJob),
one query in flight identified by a monotonic id so a second press makes
the first answer stale, and no rows is a legal answer.
KEYMAP. Helix's, verified against its default.rs rather than recalled.
gd/gD/gy/gi/gr and ]d/[d had no conflicts. The SPC letters did, so
pardes's own builtins moved instead of helix's: Kill k->q, Del d->wc
(closing a pane is a window op, and c is helix's own close), Dump/Restore
s?->f?, Tutor ht->T. A three-exception muscle-memory map is not a map.
RESULTS ARE +SEARCH ROWS. path:LINE:COL text, absolute. That is what
look.zig resolves and n/N step, so one row from a goto jumps and several
open a buffer - helix's multi-result picker needed no picker code.
Backends supply exactly one function (lsp.query) plus a supports set and
a name; the base has none on purpose. zig build lspbench scores them on
the same corpus: feature matrix (trusting results, not the supports
flag - a claimed-but-empty kind is reported as a false claim), cold and
warm latency, peak RSS.
Two snapshot scripts moved. leader.snap encoded the old key paths.
chordcut.snap's last two steps clicked column 5, which lands on a FILE
pane, so 'key c-b' toggled nothing and the typed text was being read as
normal-mode keys - the golden recorded no TTY pane and no cat -v output
anywhere. Pointing them at an actual shell makes both steps assert what
their comments claim, and the tty paste chord is now covered for the
first time.
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
| |
selects but never edits
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
asked, never into a column of their own
placeDoc classified any file pane as a doc, so a +Search opened from a lone
shell took layoutInsertColumn(0) - a whole new leftmost column, shoving every
existing column sideways - and an open results list was then eligible as the
split parent for a real file, stacking documents under the list. An output
buffer has no file behind it and no Save; it is the result list belonging to
the pane that asked for it. Now it is that on both sides of the rule: it
splits below from_id whatever kind of pane that is (a source that died falls
back to stackDocLeft, the same last resort a full column bar uses), and it is
skipped when a real doc hunts for its split parent, so a file opened from a
search row joins the docs instead of the results.
Real docs are untouched: fsearch/rsearch/look-file/tutor/lookpanes all pass
unchanged. psearch is the proof - its results snap was two columns (the shell
pushed to x=70, its gutter run at 70-71) and is now one 140-wide column with
the +Search stacked below at the same x origin.
|
| |
|
|
| |
most-recently-focused, search only when all fail
|
| |
|
|
|
|
|
|
| |
Colors lifted from the user's own helix theme (~/.config/helix/themes/pardes.toml,
pinned against a real hx render through a pty). Theme gains box/box_dim so a
restrained theme can turn the accent down; the hardcoded gray.* overlay greys
were the dark theme's own fields spelled twice and are gone. The old default is
still one NextColor away.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The image tag spelled out `Petscii <C64|Term> Ascii` and actOnSelection
matched those words by hand before the builtin dispatch. They are three
real Builtins now (SPC t p/l/a), so the by-word matching is gone, the tag
is a plain `img <path> Del`, and with nothing live left in it the image
early-returns in curTail, enterTagEdit and the tag-click guard go too — an
image tag edits like every other pane's.
Colors and Crt leave the topbar for SPC t c / SPC t r: set-once display
switches should not spend width in a bar you read every frame. NextColor
stays, it is the one you cycle repeatedly. Both remain full builtins.
The snap scripts that clicked those words now press the leader instead, and
images.snap finally exercises the palette and ascii toggles for real: its
old clicks at x=57 landed on the neighbouring shell's tag, so that toggle
had never been covered. The test PPM grew to 512x512 with LCG noise so the
ASCII bitmaps actually win cells and dropping them visibly changes the art.
|
| |
|
|
| |
painting over the UI
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Left/Down/Up/Right run the existing focusDir (now taking the pane the
builtin ran on, so an executed name acts on ITS pane); the Ctrl-w prefix
routes through the same four builtins instead of calling focusDir itself.
Toggleterm hops between the most recently focused document and terminal
off focus_hist, the MRU the core already rebuilds each sync.
Ctrl-w stays: a tty-mode pane never sees SPC, so it is the only keyboard
way out of one. Its snapshot step now moves focus somewhere (c-w l), which
the old c-w h in that layout did not.
|
| |
|
|
| |
Save moves w -> f s to free the w group for window navigation.
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
| |
the next left click instead of stretching to it (explicit v/x selections keep click-extend), and in a tty-mode shell 1-3 now pastes into the pty like a terminal emulator — the click is forwarded first (SGR or X10, only when the app listens for mouse) so mouse-aware programs paste at the click point, then the register arrives bracketed (mode 2004) or with \n->\r when unbracketed; 1-2 stays inert there. chordcut.snap pins both (a cat -v probe shows the exact wire bytes); tutor updated.
|
| |
|
|
| |
(file:LINE works) opens it focused in the left column with a greeting terminal on the launch dir at right, a DIRECTORY arg chdirs so shells spawn there. New argv.snap; boot/restore.snap pin -n 1 to keep the classic boot.
|
| |
|
|
| |
the new pane under 2 body rows, splitParent swaps in a qualifying pane (same column first), else the tallest pane anywhere; applied to Alt-n, look-dir, placeDoc and ttyForDir. New splitparent.snap.
|
| |
|
|
| |
BOX_H (the tag row doubles as the re-enlarge handle; the gutter move box wins over the handle so a tag-only pane stays movable), a hidden body's pty/vt is never resized so re-enlarging restores it intact, and cursor/modal-selection paints never land on the tag row. New shrinktag.snap; tutor updated.
|
| |
|
|
| |
middle-drag pane has no selection of its own (kept left sel, else explicit v/x n/N), the active pane is checked first (it holds the most recently made selection), then slot order. New selarg.snap covers borrowing a kept left selection and a v selection from another pane; tutor updated.
|
| |
|
|
| |
looked word (and its cwd-join) is matched against loaded file/image pane paths first, honoring :NN recentering — so the fs-less web build can focus dump-loaded panes on look, and native dedup no longer needs the file on disk. New lookloaded.snap: look at a path after rm'ing it still focuses/recenters the loaded pane.
|
| |
|
|
| |
blocked in every thread and consumed on a dedicated sigwait thread instead of vaxis's signal handler, which locked std.Io mutexes from signal context and panicked the Io.Threaded machinery; pane shells unblock it after fork so bash/vim still see resizes. winch.snap storms WINCH from a pane (crashed pre-fix) and checks a real resize still reflows.
|
| |
|
|
| |
(both in one hold = Snarf/copy; the chorded left release is inert), plus 2-3 / 3-1 / 3-2 cancel an execute or look drag mid-gesture (the 2-1 argument chord already existed). chordcut.snap covers cut/copy/paste and both cancels; tutor documents the chords.
|
| |
|
|
| |
scrolled view even when the pane isn't active (the old gate only protected the active pane; tag edits use the remembered body mode). New ttyfollow.snap: background job streams into a scrolled-up normal-mode pane without moving it, tty mode follows again.
|