Skip to content

Commit 452d2d7

Browse files
authored
Add hover documentation for 'break' keyword (#3587)
Add hover documentation for break keyword
1 parent d0ffc24 commit 452d2d7

File tree

4 files changed

+147
-18
lines changed

4 files changed

+147
-18
lines changed

lib/ruby_lsp/listeners/hover.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Hover
77
include Requests::Support::Common
88

99
ALLOWED_TARGETS = [
10+
Prism::BreakNode,
1011
Prism::CallNode,
1112
Prism::ConstantReadNode,
1213
Prism::ConstantWriteNode,
@@ -54,6 +55,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so
5455

5556
dispatcher.register(
5657
self,
58+
:on_break_node_enter,
5759
:on_constant_read_node_enter,
5860
:on_constant_write_node_enter,
5961
:on_constant_path_node_enter,
@@ -84,6 +86,11 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so
8486
)
8587
end
8688

89+
#: (Prism::BreakNode node) -> void
90+
def on_break_node_enter(node)
91+
handle_keyword_documentation(node.keyword)
92+
end
93+
8794
#: (Prism::StringNode node) -> void
8895
def on_string_node_enter(node)
8996
if @path && File.basename(@path) == GEMFILE_NAME

lib/ruby_lsp/static_docs.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module RubyLsp
1414

1515
# A map of keyword => short documentation to be displayed on hover or completion
1616
KEYWORD_DOCS = {
17+
"break" => "Terminates the execution of a block or loop",
1718
"yield" => "Invokes the passed block with the given arguments",
1819
}.freeze #: Hash[String, String]
1920
end

static_docs/break.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Break
2+
3+
In Ruby, the `break` keyword is used to exit a loop or block prematurely. Unlike `next` which skips to the next iteration, `break` terminates the loop entirely and continues with the code after the loop.
4+
5+
```ruby
6+
# Basic break usage in a loop
7+
5.times do |i|
8+
break if i == 3
9+
10+
puts i
11+
end
12+
# Output:
13+
# 0
14+
# 1
15+
# 2
16+
```
17+
18+
The `break` statement can be used with any of Ruby's iteration methods or loops.
19+
20+
```ruby
21+
array = [1, 2, 3, 4, 5]
22+
23+
# Break in each iteration
24+
array.each do |num|
25+
break if num > 3
26+
27+
puts "Number: #{num}"
28+
end
29+
# Output:
30+
# Number: 1
31+
# Number: 2
32+
# Number: 3
33+
34+
# Break in an infinite loop
35+
count = 0
36+
loop do
37+
count += 1
38+
break if count >= 3
39+
40+
puts "Count: #{count}"
41+
end
42+
# Output:
43+
# Count: 1
44+
# Count: 2
45+
```
46+
47+
## Break with a Value
48+
49+
When used inside a block, `break` can return a value that becomes the result of the method call.
50+
51+
```ruby
52+
# Break with a return value in map
53+
result = [1, 2, 3, 4, 5].map do |num|
54+
break "Too large!" if num > 3
55+
56+
num * 2
57+
end
58+
puts result # Output: "Too large!"
59+
60+
# Break with a value in find
61+
number = (1..10).find do |n|
62+
break n if n > 5 && n.even?
63+
end
64+
puts number # Output: 6
65+
```
66+
67+
## Break in Nested Loops
68+
69+
When using `break` in nested loops, it only exits the innermost loop. To break from nested loops, you typically need to use a flag or return.
70+
71+
```ruby
72+
# Break in nested iteration
73+
(1..3).each do |i|
74+
puts "Outer: #{i}"
75+
76+
(1..3).each do |j|
77+
break if j == 2
78+
79+
puts " Inner: #{j}"
80+
end
81+
end
82+
# Output:
83+
# Outer: 1
84+
# Inner: 1
85+
# Outer: 2
86+
# Inner: 1
87+
# Outer: 3
88+
# Inner: 1
89+
90+
# Breaking from nested loops with a flag
91+
found = false
92+
(1..3).each do |i|
93+
(1..3).each do |j|
94+
if i * j == 4
95+
found = true
96+
break
97+
end
98+
end
99+
break if found
100+
end
101+
```
102+
103+
The `break` keyword is essential for controlling loop execution and implementing early exit conditions. It's particularly useful when you've found what you're looking for and don't need to continue iterating.

test/requests/hover_expectations_test.rb

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -929,26 +929,44 @@ def name; end
929929
end
930930

931931
def test_hover_for_keywords
932-
source = <<~RUBY
933-
def foo
934-
yield
935-
end
936-
RUBY
932+
test_cases = {
933+
"yield" => {
934+
source: <<~RUBY,
935+
def foo
936+
yield
937+
end
938+
RUBY
939+
position: { line: 1, character: 2 },
940+
},
941+
"break" => {
942+
source: <<~RUBY,
943+
while true
944+
break
945+
end
946+
RUBY
947+
position: { line: 1, character: 2 },
948+
},
949+
}
937950

938-
with_server(source) do |server, uri|
939-
server.process_message(
940-
id: 1,
941-
method: "textDocument/hover",
942-
params: { textDocument: { uri: uri }, position: { character: 2, line: 1 } },
943-
)
951+
test_cases.each do |keyword, config|
952+
with_server(config[:source]) do |server, uri|
953+
server.process_message(
954+
id: 1,
955+
method: "textDocument/hover",
956+
params: {
957+
textDocument: { uri: uri },
958+
position: config[:position],
959+
},
960+
)
944961

945-
contents = server.pop_response.response.contents.value
946-
assert_match("```ruby\nyield\n```", contents)
947-
assert_match(
948-
RubyLsp::KEYWORD_DOCS["yield"], #: as !nil
949-
contents,
950-
)
951-
assert_match("[Read more](#{RubyLsp::STATIC_DOCS_PATH}/yield.md)", contents)
962+
contents = server.pop_response.response.contents.value
963+
assert_match("```ruby\n#{keyword}\n```", contents)
964+
assert_match(
965+
RubyLsp::KEYWORD_DOCS[keyword] || "No documentation found for #{keyword}",
966+
contents,
967+
)
968+
assert_match("[Read more](#{RubyLsp::STATIC_DOCS_PATH}/#{keyword}.md)", contents)
969+
end
952970
end
953971
end
954972

0 commit comments

Comments
 (0)