Skip to content
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
1 change: 0 additions & 1 deletion lib/ruby_lsp/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
require "ruby_lsp/base_server"
require "ruby_indexer/ruby_indexer"
require "ruby_lsp/utils"
require "ruby_lsp/static_docs"
require "ruby_lsp/scope"
require "ruby_lsp/client_capabilities"
require "ruby_lsp/global_state"
Expand Down
296 changes: 223 additions & 73 deletions lib/ruby_lsp/listeners/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,13 @@ module Listeners
class Hover
include Requests::Support::Common

ALLOWED_TARGETS = [
Prism::BreakNode,
Prism::CallNode,
Prism::ConstantReadNode,
Prism::ConstantWriteNode,
Prism::ConstantPathNode,
Prism::GlobalVariableAndWriteNode,
Prism::GlobalVariableOperatorWriteNode,
Prism::GlobalVariableOrWriteNode,
Prism::GlobalVariableReadNode,
Prism::GlobalVariableTargetNode,
Prism::GlobalVariableWriteNode,
Prism::InstanceVariableReadNode,
Prism::InstanceVariableAndWriteNode,
Prism::InstanceVariableOperatorWriteNode,
Prism::InstanceVariableOrWriteNode,
Prism::InstanceVariableTargetNode,
Prism::InstanceVariableWriteNode,
Prism::SymbolNode,
Prism::StringNode,
Prism::InterpolatedStringNode,
Prism::SuperNode,
Prism::ForwardingSuperNode,
Prism::YieldNode,
Prism::ClassVariableAndWriteNode,
Prism::ClassVariableOperatorWriteNode,
Prism::ClassVariableOrWriteNode,
Prism::ClassVariableReadNode,
Prism::ClassVariableTargetNode,
Prism::ClassVariableWriteNode,
] #: Array[singleton(Prism::Node)]

ALLOWED_REMOTE_PROVIDERS = [
"https://github.com",
"https://gitlab.com",
].freeze #: Array[String]

#: (ResponseBuilders::Hover response_builder, GlobalState global_state, URI::Generic uri, NodeContext node_context, Prism::Dispatcher dispatcher, SorbetLevel sorbet_level) -> void
def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
#: (ResponseBuilders::Hover response_builder, GlobalState global_state, URI::Generic uri, NodeContext node_context, Prism::Dispatcher dispatcher, SorbetLevel sorbet_level, Hash[Symbol, untyped] position) -> void
def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level, position) # rubocop:disable Metrics/ParameterLists
@response_builder = response_builder
@global_state = global_state
@index = global_state.index #: RubyIndexer::Index
Expand All @@ -53,45 +21,80 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so
@path = uri.to_standardized_path #: String?
@node_context = node_context
@sorbet_level = sorbet_level
@position = position

dispatcher.register(
self,
:on_alias_global_variable_node_enter,
:on_alias_method_node_enter,
:on_and_node_enter,
:on_begin_node_enter,
:on_block_node_enter,
:on_break_node_enter,
:on_call_node_enter,
:on_case_match_node_enter,
:on_case_node_enter,
:on_class_node_enter,
:on_singleton_class_node_enter,
:on_lambda_node_enter,
:on_class_variable_and_write_node_enter,
:on_class_variable_operator_write_node_enter,
:on_class_variable_or_write_node_enter,
:on_class_variable_read_node_enter,
:on_class_variable_target_node_enter,
:on_class_variable_write_node_enter,
:on_constant_path_node_enter,
:on_constant_read_node_enter,
:on_constant_write_node_enter,
:on_constant_path_node_enter,
:on_call_node_enter,
:on_def_node_enter,
:on_defined_node_enter,
:on_else_node_enter,
:on_ensure_node_enter,
:on_false_node_enter,
:on_for_node_enter,
:on_forwarding_super_node_enter,
:on_global_variable_and_write_node_enter,
:on_global_variable_operator_write_node_enter,
:on_global_variable_or_write_node_enter,
:on_global_variable_read_node_enter,
:on_global_variable_target_node_enter,
:on_global_variable_write_node_enter,
:on_instance_variable_read_node_enter,
:on_instance_variable_write_node_enter,
:on_if_node_enter,
:on_in_node_enter,
:on_instance_variable_and_write_node_enter,
:on_instance_variable_operator_write_node_enter,
:on_instance_variable_or_write_node_enter,
:on_instance_variable_read_node_enter,
:on_instance_variable_target_node_enter,
:on_super_node_enter,
:on_forwarding_super_node_enter,
:on_string_node_enter,
:on_instance_variable_write_node_enter,
:on_interpolated_string_node_enter,
:on_module_node_enter,
:on_next_node_enter,
:on_nil_node_enter,
:on_or_node_enter,
:on_post_execution_node_enter,
:on_pre_execution_node_enter,
:on_redo_node_enter,
:on_rescue_modifier_node_enter,
:on_rescue_node_enter,
:on_retry_node_enter,
:on_return_node_enter,
:on_self_node_enter,
:on_source_encoding_node_enter,
:on_source_file_node_enter,
:on_source_line_node_enter,
:on_string_node_enter,
:on_super_node_enter,
:on_true_node_enter,
:on_undef_node_enter,
:on_unless_node_enter,
:on_until_node_enter,
:on_when_node_enter,
:on_while_node_enter,
:on_yield_node_enter,
:on_class_variable_and_write_node_enter,
:on_class_variable_operator_write_node_enter,
:on_class_variable_or_write_node_enter,
:on_class_variable_read_node_enter,
:on_class_variable_target_node_enter,
:on_class_variable_write_node_enter,
)
end

