Skip to content

Commit

Permalink
Highlight definition and control block structures (#2779)
Browse files Browse the repository at this point in the history
### Motivation

Closes #2494

### Implementation

If the cursor is on a definition or control node, both the start and end of the node will be highlighted.

### Automated Tests

Amended existing test case.

### Manual Tests

Place the cursor on one of the definition or control nodes.
  • Loading branch information
rogancodes authored Nov 4, 2024
1 parent fbe2350 commit d4064db
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 7 deletions.
95 changes: 91 additions & 4 deletions lib/ruby_lsp/listeners/document_highlight.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,15 @@ class DocumentHighlight
target: T.nilable(Prism::Node),
parent: T.nilable(Prism::Node),
dispatcher: Prism::Dispatcher,
position: T::Hash[Symbol, T.untyped],
).void
end
def initialize(response_builder, target, parent, dispatcher)
def initialize(response_builder, target, parent, dispatcher, position)
@response_builder = response_builder

return unless target && parent

highlight_target =
highlight_target, highlight_target_value =
case target
when Prism::GlobalVariableReadNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode,
Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableTargetNode, Prism::GlobalVariableWriteNode,
Expand All @@ -116,13 +117,17 @@ def initialize(response_builder, target, parent, dispatcher)
Prism::CallNode, Prism::BlockParameterNode, Prism::RequiredKeywordParameterNode,
Prism::RequiredKeywordParameterNode, Prism::KeywordRestParameterNode, Prism::OptionalParameterNode,
Prism::RequiredParameterNode, Prism::RestParameterNode
[target, node_value(target)]
when Prism::ModuleNode, Prism::ClassNode, Prism::SingletonClassNode, Prism::DefNode, Prism::CaseNode,
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::IfNode, Prism::UnlessNode
target
end

@target = T.let(highlight_target, T.nilable(Prism::Node))
@target_value = T.let(node_value(highlight_target), T.nilable(String))
@target_value = T.let(highlight_target_value, T.nilable(String))
@target_position = position

if @target && @target_value
if @target
dispatcher.register(
self,
:on_call_node_enter,
Expand Down Expand Up @@ -172,6 +177,13 @@ def initialize(response_builder, target, parent, dispatcher)
:on_global_variable_or_write_node_enter,
:on_global_variable_and_write_node_enter,
:on_global_variable_operator_write_node_enter,
:on_singleton_class_node_enter,
:on_case_node_enter,
:on_while_node_enter,
:on_until_node_enter,
:on_for_node_enter,
:on_if_node_enter,
:on_unless_node_enter,
)
end
end
Expand All @@ -189,6 +201,8 @@ def on_call_node_enter(node)

sig { params(node: Prism::DefNode).void }
def on_def_node_enter(node)
add_matching_end_highlights(node.def_keyword_loc, node.end_keyword_loc) if @target.is_a?(Prism::DefNode)

return unless matches?(node, [Prism::CallNode, Prism::DefNode])

add_highlight(Constant::DocumentHighlightKind::WRITE, node.name_loc)
Expand Down Expand Up @@ -252,13 +266,17 @@ def on_required_parameter_node_enter(node)

sig { params(node: Prism::ClassNode).void }
def on_class_node_enter(node)
add_matching_end_highlights(node.class_keyword_loc, node.end_keyword_loc) if @target.is_a?(Prism::ClassNode)

return unless matches?(node, CONSTANT_NODES + CONSTANT_PATH_NODES + [Prism::ClassNode])

add_highlight(Constant::DocumentHighlightKind::WRITE, node.constant_path.location)
end

sig { params(node: Prism::ModuleNode).void }
def on_module_node_enter(node)
add_matching_end_highlights(node.module_keyword_loc, node.end_keyword_loc) if @target.is_a?(Prism::ModuleNode)

return unless matches?(node, CONSTANT_NODES + CONSTANT_PATH_NODES + [Prism::ModuleNode])

add_highlight(Constant::DocumentHighlightKind::WRITE, node.constant_path.location)
Expand Down Expand Up @@ -511,6 +529,55 @@ def on_global_variable_operator_write_node_enter(node)
add_highlight(Constant::DocumentHighlightKind::WRITE, node.name_loc)
end

