Skip to content

Add definitionStyle option to control multi-head goto definition#39

Open
flowerett wants to merge 2 commits intoremoteoss:mainfrom
flowerett:configure-goto-for-multiple-function-definitions
Open

Add definitionStyle option to control multi-head goto definition#39
flowerett wants to merge 2 commits intoremoteoss:mainfrom
flowerett:configure-goto-for-multiple-function-definitions

Conversation

@flowerett
Copy link
Copy Markdown
Contributor

@flowerett flowerett commented Apr 16, 2026

Solves this issue

When a function has multiple heads/clauses, editors like Zed show a picker UI instead of jumping directly.

The new "definitionStyle" initializationOption ("all" or "first") lets users choose whether to return all definition sites or just the first one.

the new initializationOption will look like:

"dexter": {
      "initialization_options": {
        ...
        "definitionStyle": "first" # or "all"
      }
}

Note

Medium Risk
Touches go-to-definition resolution and delegate-following logic, so incorrect arity detection or filtering could cause missed/incorrect definition targets in some call forms.

Overview
Adds a new LSP initializationOptions.definitionStyle setting (all default, first optional) to control whether textDocument/definition returns all matching function heads or only the first location (to avoid editor pickers).

Updates go-to-definition to infer callsite arity (including capture syntax and pipe-adjusted calls) and uses new arity-filtered store queries (LookupFunctionByArity, LookupFollowDelegateByArity) to avoid returning unrelated arities and to follow defdelegate chains per-arity. Adds targeted tests to pin arity filtering, delegate/def mixes, and definitionStyle behavior, and documents the option in the README (including Zed config example).

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

@flowerett flowerett self-assigned this Apr 16, 2026
@flowerett
Copy link
Copy Markdown
Contributor Author

@JesseHerrick would be great to know your thoughts about this approach and naming.

@flowerett flowerett requested a review from JesseHerrick April 16, 2026 09:24
@JesseHerrick
Copy link
Copy Markdown
Member

Hey @flowerett, thanks for the PR! This same thing happens with all editors and LSPs when there are multiple function heads for the same definition with the same arity. IMO that is a feature. While it's something we could change in the LSP, this is something I'd actually leave up to the LSP client and editor to configure to their liking. Editors should be able to just say "give me the first option" instead of opening the list if they want it. In NeoVim this isn't too tricky to configure, but maybe in Zed it's a pain. Thoughts?

  When a function has multiple heads/clauses, editors like Zed show a picker UI instead of jumping directly.
  The new "definitionStyle" initializationOption ("all" or "first") lets users choose whether to return all definition sites or just the first one.
@flowerett flowerett force-pushed the configure-goto-for-multiple-function-definitions branch from ef4e0ff to 9e448a6 Compare April 22, 2026 13:32
Two related bugs made goto-definition return multiple Location entries for
calls a human reads as resolving to a single definition. Zed renders those
multi-location results as multi-cursor selections (rather than a picker),
which is what knoebber reported on issue remoteoss#38.

1. LookupFunction did not filter by arity, so `Foo.square(3)` returned
   both `square/1` and `square/2` rows when both were defined. Added
   LookupFunctionByArity and compute the call-site arity in Definition()
   via a new TokenizedFile.ArityAtCallsite helper that handles parens,
   zero-arg calls, `&Foo.bar/2` captures, and pipe context.

2. lookupFollowDelegate only followed delegates when every row was a
   delegate, so `defdelegate foo/1` + `def foo/2` in the same module
   returned both rows instead of following the /1 delegate. Rewrote it
   to partition by arity and follow per-arity, and added
   LookupFollowDelegateByArity.

Existing LookupFunction / LookupFollowDelegate signatures are unchanged
so the 20+ other callers still work; only Definition() is arity-aware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@flowerett
Copy link
Copy Markdown
Contributor Author

Hey @JesseHerrick, thanks for the feedback!
Agreed this would be better to put into the client. The practical blocker is specifically Zed though:

  • The Zed extension API is a passthrough for LSP responses — it doesn't expose a hook to filter a Location array, so we can't solve this in the elixir-zed extension.
  • Zed core doesn't currently expose a "jump to first definition" setting either.

So for Zed users today there's no client-side option at all.
For NeoVim / VS Code users who can configure it, this PR's option is a no-op (default "all").

While validating the original report locally I also found two real bugs that were producing multi-location results for calls with a single intended target:
LookupFunction wasn't filtering by arity, and lookupFollowDelegate didn't follow when a module mixed a defdelegate with a def of the same name. I'm pushing those fixes as a separate commit on this branch. With them in, definitionStyle is narrowly the opt-in for what you described as the feature case (same module, same arity, multiple heads) rather than a workaround for the broader issue.

I've created a small Elixir project to debug and reproduce the issues - [dummy_dexter_ex}(https://github.com/flowerett/dummy_dexter_ex)

My preference would be to keep it in this PR with default "all", this won't change default behavior and Zed users get a workable option - "first" if they want to.

@flowerett flowerett marked this pull request as ready for review April 22, 2026 16:36
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.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit bdcc215. Configure here.

Comment thread internal/lsp/elixir.go
}
}
return -1
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Keyword args inflate arity, breaking definition lookup

High Severity

countCallArgs counts top-level commas to determine call arity, but Elixir's spread keyword arguments (e.g., Repo.insert(changeset, returning: true, on_conflict: :replace)) use commas between key-value pairs while the entire keyword tail counts as a single argument. The function returns arity 3 here instead of 2. When this inflated arity is passed to LookupFunctionByArity, the exact-match SQL filter finds zero rows, and go-to-definition silently returns nothing — a regression from the previous behavior that returned all arities.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bdcc215. Configure here.

@JesseHerrick
Copy link
Copy Markdown
Member

Thanks @flowerett. You're right - I was confusing the go-to-ref arity checking with go-to-definition. We indeed should add that filtering.

I'm totally fine with adding this change as default off so that those who want it can enable it. I'll review the PR later today.

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.

2 participants