#: (Prism::BreakNode node) -> void
def on_break_node_enter(node)
handle_keyword_documentation(node.keyword)
end

#: (Prism::StringNode node) -> void
def on_string_node_enter(node)
if @path && File.basename(@path) == GEMFILE_NAME
Expand Down Expand Up @@ -144,6 +147,12 @@ def on_call_node_enter(node)
message = node.message
return unless message

# `not x` is parsed as a call to `!` whose message_loc slices to "not"
if node.name == :! && message == "not"
handle_keyword_documentation("not")
return
end

handle_method_hover(message)
end

Expand Down Expand Up @@ -209,19 +218,150 @@ def on_instance_variable_target_node_enter(node)

#: (Prism::SuperNode node) -> void
def on_super_node_enter(node)
handle_super_node_hover
handle_super_node_hover(node.keyword_loc)
end

#: (Prism::ForwardingSuperNode node) -> void
def on_forwarding_super_node_enter(node)
handle_super_node_hover
handle_super_node_hover(node.location)
end

#: (Prism::AliasGlobalVariableNode) -> void
def on_alias_global_variable_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::AliasMethodNode) -> void
def on_alias_method_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::AndNode) -> void
def on_and_node_enter(node) = handle_keyword_at_location(node.operator_loc)

#: (Prism::BeginNode) -> void
def on_begin_node_enter(node) = handle_keyword_at_location(node.begin_keyword_loc, node.end_keyword_loc)

#: (Prism::BlockNode) -> void
def on_block_node_enter(node) = handle_keyword_at_location(node.opening_loc, node.closing_loc)

#: (Prism::BreakNode) -> void
def on_break_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::CaseMatchNode) -> void
def on_case_match_node_enter(node) = handle_keyword_at_location(node.case_keyword_loc, node.end_keyword_loc)

#: (Prism::CaseNode) -> void
def on_case_node_enter(node) = handle_keyword_at_location(node.case_keyword_loc, node.end_keyword_loc)

#: (Prism::ClassNode) -> void
def on_class_node_enter(node) = handle_keyword_at_location(node.class_keyword_loc, node.end_keyword_loc)

#: (Prism::SingletonClassNode) -> void
def on_singleton_class_node_enter(node)
handle_keyword_at_location(node.class_keyword_loc, node.end_keyword_loc)
end

#: (Prism::LambdaNode) -> void
def on_lambda_node_enter(node) = handle_keyword_at_location(node.opening_loc, node.closing_loc)

#: (Prism::DefNode) -> void
def on_def_node_enter(node) = handle_keyword_at_location(node.def_keyword_loc, node.end_keyword_loc)

#: (Prism::DefinedNode) -> void
def on_defined_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::ElseNode) -> void
def on_else_node_enter(node) = handle_keyword_at_location(node.else_keyword_loc, node.end_keyword_loc)

#: (Prism::EnsureNode) -> void
def on_ensure_node_enter(node) = handle_keyword_at_location(node.ensure_keyword_loc, node.end_keyword_loc)

#: (Prism::FalseNode) -> void
def on_false_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::ForNode) -> void
def on_for_node_enter(node)
handle_keyword_at_location(
node.for_keyword_loc,
node.in_keyword_loc,
node.do_keyword_loc,
node.end_keyword_loc,
)
end

