Skip to content

Implement "go to definition" for render calls in ERB templates#659

Open
janko wants to merge 6 commits into
Shopify:mainfrom
janko:go-to-render
Open

Implement "go to definition" for render calls in ERB templates#659
janko wants to merge 6 commits into
Shopify:mainfrom
janko:go-to-render

Conversation

@janko

@janko janko commented Oct 28, 2025

Copy link
Copy Markdown
Contributor

It's common to want to traverse through several partials while updating HTML that a controller action renders. Rails.vim has a neat gf shortcut for this. This brings the same functionality to Ruby LSP Rails, by implementing "go to definition" support for render calls inside ERB templates.

Screen.Recording.2025-10-28.at.21.08.51.mov

It supports partial name passed as positional argument, or via :partial, :layout, and :spacer_template keyword arguments. It even handles :variants, :formats, and :handlers options, as well as :template for rendering non-partial templates.

The initial implementation mimicked the template lookup logic inside the language server. However, that turned out to be more complex and didn't handle custom view_paths. So, I ended up calling ActionView::LookupContext to perform the actual template lookup on the server. I also needed to determine the controller from the template directory in a way that handles custom view_paths, as well as handle controllerless template directories.

To avoid the overhead of booting the Rails process too many times in tests, I updated the test helpers to allow sending multiple textDocument/definition requests to the same server.

It's common to want to traverse through several partials while updating
HTML that a controller action renders. Rails.vim has a neat `gf`
shortcut for this, though it probably doesn't have the precision that
Prism would provide.

This brings the same functionality to Ruby LSP Rails, by implementing
"go to definition" support for render calls inside ERB templates. It
supports partial name passed as positional argument, or via `:partial`,
`:layout`, and `:spacer_template` keyword arguments. It even handles
`:variants`, `:formats`, and `:handlers` options, as well as `:template`
for rendering non-partial templates. Relative lookup will also check
in view directories of controller ancestors.

For the latter, I considered doing a call to the Rails process that will
return `ActionController::Base._prefixes`. However, I couldn't think of
a good enough interface, and that method ableit public is undocumented,
so it seems like we shouldn't rely on it. Given that this ancestry
lookup is non-configurable anyway, I chose to implement it in Ruby LSP
land based on indexed controller files.

To avoid the overhead of booting the Rails process too many times in
tests, I updated the test helpers to allow sending multiple
`textDocument/definition` requests to the same server.
@janko janko requested a review from a team as a code owner October 28, 2025 20:11

@rafaelfranca rafaelfranca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Made a few comments. Typechecker is failing and new methods are missing typing information.

Comment thread lib/ruby_lsp/ruby_lsp_rails/runner_client.rb Outdated
Comment thread lib/ruby_lsp/ruby_lsp_rails/definition.rb
janko added 2 commits November 1, 2025 20:22
Also add missing type annotations.
While here, we undo changes in code lens to handle custom view paths. If
this approach is accepted, we can always update the code lens later.
@janko

janko commented Nov 3, 2025

Copy link
Copy Markdown
Contributor Author

@rafaelfranca I believe I addressed all your feedback and fixed typechecking.

@rafaelfranca rafaelfranca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Amazing work!

@Shopify/ruby-dx can someone do a second review?

@janko

janko commented Nov 10, 2025

Copy link
Copy Markdown
Contributor Author

I have signed the CLA!

In our app, we have an `app/views/shared/components` directory that
contains shared "component" partials. It doesn't have any matching
controller, and components can render other components, you just need
to specify the full path to the render call (we have a helper method for
that). We should make go to definition work for these cases as well.
@cb341

cb341 commented Dec 15, 2025

Copy link
Copy Markdown

Very cool feature!
Any plans to get this merged?

@leonvogt

Copy link
Copy Markdown

This would be awesome and such a quality of life improvement!

janko added 2 commits April 14, 2026 23:26
We want to support `render partial: "foo"` but not `render "foo",
partial: "bar"`, because in the latter case, `:partial` will be a
template local.
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.

4 participants