feat(completion): value-based completion after pipes#116
Conversation
Upstream ynqa#115 (chore: use jaq v3.0) rewrites run_jaq for the jaq 3.0 API (data::JustLut, Vars, read::parse_single, unwrap_valr). Verified locally: cargo fmt/clippy/test/build all clean. Prerequisite for the completion work (ynqa#114 thread): jaq 3.0 unlocks the load::lex token-tree segmentation and the yield filter (jaq#426) that the context-aware completion (ynqa#116) follow-up depends on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> # Conflicts: # src/json.rs
Completion matched the entire query against root-relative paths, so anything
past the first segment (`.foo | .ba`, `.items[] | .na`) yielded nothing.
Segment the query at the last top-level `|` (a small scanner that skips
string literals and `()`/`[]`/`{}` nesting and ignores `|=`), then for the
piped case evaluate the base expression with the existing run_jaq and
enumerate its output's paths with get_all_paths, so suggestions are relative
to the piped value. The editor text is rebuilt as preserved_prefix +
suggestion to keep the pre-pipe context. The parsed input is cached so a
piped completion does not re-deserialize the document per keypress. The root
(no-pipe) path is unchanged.
Closes ynqa#114.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLHuDq5UHVEYsb9UfPQBUJ
f5de526 to
5feaa3f
Compare
Replace the hand-rolled byte scanner in `last_top_level_pipe` with jaq's
own tokenizer (`jaq_core::load::lex`). The scanner re-implemented a tiny
jq lexer — tracking string-literal/escape state, `()`/`[]`/`{}` nesting
depth, and `|=` lookahead by hand — which @01mf02 objected to in the ynqa#114
design thread ("where you basically re-lex the jq script manually").
The lexer groups each delimited group into a recursive `Tok::Block` and
string literals/interpolation into `Tok::Str`, so a `|` nested in any of
those is simply not a top-level token; `|=` lexes as a distinct
`Tok::Sym`. "Last top-level pipe" becomes "last `Tok::Sym == "|"` in the
top-level token vec", with the byte offset recovered from the slice
pointer. All the manual bookkeeping is deleted, and segmentation can no
longer disagree with how jq parses the query.
Faithful port: the `analyze() -> CompletionCtx` contract and all existing
tests are unchanged. A lex failure on unbalanced mid-typing input (e.g.
`select(.a` before TAB) degrades to the root context — never worse than
the old scanner, which produced an invalid base that yielded empty
suggestions anyway. Cursor-awareness and in-paren completion remain
explicit follow-ups.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed a follow-up commit: segmentation now uses jaq's own tokenizer (
Faithful port — the |
Completion previously analyzed the whole query, implicitly assuming the cursor sat at the end. Segment only the text left of the cursor and preserve the text to its right, so TAB at `.items[] | .na‸me` completes `.na` to `.name` and keeps `me` after the cursor, landing the cursor right after the inserted suggestion. - completion_ctx: add `segment_at_cursor(query, cursor)`, a pure wrapper that slices at the cursor byte offset and reuses the unchanged token-tree `analyze` on the left side. Cursor-at-end is byte-identical to today (suffix empty), so existing behavior is regression-safe. - query_editor: convert promkit's char-index cursor to a byte offset (`text_and_cursor_byte`), send it with the completion trigger, and splice suggestions mid-line via `replace_text_at` (cursor lands after the suggestion, suffix preserved). - completion: thread the cursor + preserved suffix through `enter` and `get_current_item` so both the initial TAB result and suggestion cycling are cursor-aware. The in-paren `select(...)` case remains a separate follow-up (needs jaq's unreleased `yield` branch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed a follow-up implementing cursor-awareness (the next item on the roadmap from #114): completion now segments only the text left of the cursor and preserves the text to its right, instead of treating the whole line as the segment. So TAB at Implementation builds on the token-tree port: a pure The in-paren |
|
I would make a separate PR for the jaq 3.0 upgrade, to keep this PR smaller. |
|
Agreed. The jaq 3.0 bump is only here because the token-based segmentation uses Since #115 ( |
feat(completion): value-based completion after pipes
Implements completion beyond root JSON paths for #114, building on the design
discussion there between @ynqa, @01mf02, and @wader. It uses the value-based
analysis that thread converged on — run the filter prefix and derive
candidates from the values flowing at that point — and now follows the agreed
token-tree segmentation and cursor-aware model rather than the original
proof-of-concept's heuristics.
What works
{first: 1} | .fi‸.first.foo | .b‸.foo | .bar,.foo | .baz.foo, not root.items[] | .‸.name,.qty.items[] | .na‸me.name(keepsmeafter cursor).f‸(no pipe)How it works (reuses existing primitives)
|into(base, segment)using jaq's own tokenizer (jaq_core::load::lex), so itcan't disagree with how jq parses the query — string literals, interpolation,
and
(...)/[...]/{...}nesting are handled by the lexer, and|=is adistinct token rather than a boundary. Text right of the cursor is preserved
verbatim.
SuggestionStore.basewith the existingrun_jaq, enumerate itsoutput's paths with
get_all_paths, filter bysegment, and splice thechosen suggestion back in as
prefix + suggestion + suffix, landing thecursor right after the inserted text. Parsed input is cached so a piped
completion doesn't re-deserialize per keypress.
Relative to the agreed design
The two items the #114 thread settled on as the right direction are now in:
load::lextoken trees (replacing thePoC's byte scanner) — thanks @01mf02 for the pointer and the offer to help.
cursor and preserves the right side, instead of assuming cursor-at-end.
Still intentionally out of scope:
select(.a.b<cur> | ...), in-paren) depends on the newyieldfilter (Implement
yield $x01mf02/jaq#426), which is still unmerged and not in released jaq-core.jnv now tracks jaq 3.0.0; this case is a natural follow-up once
yieldlands.definteriors remain OUT, per @ynqa's scoping comment.Testing
cargo testcovers segmentation (strings,()/[]/{}nesting,|=,trailing-dot, unbalanced-input fallback), the cursor cases (mid-line, mid-pipe,
cursor-at-end regression, multi-byte boundary), the char→byte cursor
conversion, relative-path enumeration, array-element descent, and invalid-base
fallback.
cargo fmt/cargo clippyclean.Happy to adjust scope or approach — let me know if anything here should look
different.
🤖 Generated with Claude Code