sig { params(node: Prism::SingletonClassNode).void }
def on_singleton_class_node_enter(node)
return unless @target.is_a?(Prism::SingletonClassNode)

add_matching_end_highlights(node.class_keyword_loc, node.end_keyword_loc)
end

sig { params(node: Prism::CaseNode).void }
def on_case_node_enter(node)
return unless @target.is_a?(Prism::CaseNode)

add_matching_end_highlights(node.case_keyword_loc, node.end_keyword_loc)
end

sig { params(node: Prism::WhileNode).void }
def on_while_node_enter(node)
return unless @target.is_a?(Prism::WhileNode)

add_matching_end_highlights(node.keyword_loc, node.closing_loc)
end

sig { params(node: Prism::UntilNode).void }
def on_until_node_enter(node)
return unless @target.is_a?(Prism::UntilNode)

add_matching_end_highlights(node.keyword_loc, node.closing_loc)
end

sig { params(node: Prism::ForNode).void }
def on_for_node_enter(node)
return unless @target.is_a?(Prism::ForNode)

add_matching_end_highlights(node.for_keyword_loc, node.end_keyword_loc)
end

sig { params(node: Prism::IfNode).void }
def on_if_node_enter(node)
return unless @target.is_a?(Prism::IfNode)

add_matching_end_highlights(node.if_keyword_loc, node.end_keyword_loc)
end

sig { params(node: Prism::UnlessNode).void }
def on_unless_node_enter(node)
return unless @target.is_a?(Prism::UnlessNode)

add_matching_end_highlights(node.keyword_loc, node.end_keyword_loc)
end

private

sig { params(node: Prism::Node, classes: T::Array[T.class_of(Prism::Node)]).returns(T.nilable(T::Boolean)) }
Expand Down Expand Up @@ -550,6 +617,26 @@ def node_value(node)
node.constant_path.slice
end
end

sig { params(keyword_loc: T.nilable(Prism::Location), end_loc: T.nilable(Prism::Location)).void }
def add_matching_end_highlights(keyword_loc, end_loc)
return unless keyword_loc && end_loc && end_loc.length.positive?
return unless covers_target_position?(keyword_loc) || covers_target_position?(end_loc)

add_highlight(Constant::DocumentHighlightKind::TEXT, keyword_loc)
add_highlight(Constant::DocumentHighlightKind::TEXT, end_loc)
end

sig { params(location: Prism::Location).returns(T::Boolean) }
def covers_target_position?(location)
start_line = location.start_line - 1
end_line = location.end_line - 1
start_covered = start_line < @target_position[:line] ||
(start_line == @target_position[:line] && location.start_column <= @target_position[:character])
end_covered = end_line > @target_position[:line] ||
(end_line == @target_position[:line] && location.end_column >= @target_position[:character])
start_covered && end_covered
end
end
end
end
8 changes: 7 additions & 1 deletion lib/ruby_lsp/requests/document_highlight.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ def initialize(global_state, document, position, dispatcher)
ResponseBuilders::CollectionResponseBuilder[Interface::DocumentHighlight].new,
ResponseBuilders::CollectionResponseBuilder[Interface::DocumentHighlight],
)
Listeners::DocumentHighlight.new(@response_builder, node_context.node, node_context.parent, dispatcher)
Listeners::DocumentHighlight.new(
@response_builder,
node_context.node,
node_context.parent,
dispatcher,
position,
)
end

sig { override.returns(T::Array[Interface::DocumentHighlight]) }
Expand Down
31 changes: 29 additions & 2 deletions test/expectations/document_highlight/class_declaration.exp.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
{
"params": [
{
"line": 1,
"line": 0,
"character": 2
}
],
"result": []
"result": [
{
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 5
}
},
"kind": 1
},
{
"range": {
"start": {
"line": 4,
"character": 0
},
"end": {
"line": 4,
"character": 3
}
},
"kind": 1
}
]
}

0 comments on commit d4064db

Please sign in to comment.