Skip to content

Commit

Permalink
Add hover functionality for dependencies in Gemfile (#1279)
Browse files Browse the repository at this point in the history
* Add hover functionality to dependencies in Gemfile

* Add test case

* Always use currently loaded spec

* Incorporate feedback

* Add additional test cases

* Remove unused instance variable

* Remove leading whitespace from gem info

* Use safe navigation

* Cast info as String

* Skip assertion on gem description
  • Loading branch information
bravehager authored Jan 10, 2024
1 parent ebb4aa6 commit a68fceb
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 11 deletions.
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
49 changes: 47 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,43 @@ 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 = T.let(
[
spec.description,
spec.summary,
"This rubygem does not have a description or summary.",
].find { |text| !text.nil? && !text.empty? },
String,
)

# Remove leading whitespace if a heredoc was used for the summary or description
info = info.gsub(/^ +/, "")

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,
),
)
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
74 changes: 74 additions & 0 deletions test/requests/hover_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,80 @@ 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'
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)
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

0 comments on commit a68fceb

Please sign in to comment.