#: (Prism::IfNode) -> void
def on_if_node_enter(node)
handle_keyword_at_location(node.if_keyword_loc, node.then_keyword_loc, node.end_keyword_loc)
end

#: (Prism::YieldNode node) -> void
def on_yield_node_enter(node)
handle_keyword_documentation(node.keyword)
#: (Prism::InNode) -> void
def on_in_node_enter(node) = handle_keyword_at_location(node.in_loc, node.then_loc)

#: (Prism::ModuleNode) -> void
def on_module_node_enter(node) = handle_keyword_at_location(node.module_keyword_loc, node.end_keyword_loc)

#: (Prism::NextNode) -> void
def on_next_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::NilNode) -> void
def on_nil_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::OrNode) -> void
def on_or_node_enter(node) = handle_keyword_at_location(node.operator_loc)

#: (Prism::PostExecutionNode) -> void
def on_post_execution_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::PreExecutionNode) -> void
def on_pre_execution_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::RedoNode) -> void
def on_redo_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::RescueModifierNode) -> void
def on_rescue_modifier_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::RescueNode) -> void
def on_rescue_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.then_keyword_loc)

#: (Prism::RetryNode) -> void
def on_retry_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::ReturnNode) -> void
def on_return_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::SelfNode) -> void
def on_self_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::SourceEncodingNode) -> void
def on_source_encoding_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::SourceFileNode) -> void
def on_source_file_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::SourceLineNode) -> void
def on_source_line_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::TrueNode) -> void
def on_true_node_enter(node) = handle_keyword_at_location(node.location)

#: (Prism::UndefNode) -> void
def on_undef_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::UnlessNode) -> void
def on_unless_node_enter(node)
handle_keyword_at_location(node.keyword_loc, node.then_keyword_loc, node.end_keyword_loc)
end

#: (Prism::UntilNode) -> void
def on_until_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.do_keyword_loc, node.closing_loc)

#: (Prism::WhenNode) -> void
def on_when_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.then_keyword_loc)

#: (Prism::WhileNode) -> void
def on_while_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.do_keyword_loc, node.closing_loc)

#: (Prism::YieldNode) -> void
def on_yield_node_enter(node) = handle_keyword_at_location(node.keyword_loc)

#: (Prism::ClassVariableAndWriteNode node) -> void
def on_class_variable_and_write_node_enter(node)
handle_variable_hover(node.name.to_s)
Expand Down Expand Up @@ -279,27 +419,37 @@ def generate_heredoc_hover(node)
end
end

#: (String keyword) -> void
def handle_keyword_documentation(keyword)
content = KEYWORD_DOCS[keyword]
return unless content
#: (String) -> void
def handle_keyword_documentation(name)
keyword = @graph.keyword(name)
return unless keyword

doc_uri = URI::Generic.from_path(path: File.join(STATIC_DOCS_PATH, "#{keyword}.md"))

@response_builder.push("```ruby\n#{keyword}\n```", category: :title)
@response_builder.push("[Read more](#{doc_uri})", category: :links)
@response_builder.push(content, category: :documentation)
@response_builder.push("```ruby\n#{keyword.name}\n```", category: :title)
@response_builder.push(keyword.documentation, category: :documentation)
end

#: -> void
def handle_super_node_hover
# Sorbet can handle super hover on typed true or higher
return if @sorbet_level.true_or_higher?
# Push keyword documentation when the cursor is on one of the provided locations. The keyword name is taken from
# the covering location's slice so that operator forms (`&&`, `||`, `{`, `}`, ternary `? :`) yield no hover —
# their slice is not a keyword in the Rubydex graph.
#
#: (*Prism::Location?) -> void
def handle_keyword_at_location(*locations)
loc = locations.find { |l| l && covers_position?(l, @position) }
return unless loc

surrounding_method = @node_context.surrounding_method
return unless surrounding_method
handle_keyword_documentation(loc.slice)
end

#: (Prism::Location keyword_location) -> void
def handle_super_node_hover(keyword_location)
# Sorbet can handle the inherited-method hover on typed true or higher, but it does not surface keyword docs, so
# we still push those
unless @sorbet_level.true_or_higher?
surrounding_method = @node_context.surrounding_method
handle_method_hover(surrounding_method, inherited_only: true) if surrounding_method
end

handle_method_hover(surrounding_method, inherited_only: true)
handle_keyword_at_location(keyword_location)
end

#: (String message, ?inherited_only: bool) -> void
Expand Down
Loading
Loading