Skip to content

Add support for struct auto-completion#57

Open
JesseHerrick wants to merge 14 commits into
mainfrom
struct-support
Open

Add support for struct auto-completion#57
JesseHerrick wants to merge 14 commits into
mainfrom
struct-support

Conversation

@JesseHerrick
Copy link
Copy Markdown
Member

@JesseHerrick JesseHerrick commented May 4, 2026

#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 after variable. when the variable is typed, and variables/functions in struct value positions after name: (with a space trigger).

The Go side gains token analysis for struct literals, pattern/@spec typing, var = Module.func(...) (including pipes), and wires these into Completion with a per-build-root struct field cache, background prewarm on open/change, and invalidation on reindex/BEAM eviction. The BEAM CodeIntel service adds struct_fields (from __struct__/0) and return_type_struct (from compiled ExCk chunks), plus Dexter.CodePath so formatter/plugins find ebin in umbrellas and sibling _build layouts.

Reviewed by Cursor Bugbot for commit 2e13c53. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread internal/lsp/server.go
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.
Comment thread internal/lsp/elixir.go
Comment thread internal/lsp/server.go Outdated
- 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.
Comment thread internal/lsp/elixir.go
Comment thread internal/lsp/elixir.go Outdated
- 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.
Comment thread internal/lsp/server.go
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.
Comment thread internal/lsp/server.go Outdated
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
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).
Comment thread internal/lsp/elixir.go Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

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

Comment thread internal/lsp/elixir.go
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant