| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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'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.
|