Add support for struct auto-completion#57
Open
JesseHerrick wants to merge 14 commits into
Open
Conversation
Adds two more sources of struct-type inference for variable dot-completion: - @SPEC parameter typespecs: parameters annotated `t()` or `Module.t()` resolve to that struct (pattern matches still take precedence). - ExCk return-type lookup: `var = Mod.func(...)` resolves `var.` against the struct returned by the compiled function, queried over a new `return_type_struct` op on the persistent BEAM process.
- Detect `&fn/arity` captures in `FindBareFunctionCalls` so renames and find-references include capture call sites alongside direct and pipe calls. - Only apply the `000_`-prefixed `SortText` to variable completions in struct value positions (where a variable is the likely target). Previously the prefix leaked into all `funcPrefix != ""` completions, changing sort order of unrelated completions. - Drop the unused `source []byte` parameter from `countCallArity`. - Replace the `(line, len(text))`-based hack with a new `AllVariableFunctionCalls` that scans every function body in the file. `prewarmStructFieldsFromText` was previously only scanning the last function definition because `VariableFunctionCalls` anchors to the most recent `def` before the offset.
The BEAM formatter script hardcoded `_build/dev/lib/*/ebin` and only
looked at the build root passed by the Go side. Projects that compile
only with `MIX_ENV=test` or that keep their compiled deps in a sibling
sub-project (e.g. `apps/<app>/_build` while the file being edited lives
in `libs/<lib>/` with no `_build` of its own) couldn't load plugins like
Styler — the warning fired and the formatter silently fell back to the
standard formatter.
Try `_build/{dev,test,prod}/lib/*/ebin` in priority order, and if
nothing is found at the build root, descend one and two levels to pick
up nested mix projects' `_build` dirs. Extracted the shared logic into
`Dexter.CodePath.prepend_compiled_deps/1` so the top-level boot path and
each per-formatter init share it.
- Remove Macro.Env from knownNonStructTypes: it is a real struct with fields like module, file, line, etc. This fixes struct field autocompletion for variables typed as Macro.Env.t() in @SPEC. - Remove redundant tokenText helper and use parser.TokenText consistently throughout the file.
Use prevSignificantToken instead of raw tokens[idx-1] so that valid Elixir whitespace patterns like 'user\n.name' and 'user.\nname' are correctly detected as variable field access.
- lineCouldBeStructValueSpaceTrigger now scans previous lines for the
%Module{ opening brace, fixing space-triggered completions in
multi-line struct value positions like:
%User{
name: |
- maybePrewarmStructFields now tracks in-flight prewarms per docURI
to prevent goroutine pile-up on rapid keystrokes. A new prewarm is
skipped if one is already running for the same document; the next
change event after completion triggers a fresh prewarm.
When warmStructFields detects a structFieldGen mismatch (cache invalidated during the async lookup, e.g. for an unrelated module), it bailed out early but never reset the entry's loading flag. If the entry itself wasn't deleted, it remained stuck with loading=true forever. Subsequent calls to cachedStructFieldsOrWarmWithLogging saw the stale flag and returned immediately without spawning a new goroutine, so struct field completion for that module never worked until a server restart or independent invalidation. Reset loading=false on the entry when discarding a stale generation result.
Resolved conflict in internal/lsp/server.go: kept both sides — struct-support's existing completion logic plus main's new tree-sitter variable scope completion block. Also fixed a pre-existing 3-vs-4 return value mismatch in GetTree call.
Detect more patterns that indicate a variable holds a struct, enabling
field autocompletion on variable.field access without needing %Module{}
pattern matches in the same function.
New inference sources:
- Pipe chains: var = A.foo() |> B.bar(x) resolves to B.bar, not A.foo
- Bare local calls: var = build_user(x) stores __MODULE__, resolved via
@SPEC or ExCk lookup on the current module
- with/case <- matches: var <- Mod.func() treated like = assignments
- Struct update syntax: %User{var | ...} infers var as User struct
- Pipe starting with bare value: var = x |> Mod.func()
- @SPEC return types: @SPEC func() :: User.t() infers return struct
for bare local calls, avoiding BEAM round-trips for uncompiled modules
- Destructured patterns: {:ok, var} = Mod.func() extracts all variable
names from the left-hand side pattern
Also:
- Expanded knownNonStructTypes (22 entries) to avoid false @SPEC matches
- Unified readCallArgs helper deduplicates paren/no-paren arity counting
- Prewarm filter extended to trigger on = assignments (not just %)
- Cold cache returns IsIncomplete to signal editor re-query
- Added integration tests for Ecto.Multi struct completion
0f45d8d to
378ee65
Compare
When the BEAM returns loaded-but-empty fields (e.g. a module without a defstruct), cachedStructFieldsOrWarm sets ready=true with zero fields. Both completion paths treated this the same as a cold cache and returned IsIncomplete: true, causing the editor to re-query indefinitely. Now only return IsIncomplete when !ready (cache still warming). When ready but empty, return nil (no completions available).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c21457d. Configure here.
- Remove URI, Regex, Range, MapSet, Date, Time, NaiveDateTime, DateTime, File.Stat, IO.Stream, and Task from knownNonStructTypes. These are all real Elixir structs with defstruct; including them here caused classifySpecType to return "" for @SPEC annotations referencing them, silently disabling struct field completion. - Fix pipe chain arity in scanVariableFunctionCalls: add +1 for the implicit piped first argument in all three pipe-chain code paths (initial pipe check, initial pipe check bare call, and pipe chain following loop). Previously readCallArgs only counted explicit args, causing ExCk ReturnTypeStruct lookups to fail with wrong arity. - Fix race condition in warmStructFields: add per-entry generation tracking to structFieldCacheEntry. Stale goroutines now compare against the entry's generation (not the global gen), preventing them from resetting a newer goroutine's loading flag. This also handles stale loading state by allowing new requests to detect old-generation entries and start a fresh warm instead of waiting indefinitely.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

#43
Note
Medium Risk
Large new completion and caching paths in the LSP server and BEAM protocol; behavior depends on compiled beams and ExCk availability, with graceful fallbacks when lookup fails.
Overview
Adds struct-aware LSP completion: field keys inside
%Module{...}(including update syntax), fields aftervariable.when the variable is typed, and variables/functions in struct value positions aftername:(with a space trigger).The Go side gains token analysis for struct literals, pattern/
@spectyping,var = Module.func(...)(including pipes), and wires these intoCompletionwith a per-build-root struct field cache, background prewarm on open/change, and invalidation on reindex/BEAM eviction. The BEAM CodeIntel service addsstruct_fields(from__struct__/0) andreturn_type_struct(from compiled ExCk chunks), plusDexter.CodePathso formatter/plugins findebinin umbrellas and sibling_buildlayouts.Reviewed by Cursor Bugbot for commit 2e13c53. Bugbot is set up for automated code reviews on this repo. Configure here.