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 hover functionality for dependencies in Gemfile #1279

Merged
merged 10 commits into from
Jan 10, 2024
2 changes: 1 addition & 1 deletion lib/ruby_lsp/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ def hover(uri, position)

# Instantiate all listeners
dispatcher = Prism::Dispatcher.new
hover = Requests::Hover.new(@index, nesting, dispatcher, document.typechecker_enabled?)
hover = Requests::Hover.new(uri, @index, nesting, dispatcher, document.typechecker_enabled?)

# Emit events for all listeners
dispatcher.dispatch_once(target)
Expand Down
8 changes: 0 additions & 8 deletions lib/ruby_lsp/requests/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,6 @@ def provider
end + " -Itest ",
String,
)
GEMFILE_NAME = T.let(
begin
Bundler.with_original_env { Bundler.default_gemfile.basename.to_s }
rescue Bundler::GemfileNotFound
"Gemfile"
end,
String,
)
ACCESS_MODIFIERS = T.let([:public, :private, :protected], T::Array[Symbol])
SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])

Expand Down
44 changes: 42 additions & 2 deletions lib/ruby_lsp/requests/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ def provider

sig do
params(
uri: URI::Generic,
index: RubyIndexer::Index,
nesting: T::Array[String],
dispatcher: Prism::Dispatcher,
typechecker_enabled: T::Boolean,
).void
end
def initialize(index, nesting, dispatcher, typechecker_enabled)
def initialize(uri, index, nesting, dispatcher, typechecker_enabled)
@path = T.let(uri.to_standardized_path, T.nilable(String))
@index = index
@nesting = nesting
@_response = T.let(nil, ResponseType)
Expand Down Expand Up @@ -108,9 +110,15 @@ def on_constant_path_node_enter(node)

sig { params(node: Prism::CallNode).void }
def on_call_node_enter(node)
return if @typechecker_enabled
return unless self_receiver?(node)

if @path && File.basename(@path) == GEMFILE_NAME && node.name == :gem
generate_gem_hover(node)
return
end

return if @typechecker_enabled

message = node.message
return unless message

Expand Down Expand Up @@ -142,6 +150,38 @@ def generate_hover(name, location)
contents: markdown_from_index_entries(name, entries),
)
end

sig { params(node: Prism::CallNode).void }
def generate_gem_hover(node)
first_argument = node.arguments&.arguments&.first
return unless first_argument.is_a?(Prism::StringNode)

spec = Gem::Specification.find_by_name(first_argument.content)
return unless spec

info = [spec.description, spec.summary, "This rubygem does not have a description or summary."].find do |text|
!text.nil? && !text.empty?
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we can use #present? here since it's an Active Support extension—not the prettiest thing in the world but I think it's roughly the same in terms of functionality based on this guide.


# Remove leading whitespace if a heredoc was used for the summary or description
info = info&.gsub(/^ +/, "")
bravehager marked this conversation as resolved.
Show resolved Hide resolved

markdown = <<~MARKDOWN
**#{spec.name}** (#{spec.version})

#{info}
MARKDOWN

@_response = Interface::Hover.new(
range: range_from_location(node.location),
contents: Interface::MarkupContent.new(
kind: Constant::MarkupKind::MARKDOWN,
value: markdown,
),
)
vinistock marked this conversation as resolved.
Show resolved Hide resolved
rescue Gem::MissingSpecError
# Do nothing if the spec cannot be found
end
end
end
end
8 changes: 8 additions & 0 deletions lib/ruby_lsp/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ module RubyLsp
end,
T.nilable(String),
)
GEMFILE_NAME = T.let(
begin
Bundler.with_original_env { Bundler.default_gemfile.basename.to_s }
rescue Bundler::GemfileNotFound
"Gemfile"
end,
String,
)

# A notification to be sent to the client
class Message
Expand Down
75 changes: 75 additions & 0 deletions test/requests/hover_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,81 @@ class A
T.must(message_queue).close
end

def test_hovering_over_gemfile_dependency
message_queue = Thread::Queue.new
store = RubyLsp::Store.new

uri = URI("file:///Gemfile")
source = <<~RUBY
gem 'bundler'
vinistock marked this conversation as resolved.
Show resolved Hide resolved
RUBY
store.set(uri: uri, source: source, version: 1)

executor = RubyLsp::Executor.new(store, message_queue)
index = executor.instance_variable_get(:@index)
index.index_single(RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), source)

stub_no_typechecker
response = executor.execute({
method: "textDocument/hover",
params: { textDocument: { uri: uri }, position: { character: 0, line: 0 } },
}).response

spec = Gem.loaded_specs["bundler"]

assert_includes(response.contents.value, spec.name)
assert_includes(response.contents.value, spec.version.to_s)
assert_includes(response.contents.value, spec.description)
ensure
T.must(message_queue).close
end

def test_hovering_over_gemfile_dependency_with_missing_argument
message_queue = Thread::Queue.new
store = RubyLsp::Store.new

uri = URI("file:///Gemfile")
source = <<~RUBY
gem()
RUBY
store.set(uri: uri, source: source, version: 1)

executor = RubyLsp::Executor.new(store, message_queue)
index = executor.instance_variable_get(:@index)
index.index_single(RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), source)

stub_no_typechecker
response = executor.execute({
method: "textDocument/hover",
params: { textDocument: { uri: uri }, position: { character: 0, line: 0 } },
}).response

assert_nil(response)
end

def test_hovering_over_gemfile_dependency_with_non_gem_argument
message_queue = Thread::Queue.new
store = RubyLsp::Store.new

uri = URI("file:///Gemfile")
source = <<~RUBY
gem(method_call)
RUBY
store.set(uri: uri, source: source, version: 1)

executor = RubyLsp::Executor.new(store, message_queue)
index = executor.instance_variable_get(:@index)
index.index_single(RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), source)

stub_no_typechecker
response = executor.execute({
method: "textDocument/hover",
params: { textDocument: { uri: uri }, position: { character: 0, line: 0 } },
}).response

assert_nil(response)
end

def test_hover_addons
source = <<~RUBY
# Hello
Expand Down