Skip to content

Commit

Permalink
Add jump to view code lenses (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock authored Jul 19, 2024
1 parent b39a8de commit b0f848f
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 6 deletions.
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|
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

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,
]],
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

0 comments on commit b0f848f

Please sign in to comment.