diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index c533853f5f..af76730ef1 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -7,10 +7,18 @@ class Hover include Requests::Support::Common ALLOWED_TARGETS = [ + Prism::BreakNode, Prism::CallNode, + Prism::CaseNode, + Prism::ClassNode, Prism::ConstantReadNode, Prism::ConstantWriteNode, Prism::ConstantPathNode, + Prism::DefNode, + Prism::DefinedNode, + Prism::ElseNode, + Prism::EnsureNode, + Prism::ForNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, @@ -23,11 +31,20 @@ class Hover Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode, Prism::InstanceVariableWriteNode, + Prism::ModuleNode, + Prism::NextNode, + Prism::RescueNode, + Prism::ReturnNode, Prism::SymbolNode, Prism::StringNode, + Prism::UndefNode, + Prism::UnlessNode, + Prism::UntilNode, Prism::InterpolatedStringNode, Prism::SuperNode, Prism::ForwardingSuperNode, + Prism::WhenNode, + Prism::WhileNode, Prism::YieldNode, Prism::ClassVariableAndWriteNode, Prism::ClassVariableOperatorWriteNode, @@ -54,10 +71,18 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so dispatcher.register( 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, :on_call_node_enter, + :on_def_node_enter, + :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, @@ -70,7 +95,16 @@ 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_next_node_enter, + :on_rescue_node_enter, + :on_return_node_enter, :on_super_node_enter, + :on_undef_node_enter, + :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, @@ -97,6 +131,91 @@ 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) + end + + #: (Prism::ClassNode node) -> void + 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::DefinedNode node) -> void + 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::EnsureNode node) -> void + 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::ModuleNode node) -> void + 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::RescueNode node) -> void + 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::UndefNode node) -> void + 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::UntilNode node) -> void + 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::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 2749ff8688..e9cc521abc 100644 --- a/lib/ruby_lsp/static_docs.rb +++ b/lib/ruby_lsp/static_docs.rb @@ -14,6 +14,23 @@ module RubyLsp # A map of keyword => short documentation to be displayed on hover or completion KEYWORD_DOCS = { + "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", + "def" => "Defines a method", + "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", + "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", + "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", + "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/project-words b/project-words index 7cda229ddc..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 @@ -88,6 +91,7 @@ rruby rubyfmt rubylibdir rubylibprefix +Rubyists setqflist setsockopt shadowenv @@ -114,6 +118,7 @@ unindexed unparser unresolve vcall +Validatable Vinicius vscodemachineid vsctm 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 diff --git a/static_docs/case.md b/static_docs/case.md new file mode 100644 index 0000000000..f5e3482d7f --- /dev/null +++ b/static_docs/case.md @@ -0,0 +1,142 @@ +# 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 +``` + +## 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. + +```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 +case +when Time.now.saturday? + puts "It's Saturday!" +when Time.now.sunday? + puts "It's Sunday!" +else + puts "It's a weekday" +end +``` + +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 new file mode 100644 index 0000000000..09562d0633 --- /dev/null +++ b/static_docs/class.md @@ -0,0 +1,101 @@ +# Class + +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 # Instance variable stores state + end + + def greet # Instance method stores behavior + puts "Hello, #{@name}!" + end +end + +person = Person.new("Ruby") +person.greet +# Output: +# Hello, Ruby! +``` + +## Instance Variables and Methods + +Instance variables (starting with `@`) store object-specific state, while instance methods define the behavior that each object can perform. + +```ruby +class BankAccount + def initialize(balance) + @balance = balance + end + + def deposit(amount) + @balance += amount + end + + def current_balance + @balance + end +end + +account = BankAccount.new(100) +account.deposit(50) +puts account.current_balance # Output: 150 +``` + +## Attribute Accessors + +Ruby provides convenient methods to create getters and setters for instance variables: + +```ruby +class User + # Creates both getter and setter methods + attr_accessor :name + + # Creates getter method only + attr_reader :created_at + + # Creates setter method only + attr_writer :password + + def initialize(name) + @name = name + @created_at = Time.now + end +end + +user = User.new("Alice") +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 code by grouping related data and behavior into objects. diff --git a/static_docs/def.md b/static_docs/def.md new file mode 100644 index 0000000000..b5b6de3295 --- /dev/null +++ b/static_docs/def.md @@ -0,0 +1,113 @@ +# 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 + # 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 +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 diff --git a/static_docs/defined.md b/static_docs/defined.md new file mode 100644 index 0000000000..e7a1f6931d --- /dev/null +++ b/static_docs/defined.md @@ -0,0 +1,98 @@ +# 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" + + def check_definitions + @instance_var = "!" + + 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) + 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 diff --git a/static_docs/else.md b/static_docs/else.md new file mode 100644 index 0000000000..73bc174871 --- /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 if (positive condition) +temperature = 25 + +if 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 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/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/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 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 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/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/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/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/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/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/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 f709ceff36..c88add3549 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -929,26 +929,162 @@ 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 }, + }, + "ensure" => { + source: <<~RUBY, + begin + true + ensure + false + end + RUBY + position: { line: 2, character: 2 }, + }, + "for" => { + source: <<~RUBY, + for i in 1..10 + end + 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 }, + }, + "rescue" => { + source: <<~RUBY, + begin + true + rescue + false + end + RUBY + position: { line: 2, character: 2 }, + }, + "return" => { + source: <<~RUBY, + def foo + return + end + RUBY + position: { line: 1, character: 2 }, + }, + "break" => { + source: <<~RUBY, + while true + break + end + RUBY + position: { line: 1, character: 2 }, + }, + "undef" => { + source: <<~RUBY, + class Example + def hello + "Hello!" + end + + undef hello + end + RUBY + position: { line: 5, character: 2 }, + }, + "unless" => { + source: <<~RUBY, + unless condition + true + end + RUBY + position: { line: 0, character: 2 }, + }, + "until" => { + source: <<~RUBY, + until condition + true + end + RUBY + position: { line: 0, character: 2 }, + }, + "when" => { + source: <<~RUBY, + case value + when pattern + end + RUBY + position: { line: 1, character: 2 }, + }, + "while" => { + source: <<~RUBY, + while condition + true + end + RUBY + position: { line: 0, 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