Skip to content

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

Open
flowerett wants to merge 9 commits into
remoteoss:mainfrom
flowerett:configure-goto-for-multiple-function-definitions
Open

flowerett wants to merge 9 commits into
remoteoss:mainfrom
flowerett:configure-goto-for-multiple-function-definitions

Conversation

@flowerett

@flowerett flowerett commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor

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
Changes core LSP definition resolution (arity parsing, delegate following, use chains); broad test coverage but heuristic arity detection can mis-resolve ambiguous calls.

Overview
Adds definitionStyle ("all" | "first", default "all") as an LSP initializationOption and applies it at the end of go-to-definition so editors can jump to a single site instead of showing a multi-head picker (e.g. Zed).

Go-to-definition is now arity-aware. The server infers call arity at the cursor (ArityAtCallsite, including keyword lists, pipes, captures, and many edge cases) and threads it through lookups: indexed store queries (LookupFunctionByArity, per-arity LookupFollowDelegateByArity), current-buffer FindDefinitionLines, generated BEAM symbols, and use / __using__ chains (including inline defs and dynamic imports). When arity is unknown, behavior stays broad; when it matches, overloads and mixed defdelegate/def names resolve to the right clause.

use-chain visiting keys visits on consumer opts so the same injector used with different options is not skipped incorrectly.

README documents definitionStyle for Neovim and JSON LSP config.

Reviewed by Cursor Bugbot for commit 6fac4f1. 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?

@flowerett
flowerett force-pushed the configure-goto-for-multiple-function-definitions branch from ef4e0ff to 9e448a6 Compare April 22, 2026 13:32
@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

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread internal/lsp/elixir.go
@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.

@flowerett

Copy link
Copy Markdown
Contributor Author

Hey @JesseHerrick should I finish this fix or is it better to close the PR, wdyt?

@JesseHerrick

Copy link
Copy Markdown
Member

Hey @flowerett, sorry, yes I think we should finish this one up. There's two important elements:

  • The lookup by arity is a really nice improvement.
  • The disabled-by-default only give me one result option will also be nice to have.

flowerett and others added 3 commits September 29, 2026 19:32
  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.
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
flowerett force-pushed the configure-goto-for-multiple-function-definitions branch from bdcc215 to 380912e Compare September 29, 2026 17:40
@flowerett

Copy link
Copy Markdown
Contributor Author

Hey @JesseHerrick !
I finally found some time to fix this MR, I believe all the feedback is addressed now and it's now ready to merge.

@flowerett flowerett left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Some findings

  • [important] internal/lsp/elixir.go:85 miscomputes arity for valid nested, block, capture-like, interpolation, and parenthesis-free forms, so go-to-definition can select the wrong function.
  • [important] internal/lsp/server.go:1134 leaves bare current-module calls on the old first-name-match path, bypassing both arity filtering and definitionStyle.
  • [important] internal/lsp/name_navigation.go:74 filters use-chain results after choosing a provider, so an earlier matching-arity import is missed; initialization coverage and the three direct token loops also need follow-up.

Comment thread internal/lsp/elixir.go
Comment thread internal/lsp/server.go
Comment thread internal/lsp/name_navigation.go Outdated
Comment thread internal/lsp/server_test.go Outdated
Comment thread internal/lsp/elixir.go Outdated

@flowerett flowerett left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

New findings

  • [important] internal/lsp/elixir.go:191 still assigns special-form commas to the outer call, so exact lookup can select the wrong arity or fall back to the module. (narrowed)
  • [important] internal/lsp/elixir.go:298 scans every module in the buffer, so a bare call in the first module can navigate to an unrelated nested or later declaration. (new)
  • [important] internal/lsp/server.go:2538 shares cycle state across parameterized uses, so a valid earlier provider can be skipped and navigation falls back to the consumer module. (repeat)

Round 2 · reviewed at fd8780a

Comment thread internal/lsp/elixir.go
Comment thread internal/lsp/elixir.go Outdated
Comment thread internal/lsp/server.go
flowerett and others added 2 commits October 5, 2026 13:07
…multiple-function-definitions

# Conflicts:
#	internal/lsp/name_navigation.go
- Commas after a parenthesis-free call inside the argument list (`if`, `for`,
  `with`, `fetch user, opts`) belong to that call, so they no longer inflate
  the outer call's arity.
- The current-buffer definition scan only collects declarations made
  directly in the resolved module, not nested or sibling modules.
- Use-chain cycle state is keyed by consumer opts, so the same injector used
  twice with different providers is searched for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 3 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 4c6d467. Configure here.

Comment thread internal/lsp/elixir.go
Comment thread internal/lsp/name_navigation.go Outdated
Comment thread internal/lsp/server.go
- A `do` after a def/defp/defmacro head opens the body, so it no longer
  counts as an extra argument when resolving the head's arity.
- LookupName falls back to the module when no generated arity matches
  instead of returning nothing.
- Bare-call generated definitions narrow to the call's arity when one
  matches and honour definitionStyle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flowerett

This comment was marked as duplicate.

@flowerett flowerett left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Approve

Round 3 · reviewed at 1d5ae1a

automatic review done by (claude-opus-5-5 medium)

@flowerett flowerett left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔴 Request changes

  • [critical] internal/lsp/server_test.go:333 and :614 preserve personal attributions in repository history; replace them with issue/PR references and remove them from the branch’s commits. (new)

Blocking remaining: 1

Round 4 · reviewed at 1d5ae1a

automatic review done by (gpt-6.1-sol medium)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@flowerett flowerett left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔴 Request changes

  • [critical] internal/store/store.go:2084 follows default-argument delegates at the wrapper’s arity, selecting an unrelated overload or stopping before the implementation. (new)

Round 5 · reviewed at 18b453e

automatic review done by (gpt-6.1-sol medium)

Comment thread internal/store/store.go Outdated
A `defdelegate run(x, opts \\ [])` indexes run/1 and run/2, but both call
the target with every argument. Following the run/1 row now looks up the
target's run/2 instead of an unrelated run/1 overload, and reaches the
implementation when the target has no run/1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@flowerett flowerett left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Approve

Blocking remaining: none

Round 6 · reviewed at 6fac4f1

automatic review done by (gpt-6.1-sol medium)

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