From 56944c19bba8bcbf0fa2434226985fe564e5bda5 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Wed, 28 May 2025 19:19:36 -0400 Subject: [PATCH 01/25] Add hover documentation for 'case' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 +++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/case.md | 99 +++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 static_docs/case.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index c533853f5f..a33415bd0b 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -8,6 +8,7 @@ class Hover ALLOWED_TARGETS = [ Prism::CallNode, + Prism::CaseNode, Prism::ConstantReadNode, Prism::ConstantWriteNode, Prism::ConstantPathNode, @@ -54,6 +55,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so dispatcher.register( self, + :on_case_node_enter, :on_constant_read_node_enter, :on_constant_write_node_enter, :on_constant_path_node_enter, @@ -97,6 +99,11 @@ def on_string_node_enter(node) generate_heredoc_hover(node) end + #: (Prism::CaseNode node) -> void + def on_case_node_enter(node) + handle_keyword_documentation(node.case_keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 2749ff8688..48ddced9f1 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -14,6 +14,7 @@ module RubyLsp # A map of keyword => short documentation to be displayed on hover or completion KEYWORD_DOCS = { + "case" => "Starts a case expression for pattern matching or multiple condition checking", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/case.md b/static_docs/case.md new file mode 100644 index 0000000000..19494877c7 --- /dev/null +++ b/static_docs/case.md @@ -0,0 +1,99 @@ +# Case Statement + +In Ruby, the `case` statement provides a clean way to express conditional logic when you need to compare a value against multiple conditions. It's similar to if/else chains but often more readable and concise. + +```ruby +# Basic case statement comparing a value against multiple conditions +grade = "A" + +case grade +when "A" + puts "Excellent!" +when "B" + puts "Good job!" +when "C" + puts "Fair" +else + puts "Need improvement" +end +``` + +The `case` statement can also work with ranges, multiple values, and even custom matching using the `===` operator. + +```ruby +# Case statement with ranges and multiple conditions +score = 85 + +case score +when 90..100 + puts "A grade" +when 80..89 + puts "B grade" +when 70..79 + puts "C grade" +else + puts "Need improvement" +end + +# Case with multiple values in a single when clause +day = "Saturday" + +case day +when "Saturday", "Sunday" + puts "Weekend!" +else + puts "Weekday" +end +``` + +## Pattern Matching (Ruby 2.7+) + +Starting from Ruby 2.7, `case` statements support pattern matching, which provides powerful ways to match and destructure data. + +```ruby +# Pattern matching with arrays +data = [1, 2, 3] + +case data +when [1, 2, 3] + puts "Exact match!" +when [1, *rest] + puts "Starts with 1, followed by #{rest}" +when Array + puts "Any array" +else + puts "Not an array" +end + +# Pattern matching with hashes (Ruby 3.0+) +user = { name: "Alice", age: 30 } + +case user +in { name: "Alice", age: } + puts "Alice is #{age} years old" +in { name:, age: 20.. } + puts "#{name} is at least 20" +else + puts "No match" +end +``` + +## Case without an argument + +Ruby also allows `case` statements without an explicit argument, which acts like a series of if/elsif conditions. + +```ruby +# Case statement without an argument +# rubocop:disable Style/EmptyCaseCondition +case +when Time.now.saturday? + puts "It's Saturday!" +when Time.now.sunday? + puts "It's Sunday!" +else + puts "It's a weekday" +end +# rubocop:enable Style/EmptyCaseCondition +``` + +The case statement is particularly useful when you have multiple conditions to check against a single value, or when you want to use pattern matching to destructure complex data structures. \ No newline at end of file From 44f350495214cbdba15b1f7e2db899fd6bee6d36 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Wed, 28 May 2025 22:17:55 -0400 Subject: [PATCH 02/25] Add 'break' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 +++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/break.md | 103 ++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 static_docs/break.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index a33415bd0b..79cddc6c79 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -7,6 +7,7 @@ class Hover include Requests::Support::Common ALLOWED_TARGETS = [ + Prism::BreakNode, Prism::CallNode, Prism::CaseNode, Prism::ConstantReadNode, @@ -55,6 +56,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so dispatcher.register( self, + :on_break_node_enter, :on_case_node_enter, :on_constant_read_node_enter, :on_constant_write_node_enter, @@ -99,6 +101,11 @@ def on_string_node_enter(node) generate_heredoc_hover(node) end + #: (Prism::BreakNode node) -> void + def on_break_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::CaseNode node) -> void def on_case_node_enter(node) handle_keyword_documentation(node.case_keyword) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 48ddced9f1..ad07014213 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -14,6 +14,7 @@ module RubyLsp # A map of keyword => short documentation to be displayed on hover or completion KEYWORD_DOCS = { + "break" => "Terminates the execution of a block, loop, or method", "case" => "Starts a case expression for pattern matching or multiple condition checking", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] diff --git a/static_docs/break.md b/static_docs/break.md new file mode 100644 index 0000000000..2cf9b75ecd --- /dev/null +++ b/static_docs/break.md @@ -0,0 +1,103 @@ +# Break + +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. + +```ruby +# Basic break usage in a loop +5.times do |i| + break if i == 3 + + puts i +end +# Output: +# 0 +# 1 +# 2 +``` + +The `break` statement can be used with any of Ruby's iteration methods or loops. + +```ruby +array = [1, 2, 3, 4, 5] + +# Break in each iteration +array.each do |num| + break if num > 3 + + puts "Number: #{num}" +end +# Output: +# Number: 1 +# Number: 2 +# Number: 3 + +# Break in an infinite loop +count = 0 +loop do + count += 1 + break if count >= 3 + + puts "Count: #{count}" +end +# Output: +# Count: 1 +# Count: 2 +``` + +## Break with a Value + +When used inside a block, `break` can return a value that becomes the result of the method call. + +```ruby +# Break with a return value in map +result = [1, 2, 3, 4, 5].map do |num| + break "Too large!" if num > 3 + + num * 2 +end +puts result # Output: "Too large!" + +# Break with a value in find +number = (1..10).find do |n| + break n if n > 5 && n.even? +end +puts number # Output: 6 +``` + +## Break in Nested Loops + +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. + +```ruby +# Break in nested iteration +(1..3).each do |i| + puts "Outer: #{i}" + + (1..3).each do |j| + break if j == 2 + + puts " Inner: #{j}" + end +end +# Output: +# Outer: 1 +# Inner: 1 +# Outer: 2 +# Inner: 1 +# Outer: 3 +# Inner: 1 + +# Breaking from nested loops with a flag +found = false +(1..3).each do |i| + (1..3).each do |j| + if i * j == 4 + found = true + break + end + end + break if found +end +``` + +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. \ No newline at end of file From 393ea3f85fa0ec4bfa52133b2fa2ea730ace4d4b Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Wed, 28 May 2025 22:25:40 -0400 Subject: [PATCH 03/25] Add 'class' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/class.md | 137 ++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 static_docs/class.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 79cddc6c79..d41a373755 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -10,6 +10,7 @@ class Hover Prism::BreakNode, Prism::CallNode, Prism::CaseNode, + Prism::ClassNode, Prism::ConstantReadNode, Prism::ConstantWriteNode, Prism::ConstantPathNode, @@ -58,6 +59,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so self, :on_break_node_enter, :on_case_node_enter, + :on_class_node_enter, :on_constant_read_node_enter, :on_constant_write_node_enter, :on_constant_path_node_enter, @@ -111,6 +113,11 @@ def on_case_node_enter(node) handle_keyword_documentation(node.case_keyword) end + #: (Prism::ClassNode node) -> void + def on_class_node_enter(node) + handle_keyword_documentation(node.class_keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index ad07014213..06f7be85dd 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -16,6 +16,7 @@ module RubyLsp KEYWORD_DOCS = { "break" => "Terminates the execution of a block, loop, or method", "case" => "Starts a case expression for pattern matching or multiple condition checking", + "class" => "Defines a class and its methods", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/class.md b/static_docs/class.md new file mode 100644 index 0000000000..f8e1dda43c --- /dev/null +++ b/static_docs/class.md @@ -0,0 +1,137 @@ +# Class + +In Ruby, a `class` is a blueprint for creating objects that share similar attributes and behaviors. Classes encapsulate data and methods, following object-oriented programming principles. + +```ruby +# Basic class definition +class Person + def initialize(name) + @name = name + end + + def greet + puts "Hello, #{@name}!" + end +end + +person = Person.new("Ruby") +person.greet +# Output: +# Hello, Ruby! +``` + +Classes can include instance methods, class methods, and various types of variables. + +```ruby +class Product + # Class variable (shared across all instances) + @@count = 0 + + # Class method + def self.count + @@count + end + + def initialize(name, price) + @name = name + @price = price + @@count += 1 + end + + # Instance method + def details + "#{@name}: $#{@price}" + end +end + +book = Product.new("Ruby Guide", 29.99) +puts Product.count # Output: 1 +puts book.details # Output: Ruby Guide: $29.99 +``` + +## Inheritance + +Classes can inherit behavior from other classes using the `<` operator. A class can only inherit from one parent class. + +```ruby +# Parent class +class Animal + def speak + "Some sound" + end +end + +# Child class +class Dog < Animal + def speak + "Woof!" + end +end + +dog = Dog.new +puts dog.speak # Output: Woof! +``` + +## Access Control + +Ruby provides three levels of method access control: `public`, `private`, and `protected`. + +```ruby +class BankAccount + def initialize(balance) + @balance = balance + end + + # Public method - can be called by anyone + def display_balance + "Current balance: $#{@balance}" + end + + # Protected method - can be called by other instances + protected + + def compare_balance(other) + @balance > other.balance + end + + # Private method - can only be called internally + private + + def update_balance(amount) + @balance += amount + end +end + +account = BankAccount.new(100) +puts account.display_balance +# Output: Current balance: $100 +``` + +## Class Instance Variables + +Instance variables can be exposed using attribute accessors. Ruby provides several methods to create them. + +```ruby +class User + # Create reader and writer methods + attr_accessor :name + + # Create reader only + attr_reader :created_at + + # Create writer only + attr_writer :password + + def initialize(name) + @name = name + @created_at = Time.now + end +end + +user = User.new("Alice") +puts user.name # Output: Alice +user.name = "Bob" +puts user.name # Output: Bob +``` + +The `class` keyword is fundamental to Ruby's object-oriented nature, allowing you to create organized, reusable, and maintainable code through encapsulation, inheritance, and polymorphism. From 7fb05ddd60b51466e23c11cb0dc5a3fb6798ac2d Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Wed, 28 May 2025 22:29:06 -0400 Subject: [PATCH 04/25] Add 'def' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/def.md | 111 ++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 static_docs/def.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index d41a373755..fd749c8981 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -14,6 +14,7 @@ class Hover Prism::ConstantReadNode, Prism::ConstantWriteNode, Prism::ConstantPathNode, + Prism::DefNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, @@ -64,6 +65,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_constant_write_node_enter, :on_constant_path_node_enter, :on_call_node_enter, + :on_def_node_enter, :on_global_variable_and_write_node_enter, :on_global_variable_operator_write_node_enter, :on_global_variable_or_write_node_enter, @@ -118,6 +120,11 @@ def on_class_node_enter(node) handle_keyword_documentation(node.class_keyword) end + #: (Prism::DefNode node) -> void + def on_def_node_enter(node) + handle_keyword_documentation(node.def_keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 06f7be85dd..c17155085a 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -17,6 +17,7 @@ module RubyLsp "break" => "Terminates the execution of a block, loop, or method", "case" => "Starts a case expression for pattern matching or multiple condition checking", "class" => "Defines a class and its methods", + "def" => "Defines a method", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/def.md b/static_docs/def.md new file mode 100644 index 0000000000..d8a08ccd26 --- /dev/null +++ b/static_docs/def.md @@ -0,0 +1,111 @@ +# Def + +In Ruby, the `def` keyword is used to define methods. Methods are reusable blocks of code that can accept parameters and return values. Every method implicitly returns the value of its last executed expression. + +```ruby +# Basic method definition +def greet(name) + "Hello, #{name}!" +end + +puts greet("Ruby") +# Output: +# Hello, Ruby! +``` + +Methods can be defined with different types of parameters, including optional and keyword arguments. + +```ruby +# Method with optional parameter +def calculate_total(amount, tax = 0.1) + amount + (amount * tax) +end + +puts calculate_total(100) # Output: 110.0 +puts calculate_total(100, 0.2) # Output: 120.0 + +# Method with keyword arguments +def create_user(name:, email:, role: "member") + "#{name} (#{email}) - #{role}" +end + +puts create_user(name: "Alice", email: "alice@example.com") +# Output: Alice (alice@example.com) - member +``` + +## Method Return Values + +Methods return the value of their last expression by default, but can use an explicit `return` statement to exit early. + +```ruby +def check_status(value) + return "Invalid" if value < 0 + + if value > 100 + "Too high" + else + "OK" + end +end + +puts check_status(-1) # Output: Invalid +puts check_status(50) # Output: OK +puts check_status(150) # Output: Too high +``` + +## Instance and Class Methods + +Methods can be defined at both the instance and class level. + +```ruby +class Timer + # Instance method - called on instances + def start + @time = Time.now + "Timer started" + end + + # Class method - called on the class itself + def self.now + Time.now.strftime("%H:%M:%S") + end +end + +timer = Timer.new +puts timer.start # Output: Timer started +puts Timer.now # Output: 14:30:45 +``` + +## Method Visibility + +Methods can have different visibility levels using `private`, `protected`, or `public` (default). + +```ruby +class BankAccount + def initialize(balance) + @balance = balance + end + + def withdraw(amount) + return "Insufficient funds" unless sufficient_funds?(amount) + + process_withdrawal(amount) + "Withdrawn: $#{amount}" + end + + private + + def sufficient_funds?(amount) + @balance >= amount + end + + def process_withdrawal(amount) + @balance -= amount + end +end + +account = BankAccount.new(100) +puts account.withdraw(50) # Output: Withdrawn: $50 +``` + +The `def` keyword is essential for organizing code into reusable, maintainable methods that form the building blocks of Ruby programs. \ No newline at end of file From f0b80b7675c1dc9233d588cd06d8fbacb5c03ed0 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Wed, 28 May 2025 22:32:58 -0400 Subject: [PATCH 05/25] Add 'defined?' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 +++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/defined?.md | 100 ++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 static_docs/defined?.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index fd749c8981..d0d20d15ad 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -15,6 +15,7 @@ class Hover Prism::ConstantWriteNode, Prism::ConstantPathNode, Prism::DefNode, + Prism::DefinedNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, @@ -66,6 +67,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_constant_path_node_enter, :on_call_node_enter, :on_def_node_enter, + :on_defined_node_enter, :on_global_variable_and_write_node_enter, :on_global_variable_operator_write_node_enter, :on_global_variable_or_write_node_enter, @@ -125,6 +127,11 @@ def on_def_node_enter(node) handle_keyword_documentation(node.def_keyword) end + #: (Prism::DefinedNode node) -> void + def on_defined_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index c17155085a..6d433a66b4 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -18,6 +18,7 @@ module RubyLsp "case" => "Starts a case expression for pattern matching or multiple condition checking", "class" => "Defines a class and its methods", "def" => "Defines a method", + "defined?" => "Checks if a constant or method is defined", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/defined?.md b/static_docs/defined?.md new file mode 100644 index 0000000000..2a59b67238 --- /dev/null +++ b/static_docs/defined?.md @@ -0,0 +1,100 @@ +# Defined? + +In Ruby, the `defined?` keyword is a special operator that checks whether a given expression is defined and returns a description of that expression, or `nil` if the expression is not defined. + +```ruby +# Basic defined? usage +x = 42 +puts defined?(x) # Output: local-variable +puts defined?(y) # Output: nil +puts defined?(puts) # Output: method +``` + +The `defined?` operator can check various types of expressions and returns different description strings based on the type. + +```ruby +# Checking different types +class Example + CONSTANT = "Hello" + @@class_var = "World" + + def check_definitions + @instance_var = "!" + + puts defined?(CONSTANT) # Output: constant + puts defined?(@@class_var) # Output: class variable + puts defined?(@instance_var) # Output: instance-variable + puts defined?(yield) # Output: yield (if block given) + puts defined?(super) # Output: super (if method has super) + end +end + +example = Example.new +puts defined?(Example) # Output: constant +puts defined?(String) # Output: constant +puts defined?("string") # Output: expression +``` + +## Common Use Cases + +The `defined?` operator is often used for safe navigation and checking existence before execution. + +```ruby +def safe_operation(value) + return "No block given" unless defined?(yield) + + if defined?(value.length) + "Length is #{value.length}" + else + "Cannot determine length" + end +end + +puts safe_operation([1, 2, 3]) { |x| x * 2 } # Output: Length is 3 +puts safe_operation(42) { |x| x * 2 } # Output: Cannot determine length +puts safe_operation([1, 2, 3]) # Output: No block given +``` + +## Method and Block Checking + +`defined?` is particularly useful for checking method existence and block presence. + +```ruby +class SafeCaller + def execute + if defined?(before_execute) + before_execute + end + + puts "Executing main logic" + + if defined?(after_execute) + after_execute + end + end + + def after_execute + puts "After execution" + end +end + +caller = SafeCaller.new +caller.execute +# Output: +# Executing main logic +# After execution + +# Block checking +def process_with_block + if defined?(yield) + "Block given: #{yield}" + else + "No block given" + end +end + +puts process_with_block { "Hello!" } # Output: Block given: Hello! +puts process_with_block # Output: No block given +``` + +The `defined?` operator is a powerful tool for writing defensive code and handling optional features or dependencies in Ruby programs. \ No newline at end of file From a7961b88c39eba4699abdc779c21dcb0b907004e Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Wed, 28 May 2025 22:38:43 -0400 Subject: [PATCH 06/25] Add 'else' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 +++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/else.md | 106 ++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 static_docs/else.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index d0d20d15ad..32ee2e4305 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -16,6 +16,7 @@ class Hover Prism::ConstantPathNode, Prism::DefNode, Prism::DefinedNode, + Prism::ElseNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, @@ -68,6 +69,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_call_node_enter, :on_def_node_enter, :on_defined_node_enter, + :on_else_node_enter, :on_global_variable_and_write_node_enter, :on_global_variable_operator_write_node_enter, :on_global_variable_or_write_node_enter, @@ -132,6 +134,11 @@ def on_defined_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::ElseNode node) -> void + def on_else_node_enter(node) + handle_keyword_documentation(node.else_keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 6d433a66b4..8a60260e7b 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -19,6 +19,7 @@ module RubyLsp "class" => "Defines a class and its methods", "def" => "Defines a method", "defined?" => "Checks if a constant or method is defined", + "else" => "Executes the code in the else block if the condition is false", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/else.md b/static_docs/else.md new file mode 100644 index 0000000000..0aebb20f1c --- /dev/null +++ b/static_docs/else.md @@ -0,0 +1,106 @@ +# Else + +In Ruby, the `else` keyword is used to define an alternative execution path in conditional statements. It works with `if`, `unless`, `case`, and `begin/rescue` blocks to handle cases when the primary conditions are not met. + +```ruby +# Basic else usage with if +status = "error" + +if status == "success" + puts "Operation completed" +else + puts "Operation failed" +end +# Output: +# Operation failed +``` + +The `else` clause can be used with various conditional structures in Ruby. + +```ruby +# With unless +temperature = 25 + +unless temperature < 20 + puts "It's warm" +else + puts "It's cool" +end +# Output: +# It's warm + +# With case statement +grade = "B" + +case grade +when "A" + puts "Excellent!" +when "B" + puts "Good job!" +else + puts "Keep working hard!" +end +# Output: +# Good job! +``` + +## Error Handling + +The `else` keyword is commonly used with `begin/rescue` blocks for error handling. + +```ruby +begin + result = 10 / 0 +rescue ZeroDivisionError + puts "Cannot divide by zero" +else + # Executes only if no error was raised + puts "Result: #{result}" +ensure + puts "Calculation attempted" +end +# Output: +# Cannot divide by zero +# Calculation attempted +``` + +## Ternary Operator Alternative + +For simple conditions, Ruby provides a ternary operator as a concise alternative to `if/else`. + +```ruby +def check_temperature(temp) + temp >= 25 ? "It's hot" : "It's cool" +end + +puts check_temperature(30) # Output: It's hot +puts check_temperature(20) # Output: It's cool + +# Compared to if/else +def check_temperature_verbose(temp) + if temp >= 25 + "It's hot" + else + "It's cool" + end +end +``` + +## Method Return Values + +The `else` clause affects the return value in conditional expressions. + +```ruby +def process_number(num) + if num.even? + "Even number: #{num}" + else + "Odd number: #{num}" + end +end + +puts process_number(42) # Output: Even number: 42 +puts process_number(37) # Output: Odd number: 37 +``` + +The `else` keyword is fundamental to control flow in Ruby, providing clear paths for alternate execution when conditions are not met. \ No newline at end of file From 4fbf3ffdf43b4397cc2f40d83771fd6cd1237a99 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Wed, 28 May 2025 22:41:10 -0400 Subject: [PATCH 07/25] Rename 'defined?' to 'defined' --- lib/ruby_lsp/static_docs.rb | 2 +- static_docs/{defined?.md => defined.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename static_docs/{defined?.md => defined.md} (100%) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 8a60260e7b..93c88d3b24 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -18,7 +18,7 @@ module RubyLsp "case" => "Starts a case expression for pattern matching or multiple condition checking", "class" => "Defines a class and its methods", "def" => "Defines a method", - "defined?" => "Checks if a constant or method is defined", + "defined" => "Checks if a constant or method is defined", "else" => "Executes the code in the else block if the condition is false", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] diff --git a/static_docs/defined?.md b/static_docs/defined.md similarity index 100% rename from static_docs/defined?.md rename to static_docs/defined.md From bf83dcd7df131c2387dbd41f3aa0c72801ae5b45 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 10:51:13 -0400 Subject: [PATCH 08/25] Add test coverage --- static_docs/case.md | 2 - static_docs/class.md | 6 +- static_docs/def.md | 12 ++-- static_docs/defined.md | 8 +-- static_docs/else.md | 4 +- test/requests/hover_expectations_test.rb | 70 ++++++++++++++++++------ 6 files changed, 68 insertions(+), 34 deletions(-) diff --git a/static_docs/case.md b/static_docs/case.md index 19494877c7..833e45ae75 100644 --- a/static_docs/case.md +++ b/static_docs/case.md @@ -84,7 +84,6 @@ Ruby also allows `case` statements without an explicit argument, which acts like ```ruby # Case statement without an argument -# rubocop:disable Style/EmptyCaseCondition case when Time.now.saturday? puts "It's Saturday!" @@ -93,7 +92,6 @@ when Time.now.sunday? else puts "It's a weekday" end -# rubocop:enable Style/EmptyCaseCondition ``` The case statement is particularly useful when you have multiple conditions to check against a single value, or when you want to use pattern matching to destructure complex data structures. \ No newline at end of file diff --git a/static_docs/class.md b/static_docs/class.md index f8e1dda43c..3dd008e36d 100644 --- a/static_docs/class.md +++ b/static_docs/class.md @@ -28,8 +28,10 @@ class Product @@count = 0 # Class method - def self.count - @@count + class << self + def count + @@count + end end def initialize(name, price) diff --git a/static_docs/def.md b/static_docs/def.md index d8a08ccd26..b5b6de3295 100644 --- a/static_docs/def.md +++ b/static_docs/def.md @@ -59,16 +59,18 @@ Methods can be defined at both the instance and class level. ```ruby class Timer + # Class method - called on the class itself + class << self + def now + Time.now.strftime("%H:%M:%S") + end + end + # Instance method - called on instances def start @time = Time.now "Timer started" end - - # Class method - called on the class itself - def self.now - Time.now.strftime("%H:%M:%S") - end end timer = Timer.new diff --git a/static_docs/defined.md b/static_docs/defined.md index 2a59b67238..e7a1f6931d 100644 --- a/static_docs/defined.md +++ b/static_docs/defined.md @@ -16,16 +16,14 @@ The `defined?` operator can check various types of expressions and returns diffe # Checking different types class Example CONSTANT = "Hello" - @@class_var = "World" def check_definitions @instance_var = "!" - puts defined?(CONSTANT) # Output: constant - puts defined?(@@class_var) # Output: class variable + puts defined?(CONSTANT) # Output: constant puts defined?(@instance_var) # Output: instance-variable - puts defined?(yield) # Output: yield (if block given) - puts defined?(super) # Output: super (if method has super) + puts defined?(yield) # Output: yield (if block given) + puts defined?(super) # Output: super (if method has super) end end diff --git a/static_docs/else.md b/static_docs/else.md index 0aebb20f1c..73bc174871 100644 --- a/static_docs/else.md +++ b/static_docs/else.md @@ -18,10 +18,10 @@ end The `else` clause can be used with various conditional structures in Ruby. ```ruby -# With unless +# With if (positive condition) temperature = 25 -unless temperature < 20 +if temperature >= 20 puts "It's warm" else puts "It's cool" diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index f709ceff36..b25b7b99ac 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -929,26 +929,60 @@ def name; end end def test_hover_for_keywords - source = <<~RUBY - def foo - yield - end - RUBY + test_cases = { + "yield" => { + source: <<~RUBY, + def foo + yield + end + RUBY + position: { line: 1, character: 2 }, + }, + "class" => { + source: <<~RUBY, + class MyClass + end + RUBY + position: { line: 0, character: 2 }, + }, + "def" => { + source: <<~RUBY, + def my_method + end + RUBY + position: { line: 0, character: 2 }, + }, + "else" => { + source: <<~RUBY, + if condition + true + else + false + end + RUBY + position: { line: 2, character: 2 }, + }, + } - with_server(source) do |server, uri| - server.process_message( - id: 1, - method: "textDocument/hover", - params: { textDocument: { uri: uri }, position: { character: 2, line: 1 } }, - ) + test_cases.each do |keyword, config| + with_server(config[:source]) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/hover", + params: { + textDocument: { uri: uri }, + position: config[:position], + }, + ) - contents = server.pop_response.response.contents.value - assert_match("```ruby\nyield\n```", contents) - assert_match( - RubyLsp::KEYWORD_DOCS["yield"], #: as !nil - contents, - ) - assert_match("[Read more](#{RubyLsp::STATIC_DOCS_PATH}/yield.md)", contents) + contents = server.pop_response.response.contents.value + assert_match("```ruby\n#{keyword}\n```", contents) + assert_match( + RubyLsp::KEYWORD_DOCS[keyword] || "No documentation found for #{keyword}", + contents, + ) + assert_match("[Read more](#{RubyLsp::STATIC_DOCS_PATH}/#{keyword}.md)", contents) + end end end From ab52713259e9ebe512ff19f370c69845e5b43260 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 11:28:28 -0400 Subject: [PATCH 09/25] Add 'ensure' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/ensure.md | 83 ++++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 10 +++ 4 files changed, 101 insertions(+) create mode 100644 static_docs/ensure.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 32ee2e4305..87b2742145 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -17,6 +17,7 @@ class Hover Prism::DefNode, Prism::DefinedNode, Prism::ElseNode, + Prism::EnsureNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, @@ -70,6 +71,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_def_node_enter, :on_defined_node_enter, :on_else_node_enter, + :on_ensure_node_enter, :on_global_variable_and_write_node_enter, :on_global_variable_operator_write_node_enter, :on_global_variable_or_write_node_enter, @@ -139,6 +141,11 @@ def on_else_node_enter(node) handle_keyword_documentation(node.else_keyword) end + #: (Prism::EnsureNode node) -> void + def on_ensure_node_enter(node) + handle_keyword_documentation(node.ensure_keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 93c88d3b24..9f253694f3 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -20,6 +20,7 @@ module RubyLsp "def" => "Defines a method", "defined" => "Checks if a constant or method is defined", "else" => "Executes the code in the else block if the condition is false", + "ensure" => "Executes the code in the ensure block regardless of whether an exception is raised or not", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/ensure.md b/static_docs/ensure.md new file mode 100644 index 0000000000..f51f58c907 --- /dev/null +++ b/static_docs/ensure.md @@ -0,0 +1,83 @@ +# Ensure + +In Ruby, the `ensure` keyword is used to define a block of code that will always execute, regardless of whether an exception was raised or not. It's commonly used for cleanup operations like closing files or network connections. + +```ruby +# Basic ensure usage +file = File.open("example.txt") +begin + content = file.read +rescue StandardError => e + puts "Error reading file: #{e.message}" +ensure + file.close # Always executes +end +``` + +The `ensure` clause can be used with or without `rescue` blocks, and it will execute even if there's a return statement in the main block. + +```ruby +def process_data + connection = Database.connect + begin + return connection.query("SELECT * FROM users") + ensure + connection.close # Executes even with the return statement + end +end + +# Without rescue clause +def write_log(message) + file = File.open("log.txt", "a") + begin + file.puts(message) + ensure + file.close + end +end +``` + +## Multiple Rescue Clauses + +When using multiple `rescue` clauses, the `ensure` block always comes last and executes regardless of which `rescue` clause is triggered. + +```ruby +def perform_operation + begin + # Main operation + result = dangerous_operation + rescue ArgumentError => e + puts "Invalid arguments: #{e.message}" + rescue StandardError => e + puts "Other error: #{e.message}" + ensure + # Cleanup code always runs + cleanup_resources + end +end +``` + +## Implicit Begin Blocks + +In methods and class definitions, you can use `ensure` without an explicit `begin` block. + +```ruby +def process_file(path) + file = File.open(path) + file.read # If this raises an error, ensure still executes +ensure + file&.close # Using safe navigation operator in case file is nil +end + +class DataProcessor + def initialize + @connection = Database.connect + rescue StandardError => e + puts "Failed to connect: #{e.message}" + ensure + puts "Initialization complete" + end +end +``` + +The `ensure` keyword is essential for writing robust Ruby code that properly manages resources and handles cleanup operations, regardless of whether exceptions occur. \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index b25b7b99ac..1334387f80 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -962,6 +962,16 @@ def my_method RUBY position: { line: 2, character: 2 }, }, + "ensure" => { + source: <<~RUBY, + begin + true + ensure + false + end + RUBY + position: { line: 2, character: 2 }, + }, } test_cases.each do |keyword, config| From d55485076e3d2986ad12d83d8893274b2ef9ac2a Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 12:56:31 -0400 Subject: [PATCH 10/25] Clarify language --- lib/ruby_lsp/static_docs.rb | 6 +++--- static_docs/case.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 9f253694f3..6068041b9b 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -14,11 +14,11 @@ module RubyLsp # A map of keyword => short documentation to be displayed on hover or completion KEYWORD_DOCS = { - "break" => "Terminates the execution of a block, loop, or method", + "break" => "Terminates the execution of a block or loop", "case" => "Starts a case expression for pattern matching or multiple condition checking", - "class" => "Defines a class and its methods", + "class" => "Defines a class", "def" => "Defines a method", - "defined" => "Checks if a constant or method is defined", + "defined" => "Checks if a constant, variable or method is defined", "else" => "Executes the code in the else block if the condition is false", "ensure" => "Executes the code in the ensure block regardless of whether an exception is raised or not", "yield" => "Invokes the passed block with the given arguments", diff --git a/static_docs/case.md b/static_docs/case.md index 833e45ae75..8bb1001cc0 100644 --- a/static_docs/case.md +++ b/static_docs/case.md @@ -46,9 +46,9 @@ else end ``` -## Pattern Matching (Ruby 2.7+) +## Pattern Matching -Starting from Ruby 2.7, `case` statements support pattern matching, which provides powerful ways to match and destructure data. +`case` statements support pattern matching, which provides powerful ways to match and destructure data. ```ruby # Pattern matching with arrays From ae11a22cfc87493b786a2569354c344bc7b6ed14 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 13:32:06 -0400 Subject: [PATCH 11/25] Revise docs to be more focused --- static_docs/case.md | 45 +++++++++++++++ static_docs/class.md | 132 +++++++++++++++---------------------------- 2 files changed, 92 insertions(+), 85 deletions(-) diff --git a/static_docs/case.md b/static_docs/case.md index 8bb1001cc0..f5e3482d7f 100644 --- a/static_docs/case.md +++ b/static_docs/case.md @@ -46,6 +46,51 @@ else end ``` +## Type and Module Checking + +`case` statements are commonly used to check an object's type or included modules, leveraging Ruby's `===` operator. + +```ruby +# Checking inheritance and module inclusion +module Printable + def print_info + puts "[INFO] #{to_s}" + end +end + +class Report + include Printable +end + +class User +end + +object = Report.new + +case object +when Printable + object.print_info +when User + puts "Found a user" +else + puts "Unknown object type" +end +# Output: [INFO] # + +# Multiple type checks +result = 42 + +case result +when String + puts "Got a string: #{result}" +when Integer + puts "Got a number: #{result}" +when Array + puts "Got an array with #{result.length} items" +end +# Output: Got a number: 42 +``` + ## Pattern Matching `case` statements support pattern matching, which provides powerful ways to match and destructure data. diff --git a/static_docs/class.md b/static_docs/class.md index 3dd008e36d..09562d0633 100644 --- a/static_docs/class.md +++ b/static_docs/class.md @@ -1,15 +1,15 @@ # Class -In Ruby, a `class` is a blueprint for creating objects that share similar attributes and behaviors. Classes encapsulate data and methods, following object-oriented programming principles. +In Ruby, a `class` is a blueprint for creating objects that encapsulate related state and behavior. Each instance of a class has its own set of instance variables and methods, allowing objects to maintain their individual state. ```ruby # Basic class definition class Person def initialize(name) - @name = name + @name = name # Instance variable stores state end - def greet + def greet # Instance method stores behavior puts "Hello, #{@name}!" end end @@ -20,63 +20,9 @@ person.greet # Hello, Ruby! ``` -Classes can include instance methods, class methods, and various types of variables. - -```ruby -class Product - # Class variable (shared across all instances) - @@count = 0 - - # Class method - class << self - def count - @@count - end - end +## Instance Variables and Methods - def initialize(name, price) - @name = name - @price = price - @@count += 1 - end - - # Instance method - def details - "#{@name}: $#{@price}" - end -end - -book = Product.new("Ruby Guide", 29.99) -puts Product.count # Output: 1 -puts book.details # Output: Ruby Guide: $29.99 -``` - -## Inheritance - -Classes can inherit behavior from other classes using the `<` operator. A class can only inherit from one parent class. - -```ruby -# Parent class -class Animal - def speak - "Some sound" - end -end - -# Child class -class Dog < Animal - def speak - "Woof!" - end -end - -dog = Dog.new -puts dog.speak # Output: Woof! -``` - -## Access Control - -Ruby provides three levels of method access control: `public`, `private`, and `protected`. +Instance variables (starting with `@`) store object-specific state, while instance methods define the behavior that each object can perform. ```ruby class BankAccount @@ -84,44 +30,33 @@ class BankAccount @balance = balance end - # Public method - can be called by anyone - def display_balance - "Current balance: $#{@balance}" - end - - # Protected method - can be called by other instances - protected - - def compare_balance(other) - @balance > other.balance + def deposit(amount) + @balance += amount end - # Private method - can only be called internally - private - - def update_balance(amount) - @balance += amount + def current_balance + @balance end end account = BankAccount.new(100) -puts account.display_balance -# Output: Current balance: $100 +account.deposit(50) +puts account.current_balance # Output: 150 ``` -## Class Instance Variables +## Attribute Accessors -Instance variables can be exposed using attribute accessors. Ruby provides several methods to create them. +Ruby provides convenient methods to create getters and setters for instance variables: ```ruby class User - # Create reader and writer methods + # Creates both getter and setter methods attr_accessor :name - # Create reader only + # Creates getter method only attr_reader :created_at - # Create writer only + # Creates setter method only attr_writer :password def initialize(name) @@ -131,9 +66,36 @@ class User end user = User.new("Alice") -puts user.name # Output: Alice -user.name = "Bob" -puts user.name # Output: Bob +puts user.name # Using getter (Output: Alice) +user.name = "Bob" # Using setter +puts user.name # Output: Bob +puts user.created_at # Using reader +user.password = "123" # Using writer +``` + +## Inheritance + +Classes can inherit behavior from other classes using the `<` operator, allowing for code reuse and specialization. + +```ruby +class Animal + def initialize(name) + @name = name + end + + def speak + "Some sound" + end +end + +class Dog < Animal + def speak + "#{@name} says: Woof!" + end +end + +dog = Dog.new("Rex") +puts dog.speak # Output: Rex says: Woof! ``` -The `class` keyword is fundamental to Ruby's object-oriented nature, allowing you to create organized, reusable, and maintainable code through encapsulation, inheritance, and polymorphism. +The `class` keyword is fundamental to Ruby's object-oriented nature, allowing you to create organized, reusable code by grouping related data and behavior into objects. From 73241573f7578268b16856c3d0c93ee3f494b798 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 13:55:11 -0400 Subject: [PATCH 12/25] Add 'for' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/for.md | 97 ++++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 7 ++ 4 files changed, 112 insertions(+) create mode 100644 static_docs/for.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 87b2742145..5e62583dbf 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -18,6 +18,7 @@ class Hover Prism::DefinedNode, Prism::ElseNode, Prism::EnsureNode, + Prism::ForNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, @@ -72,6 +73,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_defined_node_enter, :on_else_node_enter, :on_ensure_node_enter, + :on_for_node_enter, :on_global_variable_and_write_node_enter, :on_global_variable_operator_write_node_enter, :on_global_variable_or_write_node_enter, @@ -146,6 +148,11 @@ def on_ensure_node_enter(node) handle_keyword_documentation(node.ensure_keyword) end + #: (Prism::ForNode node) -> void + def on_for_node_enter(node) + handle_keyword_documentation(node.for_keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 6068041b9b..c6144145e1 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -21,6 +21,7 @@ module RubyLsp "defined" => "Checks if a constant, variable or method is defined", "else" => "Executes the code in the else block if the condition is false", "ensure" => "Executes the code in the ensure block regardless of whether an exception is raised or not", + "for" => "Iterates over a collection of elements", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/for.md b/static_docs/for.md new file mode 100644 index 0000000000..00f508d7b9 --- /dev/null +++ b/static_docs/for.md @@ -0,0 +1,97 @@ +# For + +In Ruby, the `for` keyword creates a loop that iterates over a collection. While functional, Rubyists typically prefer using iterators like `each` for better readability and block scoping. + +```ruby +# Basic for loop with a range +for i in 1..3 + puts i +end +# Output: +# 1 +# 2 +# 3 +``` + +The `for` loop can iterate over any object that responds to `each`, including arrays and hashes. + +```ruby +# Iterating over an array +fruits = ["apple", "banana", "orange"] + +for fruit in fruits + puts "I like #{fruit}" +end +# Output: +# I like apple +# I like banana +# I like orange + +# Iterating over a hash +scores = { alice: 95, bob: 87 } + +for name, score in scores + puts "#{name} scored #{score}" +end +# Output: +# alice scored 95 +# bob scored 87 +``` + +## Variable Scope + +Unlike block-based iterators, variables defined in a `for` loop remain accessible after the loop ends. + +```ruby +# Variable remains in scope +for value in [1, 2, 3] + doubled = value * 2 +end + +puts doubled # Output: 6 (last value) + +# Comparison with each (creates new scope) +[1, 2, 3].each do |value| + doubled = value * 2 +end + +# puts doubled # Would raise NameError +``` + +## Breaking and Next + +The `for` loop supports control flow keywords like `break` and `next`. + +```ruby +# Using break to exit early +for number in 1..5 + break if number > 3 + puts number +end +# Output: +# 1 +# 2 +# 3 + +# Using next to skip iterations +for number in 1..5 + next if number.even? + puts number +end +# Output: +# 1 +# 3 +# 5 +``` + +While Ruby provides the `for` loop for compatibility and familiarity, the preferred Ruby way is to use iterators with blocks: + +```ruby +# Preferred Ruby style using each +(1..3).each { |i| puts i } + +# For more complex iterations +(1..3).each do |i| + puts i +end +``` \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index 1334387f80..4ba5431211 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -972,6 +972,13 @@ def my_method RUBY position: { line: 2, character: 2 }, }, + "for" => { + source: <<~RUBY, + for i in 1..10 + end + RUBY + position: { line: 0, character: 2 }, + }, } test_cases.each do |keyword, config| From 847ba97e139249bbe056c67cab5f933929d15fbc Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 14:29:38 -0400 Subject: [PATCH 13/25] Add 'module' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 +++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/module.md | 104 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 static_docs/module.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 5e62583dbf..a95288f6e0 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -31,6 +31,7 @@ class Hover Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode, Prism::InstanceVariableWriteNode, + Prism::ModuleNode, Prism::SymbolNode, Prism::StringNode, Prism::InterpolatedStringNode, @@ -86,6 +87,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_instance_variable_operator_write_node_enter, :on_instance_variable_or_write_node_enter, :on_instance_variable_target_node_enter, + :on_module_node_enter, :on_super_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, @@ -153,6 +155,11 @@ def on_for_node_enter(node) handle_keyword_documentation(node.for_keyword) end + #: (Prism::ModuleNode node) -> void + def on_module_node_enter(node) + handle_keyword_documentation(node.module_keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index c6144145e1..cdde56b475 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -22,6 +22,7 @@ module RubyLsp "else" => "Executes the code in the else block if the condition is false", "ensure" => "Executes the code in the ensure block regardless of whether an exception is raised or not", "for" => "Iterates over a collection of elements", + "module" => "Defines a module", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/module.md b/static_docs/module.md new file mode 100644 index 0000000000..e0a128ffc6 --- /dev/null +++ b/static_docs/module.md @@ -0,0 +1,104 @@ +# Module + +In Ruby, the `module` keyword creates a container for methods and constants. Modules serve two primary purposes: namespacing related code and providing reusable behavior through mixins. + +```ruby +# Basic module definition +module Formatter + def self.titleize(text) + text.split.map(&:capitalize).join(" ") + end +end + +puts Formatter.titleize("hello world") +# Output: +# Hello World +``` + +Modules can be included in classes to share behavior through mixins, allowing for code reuse without inheritance. + +```ruby +# Module as a mixin +module Printable + def print_details + puts "Name: #{name}" + puts "ID: #{id}" + end +end + +class Product + include Printable + attr_reader :name, :id + + def initialize(name, id) + @name = name + @id = id + end +end + +book = Product.new("Ruby Guide", "B123") +book.print_details +# Output: +# Name: Ruby Guide +# ID: B123 +``` + +## Namespacing + +Modules help organize code by grouping related classes and methods under a namespace. + +```ruby +module Shop + class Product + def initialize(name) + @name = name + end + end + + class Order + def initialize(product) + @product = product + end + end +end + +# Using namespaced classes +product = Shop::Product.new("Coffee") +order = Shop::Order.new(product) +``` + +## Multiple Includes + +A class can include multiple modules to compose different behaviors. + +```ruby +module Validatable + def valid? + !name.nil? && !id.nil? + end +end + +module Displayable + def display + "#{name} (#{id})" + end +end + +class Item + include Validatable + include Displayable + + attr_reader :name, :id + + def initialize(name, id) + @name = name + @id = id + end +end + +item = Item.new("Laptop", "L456") +puts item.valid? # Output: true +puts item.display # Output: Laptop (L456) +``` + +The `module` keyword is essential for organizing code and implementing Ruby's version of multiple inheritance through mixins. \ No newline at end of file From 383cd7213297952c8ec711d699feeacd57bb3db1 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 14:49:06 -0400 Subject: [PATCH 14/25] Add 'next' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 +++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/next.md | 84 +++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 static_docs/next.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index a95288f6e0..551669dd8d 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -32,6 +32,7 @@ class Hover Prism::InstanceVariableTargetNode, Prism::InstanceVariableWriteNode, Prism::ModuleNode, + Prism::NextNode, Prism::SymbolNode, Prism::StringNode, Prism::InterpolatedStringNode, @@ -88,6 +89,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_instance_variable_or_write_node_enter, :on_instance_variable_target_node_enter, :on_module_node_enter, + :on_next_node_enter, :on_super_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, @@ -160,6 +162,11 @@ def on_module_node_enter(node) handle_keyword_documentation(node.module_keyword) end + #: (Prism::NextNode node) -> void + def on_next_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index cdde56b475..05b23a38db 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -23,6 +23,7 @@ module RubyLsp "ensure" => "Executes the code in the ensure block regardless of whether an exception is raised or not", "for" => "Iterates over a collection of elements", "module" => "Defines a module", + "next" => "Skips to the next iteration of a loop", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/next.md b/static_docs/next.md new file mode 100644 index 0000000000..abab0ec44e --- /dev/null +++ b/static_docs/next.md @@ -0,0 +1,84 @@ +# Next + +In Ruby, the `next` keyword is used to skip the rest of the current iteration and move to the next iteration of a loop or block. It's similar to `continue` in other programming languages. + +```ruby +# Basic next usage in a loop +["README.md", ".git", "lib", ".gitignore"].each do |path| + next if path.start_with?(".") + puts "Processing: #{path}" +end +# Output: +# Processing: README.md +# Processing: lib +``` + +The `next` statement can be used with any of Ruby's iteration methods or blocks. + +```ruby +# Using next with different iterators +users = [ + { name: "Alice", active: true }, + { name: "Bob", active: false }, + { name: "Carol", active: true } +] + +# With each +users.each do |user| + next unless user[:active] + puts "Notifying #{user[:name]}" +end +# Output: +# Notifying Alice +# Notifying Carol + +# With map +messages = users.map do |user| + next "Account inactive" unless user[:active] + "Welcome back, #{user[:name]}!" +end +puts messages.inspect +# Output: +# ["Welcome back, Alice!", "Account inactive", "Welcome back, Carol!"] +``` + +## Conditional Next + +The `next` keyword is often used with conditions to create more complex iteration logic. + +```ruby +# Processing specific elements +orders = [ + { id: 1, status: "paid" }, + { id: 2, status: "pending" }, + { id: 3, status: "cancelled" }, + { id: 4, status: "paid" } +] + +orders.each do |order| + # Skip non-paid orders + next unless order[:status] == "paid" + puts "Processing payment for order #{order[:id]}" +end +# Output: +# Processing payment for order 1 +# Processing payment for order 4 + +# Processing with multiple conditions +products = [ + { name: "Book", price: 15, in_stock: true }, + { name: "Shirt", price: 25, in_stock: false }, + { name: "Hat", price: 12, in_stock: true } +] + +products.each do |product| + next unless product[:in_stock] # Skip out of stock items + next if product[:price] > 20 # Skip expensive items + puts "Featured item: #{product[:name]} at $#{product[:price]}" +end +# Output: +# Featured item: Book at $15 +# Featured item: Hat at $12 +``` + +The `next` keyword helps control the flow of iterations, allowing you to skip unwanted elements or conditions while continuing the loop. \ No newline at end of file From f84f4a684e576a39bd3e1993c77c32431adb3145 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 14:59:02 -0400 Subject: [PATCH 15/25] Add test coverage for 'module' and 'next' --- lib/ruby_lsp/static_docs.rb | 2 +- test/requests/hover_expectations_test.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 05b23a38db..61818ae7ef 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -23,7 +23,7 @@ module RubyLsp "ensure" => "Executes the code in the ensure block regardless of whether an exception is raised or not", "for" => "Iterates over a collection of elements", "module" => "Defines a module", - "next" => "Skips to the next iteration of a loop", + "next" => "Skips the rest of the current iteration and moves to the next iteration of a loop or block", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index 4ba5431211..6271d24fed 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -979,6 +979,21 @@ def my_method RUBY position: { line: 0, character: 2 }, }, + "module" => { + source: <<~RUBY, + module MyModule + end + RUBY + position: { line: 0, character: 2 }, + }, + "next" => { + source: <<~RUBY, + for i in 1..10 + next + end + RUBY + position: { line: 1, character: 2 }, + }, } test_cases.each do |keyword, config| From e189737ddcf4e1782d6e035e835e313f4cd8faf8 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 15:11:57 -0400 Subject: [PATCH 16/25] Add 'rescue' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/rescue.md | 93 ++++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 10 +++ 4 files changed, 111 insertions(+) create mode 100644 static_docs/rescue.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 551669dd8d..cb12563460 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -33,6 +33,7 @@ class Hover Prism::InstanceVariableWriteNode, Prism::ModuleNode, Prism::NextNode, + Prism::RescueNode, Prism::SymbolNode, Prism::StringNode, Prism::InterpolatedStringNode, @@ -90,6 +91,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_instance_variable_target_node_enter, :on_module_node_enter, :on_next_node_enter, + :on_rescue_node_enter, :on_super_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, @@ -167,6 +169,11 @@ def on_next_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::RescueNode node) -> void + def on_rescue_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 61818ae7ef..9f0c65f089 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -24,6 +24,7 @@ module RubyLsp "for" => "Iterates over a collection of elements", "module" => "Defines a module", "next" => "Skips the rest of the current iteration and moves to the next iteration of a loop or block", + "rescue" => "Handles exceptions that occur in the code block", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/rescue.md b/static_docs/rescue.md new file mode 100644 index 0000000000..71ad00143a --- /dev/null +++ b/static_docs/rescue.md @@ -0,0 +1,93 @@ +# Rescue + +In Ruby, `rescue` is used to handle exceptions that occur during program execution. It allows you to catch and handle errors gracefully, preventing your program from crashing. + +```ruby +# Basic rescue usage +begin + # Code that might raise an exception + result = 10 / 0 +rescue + puts "An error occurred!" +end +``` + +You can specify which type of exception to rescue, and capture the exception object for inspection: + +```ruby +begin + # Attempting to divide by zero raises a ZeroDivisionError + result = 10 / 0 +rescue ZeroDivisionError => e + puts "Cannot divide by zero: #{e.message}" +end +``` + +Multiple rescue clauses can be used to handle different types of exceptions: + +```ruby +begin + # Code that might raise different types of exceptions + JSON.parse(invalid_json) +rescue JSON::ParserError => e + puts "Invalid JSON format: #{e.message}" +rescue StandardError => e + puts "Some other error occurred: #{e.message}" +end +``` + +## Inline rescue + +Ruby also supports inline rescue clauses for simple error handling: + +```ruby +# If the division fails, return nil instead +result = 10 / params[:divisor].to_i rescue nil + +# This is equivalent to: +result = begin + 10 / params[:divisor].to_i +rescue + nil +end +``` + +## Ensure and else clauses + +The `rescue` keyword can be used with `ensure` and `else` clauses: + +```ruby +begin + # Attempt some operation + file = File.open("example.txt") + content = file.read +rescue Errno::ENOENT => e + puts "Could not find the file: #{e.message}" +else + # This block only executes if no exception was raised + puts "Successfully read #{content.length} bytes" +ensure + # This block always executes, whether an exception occurred or not + file&.close +end +``` + +## Method-level rescue + +You can also use `rescue` at the method level without an explicit `begin` block: + +```ruby +def process_file(path) + File.read(path) +rescue Errno::ENOENT + puts "File not found" +rescue Errno::EACCES + puts "Permission denied" +end +``` + +When rescuing exceptions, it's important to: +- Only rescue specific exceptions you can handle +- Avoid rescuing `Exception` as it captures all exceptions, including system ones +- Use `ensure` for cleanup code that must always run +- Keep the rescue block focused on error handling logic \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index 6271d24fed..9f050a66d5 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -994,6 +994,16 @@ module MyModule RUBY position: { line: 1, character: 2 }, }, + "rescue" => { + source: <<~RUBY, + begin + true + rescue + false + end + RUBY + position: { line: 2, character: 2 }, + }, } test_cases.each do |keyword, config| From ec0deddb9783c78f30dd2196fc85180e3839f7da Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 17:29:23 -0400 Subject: [PATCH 17/25] Add 'Rubyists' to project-words --- project-words | 1 + 1 file changed, 1 insertion(+) diff --git a/project-words b/project-words index 7cda229ddc..e2f66ebcd7 100644 --- a/project-words +++ b/project-words @@ -88,6 +88,7 @@ rruby rubyfmt rubylibdir rubylibprefix +Rubyists setqflist setsockopt shadowenv From c7d8d1ea985963e5d73586173fafe22d6f4691ef Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 17:33:30 -0400 Subject: [PATCH 18/25] Add 'return' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/return.md | 111 +++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 8 ++ 4 files changed, 127 insertions(+) create mode 100644 static_docs/return.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index cb12563460..85cacc30f1 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -34,6 +34,7 @@ class Hover Prism::ModuleNode, Prism::NextNode, Prism::RescueNode, + Prism::ReturnNode, Prism::SymbolNode, Prism::StringNode, Prism::InterpolatedStringNode, @@ -92,6 +93,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_module_node_enter, :on_next_node_enter, :on_rescue_node_enter, + :on_return_node_enter, :on_super_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, @@ -174,6 +176,11 @@ def on_rescue_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::ReturnNode node) -> void + def on_return_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 9f0c65f089..880b712818 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -25,6 +25,7 @@ module RubyLsp "module" => "Defines a module", "next" => "Skips the rest of the current iteration and moves to the next iteration of a loop or block", "rescue" => "Handles exceptions that occur in the code block", + "return" => "Exits a method and returns a value", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/return.md b/static_docs/return.md new file mode 100644 index 0000000000..c01adb8ff4 --- /dev/null +++ b/static_docs/return.md @@ -0,0 +1,111 @@ +# Return + +In Ruby, `return` is used to explicitly return a value from a method or block. While Ruby automatically returns the value of the last evaluated expression, `return` allows you to exit the method early and specify the return value. + +```ruby +def greet(name) + return "Hello, #{name}!" +end + +puts greet("Ruby") # => "Hello, Ruby!" +``` + +When no value is provided to `return`, it returns `nil`: + +```ruby +def early_exit + return if condition? + # Code here won't execute if condition? is true + perform_task +end +``` + +## Multiple values + +Ruby allows returning multiple values, which are automatically converted into an array: + +```ruby +def calculate_stats(numbers) + sum = numbers.sum + average = sum / numbers.length.to_f + return sum, average +end + +total, mean = calculate_stats([1, 2, 3, 4]) +puts total # => 10 +puts mean # => 2.5 +``` + +## Early returns + +Using `return` for early exits can help make code more readable by reducing nesting: + +```ruby +# Without early return +def process_user(user) + if user.active? + if user.admin? + perform_admin_task + else + perform_regular_task + end + else + puts "Inactive user" + end +end + +# With early return +def process_user(user) + return puts "Inactive user" unless user.active? + return perform_admin_task if user.admin? + perform_regular_task +end +``` + +## Return in blocks + +When used inside a block, `return` will exit from the method that yielded to the block: + +```ruby +def process_items + [1, 2, 3].each do |item| + return item if item > 1 + puts "Processing #{item}" + end + puts "Done processing" +end + +result = process_items +# Prints "Processing 1" +puts result # => 2 +``` + +## Return in procs vs lambdas + +The behavior of `return` differs between procs and lambdas: + +```ruby +# In a proc, return exits the enclosing method +def proc_return + proc = Proc.new { return "From proc" } + proc.call + "From method" # Never reached +end + +puts proc_return # => "From proc" + +# In a lambda, return only exits the lambda itself +def lambda_return + lambda = -> { return "From lambda" } + lambda.call + "From method" # This is reached +end + +puts lambda_return # => "From method" +``` + +When using `return`, consider: +- Whether an implicit return would be clearer +- If early returns improve code readability +- The context (proc vs lambda) when using `return` in blocks +- Using multiple returns judiciously \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index 9f050a66d5..f920f83f1b 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -1004,6 +1004,14 @@ module MyModule RUBY position: { line: 2, character: 2 }, }, + "return" => { + source: <<~RUBY, + def foo + return + end + RUBY + position: { line: 1, character: 2 }, + }, } test_cases.each do |keyword, config| From b3cec01294d36bd99db6c80bc01a91dcf1f3872a Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 18:03:57 -0400 Subject: [PATCH 19/25] Add test coverage for 'break'. Add to project-words --- project-words | 4 ++++ test/requests/hover_expectations_test.rb | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/project-words b/project-words index e2f66ebcd7..79ee76930f 100644 --- a/project-words +++ b/project-words @@ -61,6 +61,8 @@ Lstart metaprogramming mkpath multibyte +namespacing +Namespacing nargs nodoc noreturn @@ -69,6 +71,7 @@ onig # abbreviation for oniguruma Pacman pathlist popen +procs qorge qtlzwssomeking quickfixes @@ -115,6 +118,7 @@ unindexed unparser unresolve vcall +Validatable Vinicius vscodemachineid vsctm diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index f920f83f1b..59b08f1689 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -1012,6 +1012,14 @@ def foo RUBY position: { line: 1, character: 2 }, }, + "break" => { + source: <<~RUBY, + while true + break + end + RUBY + position: { line: 1, character: 2 }, + }, } test_cases.each do |keyword, config| From 6a6948e75ea87407c39023a1f88c487832499023 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 20:00:36 -0400 Subject: [PATCH 20/25] Add 'undef' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 +++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/undef.md | 62 ++++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 12 +++++ 4 files changed, 82 insertions(+) create mode 100644 static_docs/undef.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 85cacc30f1..3647b9b9e4 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -37,6 +37,7 @@ class Hover Prism::ReturnNode, Prism::SymbolNode, Prism::StringNode, + Prism::UndefNode, Prism::InterpolatedStringNode, Prism::SuperNode, Prism::ForwardingSuperNode, @@ -95,6 +96,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_rescue_node_enter, :on_return_node_enter, :on_super_node_enter, + :on_undef_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, :on_interpolated_string_node_enter, @@ -181,6 +183,11 @@ def on_return_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::UndefNode node) -> void + def on_undef_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 880b712818..1d230e1882 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -26,6 +26,7 @@ module RubyLsp "next" => "Skips the rest of the current iteration and moves to the next iteration of a loop or block", "rescue" => "Handles exceptions that occur in the code block", "return" => "Exits a method and returns a value", + "undef" => "Undefines a method", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/undef.md b/static_docs/undef.md new file mode 100644 index 0000000000..ef893ff5af --- /dev/null +++ b/static_docs/undef.md @@ -0,0 +1,62 @@ +# Undef + +The `undef` keyword in Ruby is used to undefine methods. When a method is undefined, any subsequent attempts to call it will result in a `NoMethodError`. This is different from making a method private or protected - the method is completely removed from the class. + +```ruby +class Example + def hello + "Hello!" + end + + def goodbye + "Goodbye!" + end + + # Undefine the hello method + undef hello +end + +example = Example.new +example.goodbye # => "Goodbye!" +example.hello # => NoMethodError: undefined method `hello' for # +``` + +## Multiple methods + +You can undefine multiple methods at once by providing multiple method names: + +```ruby +class Greeter + def hello + "Hello!" + end + + def hi + "Hi!" + end + + def hey + "Hey!" + end + + # Undefine multiple methods at once + undef hello, hi, hey +end +``` + +## Common use cases + +The `undef` keyword is often used when: +1. You want to prevent a method inherited from a superclass from being called +2. You want to ensure certain methods cannot be called on instances of your class +3. You're implementing a strict interface and want to remove methods that don't belong + +```ruby +class RestrictedArray < Array + # Prevent destructive methods from being called + undef push, <<, pop, shift, unshift +end + +restricted = RestrictedArray.new([1, 2, 3]) +restricted.push(4) # => NoMethodError: undefined method `push' for # +``` \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index 59b08f1689..c44dec993a 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -1020,6 +1020,18 @@ def foo RUBY position: { line: 1, character: 2 }, }, + "undef" => { + source: <<~RUBY, + class Example + def hello + "Hello!" + end + + undef hello + end + RUBY + position: { line: 5, character: 2 }, + }, } test_cases.each do |keyword, config| From 74649c1a6bad2af1a861ec8508b0abee05fceebf Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 20:04:59 -0400 Subject: [PATCH 21/25] Add 'unless' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/unless.md | 90 ++++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 8 +++ 4 files changed, 106 insertions(+) create mode 100644 static_docs/unless.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 3647b9b9e4..a02c91292a 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -38,6 +38,7 @@ class Hover Prism::SymbolNode, Prism::StringNode, Prism::UndefNode, + Prism::UnlessNode, Prism::InterpolatedStringNode, Prism::SuperNode, Prism::ForwardingSuperNode, @@ -97,6 +98,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_return_node_enter, :on_super_node_enter, :on_undef_node_enter, + :on_unless_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, :on_interpolated_string_node_enter, @@ -188,6 +190,11 @@ def on_undef_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::UnlessNode node) -> void + def on_unless_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 1d230e1882..25d3a92d3e 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -27,6 +27,7 @@ module RubyLsp "rescue" => "Handles exceptions that occur in the code block", "return" => "Exits a method and returns a value", "undef" => "Undefines a method", + "unless" => "Executes the code in the unless block if the condition is false", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/unless.md b/static_docs/unless.md new file mode 100644 index 0000000000..1ff3ac4763 --- /dev/null +++ b/static_docs/unless.md @@ -0,0 +1,90 @@ +# Unless + +The `unless` keyword in Ruby is used as a conditional statement that executes code when a condition is `false`. It's effectively the opposite of an `if` statement and is often used to make negative conditions more readable. + +```ruby +def process_order(order) + # Using unless to handle invalid cases + unless order.valid? + puts "Cannot process invalid order" + return + end + + # Process the valid order... + order.process +end +``` + +## Guard clauses + +`unless` is commonly used in guard clauses at the beginning of methods to handle invalid cases early: + +```ruby +def send_notification(user) + unless user.subscribed? + return "User must be subscribed to receive notifications" + end + + # Send the notification... + NotificationService.deliver(user) +end +``` + +## Single line usage + +For simple conditions, `unless` can be used as a statement modifier at the end of a line: + +```ruby +def display_status(record) + record.display_warning unless record.active? + # More status handling... +end +``` + +## Best practices + +1. Avoid using `else` with `unless` as it makes the logic harder to follow: + +```ruby +# bad +unless success? + puts "failure" +else + puts "success" +end + +# good +if success? + puts "success" +else + puts "failure" +end +``` + +2. Avoid complex conditions with `unless`. Use `if` with positive conditions instead: + +```ruby +# bad +unless user.nil? || user.subscribed? + notify_inactive_user(user) +end + +# good +if user.present? && !user.subscribed? + notify_inactive_user(user) +end +``` + +3. Don't use `unless` with multiple conditions joined by `&&`: + +```ruby +# bad +unless user.active? && user.confirmed? + handle_inactive_user +end + +# good +if !user.active? || !user.confirmed? + handle_inactive_user +end +``` \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index c44dec993a..f65581f124 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -1032,6 +1032,14 @@ def hello RUBY position: { line: 5, character: 2 }, }, + "unless" => { + source: <<~RUBY, + unless condition + true + end + RUBY + position: { line: 0, character: 2 }, + }, } test_cases.each do |keyword, config| From e00d9cd4d3cdde00daf30efb951997a7ec05cf51 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 20:12:22 -0400 Subject: [PATCH 22/25] Add 'until' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/until.md | 108 +++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 8 ++ 4 files changed, 124 insertions(+) create mode 100644 static_docs/until.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index a02c91292a..bfa9b55f61 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -39,6 +39,7 @@ class Hover Prism::StringNode, Prism::UndefNode, Prism::UnlessNode, + Prism::UntilNode, Prism::InterpolatedStringNode, Prism::SuperNode, Prism::ForwardingSuperNode, @@ -99,6 +100,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_super_node_enter, :on_undef_node_enter, :on_unless_node_enter, + :on_until_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, :on_interpolated_string_node_enter, @@ -195,6 +197,11 @@ def on_unless_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::UntilNode node) -> void + def on_until_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 25d3a92d3e..36c8a9efbd 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -28,6 +28,7 @@ module RubyLsp "return" => "Exits a method and returns a value", "undef" => "Undefines a method", "unless" => "Executes the code in the unless block if the condition is false", + "until" => "Executes the code in the until block until the condition is true", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/until.md b/static_docs/until.md new file mode 100644 index 0000000000..5fa62052ba --- /dev/null +++ b/static_docs/until.md @@ -0,0 +1,108 @@ +# Until + +In Ruby, the `until` keyword creates a loop that executes code until a condition becomes `true`. It's effectively the opposite of a `while` loop and is often used when you want to continue an action while a condition is `false`. + +```ruby +counter = 0 + +until counter >= 5 + puts counter + counter += 1 +end +# Prints: +# 0 +# 1 +# 2 +# 3 +# 4 +``` + +The `until` loop first evaluates the condition. If the condition is `false`, it executes the code block. After each iteration, it checks the condition again. When the condition becomes `true`, the loop ends. + +## Modifier form + +Like many Ruby control structures, `until` can be used as a statement modifier at the end of a line: + +```ruby +# Keep prompting for input until a valid response is received +response = gets.chomp +response = gets.chomp until response.downcase == "yes" || response.downcase == "no" +``` + +## Break and next + +You can use `break` to exit an `until` loop early and `next` to skip to the next iteration: + +```ruby +number = 0 + +until number > 10 + number += 1 + next if number.odd? # Skip odd numbers + puts number # Print only even numbers + break if number == 8 # Stop when we reach 8 +end +# Prints: +# 2 +# 4 +# 6 +# 8 +``` + +## Begin/Until + +Ruby also provides a `begin/until` construct that ensures the loop body is executed at least once before checking the condition: + +```ruby +attempts = 0 + +begin + attempts += 1 + result = perform_operation +end until result.success? || attempts >= 3 + +# The operation will be attempted at least once, and up to three times +# if it doesn't succeed +``` + +## Best practices + +1. Use `until` when waiting for a condition to become `true`: + +```ruby +# Good - clear that we're waiting for readiness +until server.ready? + sleep 1 +end + +# Less clear intention +while !server.ready? + sleep 1 +end +``` + +2. Consider using `while` with positive conditions instead of `until` with negative ones: + +```ruby +# Less clear with double negative +until !queue.empty? + process_next_item +end + +# Better - clearer intention +while queue.any? + process_next_item +end +``` + +3. Use modifier form for simple, single-line operations: + +```ruby +# Good - concise and clear +retry_operation until successful? + +# Less concise for simple operations +until successful? + retry_operation +end +``` \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index f65581f124..d1bb145565 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -1040,6 +1040,14 @@ def hello RUBY position: { line: 0, character: 2 }, }, + "until" => { + source: <<~RUBY, + until condition + true + end + RUBY + position: { line: 0, character: 2 }, + }, } test_cases.each do |keyword, config| From ee272c4507f8a19bde0c6838326af10b88d383a7 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 20:20:33 -0400 Subject: [PATCH 23/25] Add 'when' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 + lib/ruby_lsp/static_docs.rb | 1 + static_docs/when.md | 156 +++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 8 ++ 4 files changed, 172 insertions(+) create mode 100644 static_docs/when.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index bfa9b55f61..1921a4f43e 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -43,6 +43,7 @@ class Hover Prism::InterpolatedStringNode, Prism::SuperNode, Prism::ForwardingSuperNode, + Prism::WhenNode, Prism::YieldNode, Prism::ClassVariableAndWriteNode, Prism::ClassVariableOperatorWriteNode, @@ -101,6 +102,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_undef_node_enter, :on_unless_node_enter, :on_until_node_enter, + :on_when_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, :on_interpolated_string_node_enter, @@ -202,6 +204,11 @@ def on_until_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::WhenNode node) -> void + def on_when_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 36c8a9efbd..86aa48fdee 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -29,6 +29,7 @@ module RubyLsp "undef" => "Undefines a method", "unless" => "Executes the code in the unless block if the condition is false", "until" => "Executes the code in the until block until the condition is true", + "when" => "Matches a value against a pattern", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/when.md b/static_docs/when.md new file mode 100644 index 0000000000..1c239ad2ec --- /dev/null +++ b/static_docs/when.md @@ -0,0 +1,156 @@ +# When + +The `when` keyword in Ruby is primarily used within `case` statements to define different conditions or patterns to match against. It's similar to `if/elsif` chains but often provides more readable and maintainable code when dealing with multiple conditions. + +```ruby +grade = "A" + +case grade +when "A" + puts "Excellent!" +when "B" + puts "Good job!" +when "C" + puts "Fair" +else + puts "Keep trying!" +end +# Prints: Excellent! +``` + +## Pattern matching + +The `when` clause can match against multiple values using comma-separated expressions: + +```ruby +day = "Saturday" + +case day +when "Saturday", "Sunday" + puts "It's the weekend!" +when "Monday" + puts "Back to work!" +else + puts "It's a regular weekday" +end +# Prints: It's the weekend! +``` + +## Range matching + +`when` can match against ranges: + +```ruby +score = 85 + +case score +when 90..100 + puts "A grade" +when 80..89 + puts "B grade" +when 70..79 + puts "C grade" +else + puts "Need improvement" +end +# Prints: B grade +``` + +## Class/type matching + +`when` can match against classes to check object types: + +```ruby +data = [1, 2, 3] + +case data +when String + puts "Processing string: #{data}" +when Array + puts "Processing array with #{data.length} elements" +when Hash + puts "Processing hash with #{data.keys.length} keys" +else + puts "Unknown data type" +end +# Prints: Processing array with 3 elements +``` + +## Pattern matching with 'in' + +Ruby also supports advanced pattern matching with `in` patterns: + +```ruby +response = { status: 200, body: { name: "Ruby" } } + +case response +when { status: 200, body: { name: String => name } } + puts "Success! Name: #{name}" +when { status: 404 } + puts "Not found" +when { status: 500..599 } + puts "Server error" +end +# Prints: Success! Name: Ruby +``` + +## Best practices + +1. Use `when` with `case` when dealing with multiple conditions based on a single value: + +```ruby +# Good - clear and concise with case/when +case status +when :pending + process_pending +when :approved + process_approved +when :rejected + process_rejected +end + +# Less clear with if/elsif chains +if status == :pending + process_pending +elsif status == :approved + process_approved +elsif status == :rejected + process_rejected +end +``` + +2. Take advantage of pattern matching for complex conditions: + +```ruby +# Good - using pattern matching capabilities +case user +when Admin + handle_admin_access +when Moderator, Editor + handle_moderator_access +when BasicUser + handle_basic_access +end +``` + +3. Use comma-separated values instead of multiple `when` clauses for the same outcome: + +```ruby +# Good - concise and clear +case day +when "Saturday", "Sunday" + weekend_schedule +else + weekday_schedule +end + +# Less concise +case day +when "Saturday" + weekend_schedule +when "Sunday" + weekend_schedule +else + weekday_schedule +end +``` \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index d1bb145565..220a653872 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -1048,6 +1048,14 @@ def hello RUBY position: { line: 0, character: 2 }, }, + "when" => { + source: <<~RUBY, + case value + when pattern + end + RUBY + position: { line: 1, character: 2 }, + }, } test_cases.each do |keyword, config| From 27ccb0768b2eca004b532d5d3be6c2a3fe5c8b74 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 20:24:40 -0400 Subject: [PATCH 24/25] Add 'while' keyword --- lib/ruby_lsp/listeners/hover.rb | 7 ++ lib/ruby_lsp/static_docs.rb | 1 + static_docs/while.md | 123 +++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 8 ++ 4 files changed, 139 insertions(+) create mode 100644 static_docs/while.md diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 1921a4f43e..af76730ef1 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -44,6 +44,7 @@ class Hover Prism::SuperNode, Prism::ForwardingSuperNode, Prism::WhenNode, + Prism::WhileNode, Prism::YieldNode, Prism::ClassVariableAndWriteNode, Prism::ClassVariableOperatorWriteNode, @@ -103,6 +104,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so :on_unless_node_enter, :on_until_node_enter, :on_when_node_enter, + :on_while_node_enter, :on_forwarding_super_node_enter, :on_string_node_enter, :on_interpolated_string_node_enter, @@ -209,6 +211,11 @@ def on_when_node_enter(node) handle_keyword_documentation(node.keyword) end + #: (Prism::WhileNode node) -> void + def on_while_node_enter(node) + handle_keyword_documentation(node.keyword) + end + #: (Prism::InterpolatedStringNode node) -> void def on_interpolated_string_node_enter(node) generate_heredoc_hover(node) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index 86aa48fdee..cdf69a7a14 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -30,6 +30,7 @@ module RubyLsp "unless" => "Executes the code in the unless block if the condition is false", "until" => "Executes the code in the until block until the condition is true", "when" => "Matches a value against a pattern", + "while" => "Executes the code in the while block while the condition is true", "yield" => "Invokes the passed block with the given arguments", }.freeze #: Hash[String, String] end diff --git a/static_docs/while.md b/static_docs/while.md new file mode 100644 index 0000000000..919471b4c5 --- /dev/null +++ b/static_docs/while.md @@ -0,0 +1,123 @@ +# While + +The `while` keyword in Ruby creates a loop that executes a block of code as long as a given condition is true. The condition is checked before each iteration. + +```ruby +counter = 0 + +while counter < 3 + puts counter + counter += 1 +end +# Prints: +# 0 +# 1 +# 2 +``` + +## Basic usage with break + +The `break` keyword can be used to exit a `while` loop prematurely: + +```ruby +number = 1 + +while true # infinite loop + puts number + break if number >= 3 + number += 1 +end +# Prints: +# 1 +# 2 +# 3 +``` + +## Using next to skip iterations + +The `next` keyword can be used to skip to the next iteration: + +```ruby +counter = 0 + +while counter < 5 + counter += 1 + next if counter.even? # Skip even numbers + puts counter +end +# Prints: +# 1 +# 3 +# 5 +``` + +## While with begin (do-while equivalent) + +Ruby doesn't have a direct do-while loop, but you can achieve similar behavior using `begin...end while`: + +```ruby +count = 5 + +begin + puts count + count -= 1 +end while count > 0 +# Prints: +# 5 +# 4 +# 3 +# 2 +# 1 +``` + +## Best practices + +1. Use `while` when you need to loop based on a condition rather than a specific number of iterations: + +```ruby +# Good - clear condition-based looping +input = gets.chomp +while input != "quit" + process_input(input) + input = gets.chomp +end + +# Less appropriate for condition-based looping +loop do + input = gets.chomp + break if input == "quit" + process_input(input) +end +``` + +2. Consider using `until` for negative conditions instead of `while !condition`: + +```ruby +# Good - using until for negative conditions +until queue.empty? + process_item(queue.pop) +end + +# Less readable +while !queue.empty? + process_item(queue.pop) +end +``` + +3. Use `while true` sparingly and ensure there's a clear exit condition: + +```ruby +# Good - clear exit condition with break +while true + command = get_command + break if command == "exit" + execute_command(command) +end + +# Better - using a more explicit condition +command = get_command +while command != "exit" + execute_command(command) + command = get_command +end +``` \ No newline at end of file diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index 220a653872..c88add3549 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -1056,6 +1056,14 @@ def hello RUBY position: { line: 1, character: 2 }, }, + "while" => { + source: <<~RUBY, + while condition + true + end + RUBY + position: { line: 0, character: 2 }, + }, } test_cases.each do |keyword, config| From 6591e00c91f92b113e5c665816dcadfae872aa89 Mon Sep 17 00:00:00 2001 From: Mikey Gough Date: Thu, 29 May 2025 20:33:50 -0400 Subject: [PATCH 25/25] Revise 'undef' hover definition --- lib/ruby_lsp/static_docs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ruby_lsp/static_docs.rb b/lib/ruby_lsp/static_docs.rb index cdf69a7a14..e9cc521abc 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -26,7 +26,7 @@ module RubyLsp "next" => "Skips the rest of the current iteration and moves to the next iteration of a loop or block", "rescue" => "Handles exceptions that occur in the code block", "return" => "Exits a method and returns a value", - "undef" => "Undefines a method", + "undef" => "Removes a method definition from a class or module", "unless" => "Executes the code in the unless block if the condition is false", "until" => "Executes the code in the until block until the condition is true", "when" => "Matches a value against a pattern",