Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jump to view code lenses #412

Merged
merged 1 commit into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ GEM
rbi (0.1.13)
prism (>= 0.18.0, < 1.0.0)
sorbet-runtime (>= 0.5.9204)
rbs (3.5.1)
rbs (3.5.2)
logger
regexp_parser (2.9.0)
reline (0.5.7)
Expand Down Expand Up @@ -232,7 +232,7 @@ GEM
rubocop (~> 1.51)
rubocop-sorbet (0.8.3)
rubocop (>= 0.90.0)
ruby-lsp (0.17.4)
ruby-lsp (0.17.7)
language_server-protocol (~> 3.17.0)
prism (>= 0.29.0, < 0.31)
rbs (>= 3, < 4)
Expand Down
45 changes: 44 additions & 1 deletion lib/ruby_lsp/ruby_lsp_rails/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,15 @@ def initialize(client, global_state, response_builder, uri, dispatcher)
@group_id_stack = T.let([], T::Array[Integer])
@constant_name_stack = T.let([], T::Array[[String, T.nilable(String)]])

dispatcher.register(self, :on_call_node_enter, :on_class_node_enter, :on_def_node_enter, :on_class_node_leave)
dispatcher.register(
self,
:on_call_node_enter,
:on_class_node_enter,
:on_def_node_enter,
:on_class_node_leave,
:on_module_node_enter,
:on_module_node_leave,
)
end

sig { params(node: Prism::CallNode).void }
Expand All @@ -121,6 +129,7 @@ def on_def_node_enter(node)

if controller?
add_route_code_lens_to_action(node)
add_jump_to_view(node)
end
end

Expand Down Expand Up @@ -158,6 +167,16 @@ def on_class_node_leave(node)
@constant_name_stack.pop
end

sig { params(node: Prism::ModuleNode).void }
def on_module_node_enter(node)
@constant_name_stack << [node.constant_path.slice, nil]
end

sig { params(node: Prism::ModuleNode).void }
def on_module_node_leave(node)
@constant_name_stack.pop
end

private

sig { returns(T.nilable(T::Boolean)) }
Expand All @@ -168,6 +187,30 @@ def controller?
class_name.end_with?("Controller") && superclass_name.end_with?("Controller")
end

sig { params(node: Prism::DefNode).void }
def add_jump_to_view(node)
class_name = @constant_name_stack.map(&:first).join("::")
action_name = node.name
controller_name = class_name
.delete_suffix("Controller")
.gsub(/([a-z])([A-Z])/, "\\1_\\2")
.gsub("::", "/")
.downcase

view_uris = Dir.glob("#{@client.rails_root}/app/views/#{controller_name}/#{action_name}*").map! do |path|
Copy link
Contributor

Choose a reason for hiding this comment

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

We could get this programatically:

irb(main):004> UsersController.view_paths.map(&:path)
=> ["/Users/andyw8/src/github.com/Shopify/ruby-lsp-rails/test/dummy/app/views"]

Copy link
Contributor

Choose a reason for hiding this comment

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

(would require a little more plumbing for the server request of course)

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't look like that method is returning what we want though. It's returning the views directory, but not the directory for the UsersController views. Is there a way to make it return it?

Copy link
Contributor

Choose a reason for hiding this comment

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

I tried but couldn't find an obvious way.

URI::Generic.from_path(path: path).to_s
end
return if view_uris.empty?

@response_builder << create_code_lens(
node,
title: "Jump to view",
command_name: "rubyLsp.openFile",
arguments: [view_uris],
data: { type: "file" },
)
end

sig { params(node: Prism::DefNode).void }
def add_route_code_lens_to_action(node)
class_name, _ = T.must(@constant_name_stack.last)
Expand Down
11 changes: 10 additions & 1 deletion lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class EmptyMessageError < StandardError; end

extend T::Sig

sig { returns(String) }
attr_reader :rails_root
vinistock marked this conversation as resolved.
Show resolved Hide resolved

sig { void }
def initialize
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
Expand Down Expand Up @@ -67,7 +70,8 @@ def initialize

begin
count += 1
read_response
initialize_response = T.must(read_response)
@rails_root = T.let(initialize_response[:root], String)
rescue EmptyMessageError
$stderr.puts("Ruby LSP Rails is retrying initialize (#{count})")
retry if count < MAX_RETRIES
Expand Down Expand Up @@ -218,6 +222,11 @@ def stopped?
true
end

sig { override.returns(String) }
def rails_root
Dir.pwd
end

private

sig { override.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_lsp/ruby_lsp_rails/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def start
routes_reloader = ::Rails.application.routes_reloader
routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)

initialize_result = { result: { message: "ok" } }.to_json
initialize_result = { result: { message: "ok", root: ::Rails.root.to_s } }.to_json
$stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")

while @running
Expand Down
1 change: 1 addition & 0 deletions test/dummy/app/views/users/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>User list!</h1>
Empty file.
46 changes: 45 additions & 1 deletion test/ruby_lsp_rails/code_lens_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ def index
RUBY
uri = response[0].command.arguments.first.first

assert_equal(1, response.size)
assert_match("GET /users(.:format)", response[0].command.title)
assert_match("config/routes.rb#L4", uri)
end
Expand Down Expand Up @@ -334,6 +333,51 @@ def change
refute_empty(response)
end

test "displays jump to view lenses for actions" do
response = generate_code_lens_for_source(<<~RUBY)
class UsersController < ApplicationController
def index
end
end
RUBY
view_lens = response[1]

assert_equal("Jump to view", view_lens.command.title)
assert_equal(
[[
URI::Generic.from_path(path: "#{dummy_root}/app/views/users/index.html.erb").to_s,
URI::Generic.from_path(path: "#{dummy_root}/app/views/users/index.json.jbuilder").to_s,
vinistock marked this conversation as resolved.
Show resolved Hide resolved
]],
view_lens.command.arguments,
)
end

test "displays jump to view lenses for namespaced controllers" do
FileUtils.mkdir_p("#{dummy_root}/app/views/admin/users")
FileUtils.touch("#{dummy_root}/app/views/admin/users/index.html.erb")
FileUtils.touch("#{dummy_root}/app/views/admin/users/index.json.jbuilder")
response = generate_code_lens_for_source(<<~RUBY)
module Admin
class UsersController < ApplicationController
def index
end
end
end
RUBY
view_lens = response[1]

assert_equal("Jump to view", view_lens.command.title)
assert_equal(
[[
URI::Generic.from_path(path: "#{dummy_root}/app/views/admin/users/index.html.erb").to_s,
URI::Generic.from_path(path: "#{dummy_root}/app/views/admin/users/index.json.jbuilder").to_s,
]],
view_lens.command.arguments,
)
ensure
FileUtils.rm_r("#{dummy_root}/app/views/admin/users")
end

private

attr_reader :ruby
Expand Down
Loading