diff --git a/lib/ruby_lsp/executor.rb b/lib/ruby_lsp/executor.rb index 313627290..74c35448b 100644 --- a/lib/ruby_lsp/executor.rb +++ b/lib/ruby_lsp/executor.rb @@ -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) diff --git a/lib/ruby_lsp/requests/code_lens.rb b/lib/ruby_lsp/requests/code_lens.rb index ca1557fdd..7dc53f1bc 100644 --- a/lib/ruby_lsp/requests/code_lens.rb +++ b/lib/ruby_lsp/requests/code_lens.rb @@ -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]) diff --git a/lib/ruby_lsp/requests/hover.rb b/lib/ruby_lsp/requests/hover.rb index 21fd02b08..f9d6d8c47 100644 --- a/lib/ruby_lsp/requests/hover.rb +++ b/lib/ruby_lsp/requests/hover.rb @@ -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) @@ -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 @@ -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 diff --git a/lib/ruby_lsp/utils.rb b/lib/ruby_lsp/utils.rb index 28b802a67..6dd942e96 100644 --- a/lib/ruby_lsp/utils.rb +++ b/lib/ruby_lsp/utils.rb @@ -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 diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index bac76eee5..7808f1b5f 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -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