From b3e59adb96c8602f9b48b1d31ec1e7ce672e3402 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 03:57:22 +0000 Subject: [PATCH 1/3] Initial plan From 9e8d3133301df97163684844dd2fef6f55c457a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 04:04:24 +0000 Subject: [PATCH 2/3] Add support for module inclusion in completions Co-authored-by: kwerle <23320+kwerle@users.noreply.github.com> --- lib/db/schema.rb | 1 + lib/ruby_language_server/completion.rb | 42 ++++++++++++ lib/ruby_language_server/scope_data/scope.rb | 16 +++++ .../scope_parser_commands/ruby_commands.rb | 9 +++ .../ruby_language_server/completion_spec.rb | 65 +++++++++++++++++++ 5 files changed, 133 insertions(+) diff --git a/lib/db/schema.rb b/lib/db/schema.rb index 30117de..2627e18 100644 --- a/lib/db/schema.rb +++ b/lib/db/schema.rb @@ -20,6 +20,7 @@ def write(*args) t.string :class_type, null: false t.text :parameters # JSON string of method parameters t.boolean :class_method, default: false # true for class methods (def self.method) + t.text :included_modules # JSON array of included module names end add_index :scopes, :name diff --git a/lib/ruby_language_server/completion.rb b/lib/ruby_language_server/completion.rb index 972b137..584e223 100644 --- a/lib/ruby_language_server/completion.rb +++ b/lib/ruby_language_server/completion.rb @@ -153,6 +153,13 @@ def scope_completions(word, scopes) scope_ids = scopes.map(&:id) word_scopes = scopes.to_a + RubyLanguageServer::ScopeData::Scope.where(parent_id: scope_ids).closest_to(word).limit(5) + + # Add methods from included modules + scopes.each do |scope| + included_module_scopes = get_included_module_scopes(scope) + word_scopes += included_module_scopes + end + scope_words = word_scopes.select(&:named_scope?).sort_by(&:depth).map { |scope| [scope.name, scope] } variable_words = RubyLanguageServer::ScopeData::Variable.where(scope_id: scope_ids).closest_to(word).limit(5).map { |variable| [variable.name, variable.scope] } words = (scope_words + variable_words).to_h @@ -164,6 +171,41 @@ def scope_completions(word, scopes) hash[w][:parameters] = scope.parsed_parameters if scope.method? && scope.parameters.present? end end + + # Get all scopes from included modules + def get_included_module_scopes(scope) + included_scopes = [] + module_names = scope.parsed_included_modules + + module_names.each do |module_name| + # Try to find the module by full path first + full_module_path = resolve_module_path(module_name, scope) + module_scope = RubyLanguageServer::ScopeData::Scope.find_by(path: full_module_path, class_type: RubyLanguageServer::ScopeData::Base::TYPE_MODULE) + + # If not found by full path, try searching by name + module_scope ||= RubyLanguageServer::ScopeData::Scope.where(name: module_name, class_type: RubyLanguageServer::ScopeData::Base::TYPE_MODULE).first + + if module_scope + # Get all methods defined in the module + included_scopes += module_scope.children.where(class_type: RubyLanguageServer::ScopeData::Base::TYPE_METHOD).to_a + end + end + + included_scopes + end + + # Resolve the full module path considering the current scope + def resolve_module_path(module_name, scope) + # If it starts with ::, it's an absolute path + return module_name.sub(/^::/, '') if module_name.start_with?('::') + + # Try to resolve relative to current scope + if scope.parent && scope.parent.path.present? + "#{scope.parent.path}::#{module_name}" + else + module_name + end + end end end end diff --git a/lib/ruby_language_server/scope_data/scope.rb b/lib/ruby_language_server/scope_data/scope.rb index 09a7b33..18d09c0 100644 --- a/lib/ruby_language_server/scope_data/scope.rb +++ b/lib/ruby_language_server/scope_data/scope.rb @@ -94,6 +94,22 @@ def set_parameters(params_array) self.parameters = params_array.to_json if params_array.present? end + # Get included modules as an array of strings + def parsed_included_modules + return [] unless included_modules.present? + + JSON.parse(included_modules) + rescue JSON::ParserError + [] + end + + # Add an included module name + def add_included_module(module_name) + current = parsed_included_modules + current << module_name unless current.include?(module_name) + self.included_modules = current.to_json + end + # Called from ScopeParser to cleanup empty blocks. def close destroy! if block_scope? && variables.none? diff --git a/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb b/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb index 70b7944..3c96a80 100644 --- a/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb +++ b/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb @@ -41,6 +41,15 @@ def on_attr_writer_command(line, args, rest) ruby_command_add_attr(line, column, names, false, true) end + def on_include_command(line, args, rest) + # Extract the module name(s) from the include statement + module_names = rest.flatten.select { |o| o.instance_of? String } + module_names.each do |module_name| + @current_scope.add_included_module(module_name) + @current_scope.save! + end + end + private def ruby_command_names(rest) diff --git a/spec/lib/ruby_language_server/completion_spec.rb b/spec/lib/ruby_language_server/completion_spec.rb index 3c99201..b77926e 100644 --- a/spec/lib/ruby_language_server/completion_spec.rb +++ b/spec/lib/ruby_language_server/completion_spec.rb @@ -194,4 +194,69 @@ def method_mixed(required, optional = nil, *rest, keyword:, **kwargs, &block) assert_equal('arg1 (required), arg2 (required)', simple_method_item[:detail], 'should include parameter details') end end + + describe 'module inclusion' do + before do + @code_with_include = <<-SOURCE + module FooModule + def foo_method + end + + def another_foo_method(param1) + end + end + + class BarClass + include FooModule + + def bar_method + foo # completion should find foo_method from included FooModule module + end + end + SOURCE + @include_parser = RubyLanguageServer::ScopeParser.new(@code_with_include) + end + + it 'should track included modules in scope' do + all_scopes = @include_parser.root_scope.self_and_descendants + bar_class = all_scopes.find_by_path('BarClass') + + assert bar_class, 'BarClass should exist' + included_modules = bar_class.parsed_included_modules + assert_equal(['FooModule'], included_modules, 'BarClass should have FooModule in its included modules') + end + + it 'should include methods from included modules in completions' do + all_scopes = @include_parser.root_scope.self_and_descendants + bar_method_scope = all_scopes.find_by_path('BarClass#bar_method') + + assert bar_method_scope, 'bar_method scope should exist' + + # Get completions for 'foo' prefix within BarClass#bar_method + context = ['foo'] + position_scopes = @include_parser.root_scope.self_and_descendants.for_line(bar_method_scope.top_line + 1) + completions = scope_completions(context.last, position_scopes) + + # Should include foo_method from the included FooModule module + completion_names = completions.map(&:first) + assert_includes(completion_names, 'foo_method', 'Should find foo_method from included module FooModule') + assert_includes(completion_names, 'another_foo_method', 'Should find another_foo_method from included module FooModule') + end + + it 'should include method parameters from included modules' do + all_scopes = @include_parser.root_scope.self_and_descendants + bar_method_scope = all_scopes.find_by_path('BarClass#bar_method') + + # Get completions for 'another_foo' prefix + context = ['another_foo'] + completions = RubyLanguageServer::Completion.completion(context, bar_method_scope, all_scopes) + + # Find the another_foo_method completion + another_foo_item = completions[:items].find { |item| item[:label].start_with?('another_foo_method') } + + assert another_foo_item, 'another_foo_method should be in completions' + assert_equal('another_foo_method(param1)', another_foo_item[:label], 'should include parameters in label') + assert_equal('another_foo_method(${1:param1})', another_foo_item[:insertText], 'should include parameter snippet') + end + end end From 09a2e8b42676c3b31f61539bfa115d7d5724c047 Mon Sep 17 00:00:00 2001 From: Kurt Werle Date: Tue, 17 Feb 2026 20:29:07 -0800 Subject: [PATCH 3/3] cops --- lib/ruby_language_server/completion.rb | 18 +++++++++--------- .../scope_parser_commands/ruby_commands.rb | 2 +- .../ruby_language_server/completion_spec.rb | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/ruby_language_server/completion.rb b/lib/ruby_language_server/completion.rb index 584e223..7dd5564 100644 --- a/lib/ruby_language_server/completion.rb +++ b/lib/ruby_language_server/completion.rb @@ -153,13 +153,13 @@ def scope_completions(word, scopes) scope_ids = scopes.map(&:id) word_scopes = scopes.to_a + RubyLanguageServer::ScopeData::Scope.where(parent_id: scope_ids).closest_to(word).limit(5) - + # Add methods from included modules scopes.each do |scope| included_module_scopes = get_included_module_scopes(scope) word_scopes += included_module_scopes end - + scope_words = word_scopes.select(&:named_scope?).sort_by(&:depth).map { |scope| [scope.name, scope] } variable_words = RubyLanguageServer::ScopeData::Variable.where(scope_id: scope_ids).closest_to(word).limit(5).map { |variable| [variable.name, variable.scope] } words = (scope_words + variable_words).to_h @@ -171,34 +171,34 @@ def scope_completions(word, scopes) hash[w][:parameters] = scope.parsed_parameters if scope.method? && scope.parameters.present? end end - + # Get all scopes from included modules def get_included_module_scopes(scope) included_scopes = [] module_names = scope.parsed_included_modules - + module_names.each do |module_name| # Try to find the module by full path first full_module_path = resolve_module_path(module_name, scope) module_scope = RubyLanguageServer::ScopeData::Scope.find_by(path: full_module_path, class_type: RubyLanguageServer::ScopeData::Base::TYPE_MODULE) - + # If not found by full path, try searching by name module_scope ||= RubyLanguageServer::ScopeData::Scope.where(name: module_name, class_type: RubyLanguageServer::ScopeData::Base::TYPE_MODULE).first - + if module_scope # Get all methods defined in the module included_scopes += module_scope.children.where(class_type: RubyLanguageServer::ScopeData::Base::TYPE_METHOD).to_a end end - + included_scopes end - + # Resolve the full module path considering the current scope def resolve_module_path(module_name, scope) # If it starts with ::, it's an absolute path return module_name.sub(/^::/, '') if module_name.start_with?('::') - + # Try to resolve relative to current scope if scope.parent && scope.parent.path.present? "#{scope.parent.path}::#{module_name}" diff --git a/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb b/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb index 3c96a80..3a20e48 100644 --- a/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb +++ b/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb @@ -41,7 +41,7 @@ def on_attr_writer_command(line, args, rest) ruby_command_add_attr(line, column, names, false, true) end - def on_include_command(line, args, rest) + def on_include_command(_line, _args, rest) # Extract the module name(s) from the include statement module_names = rest.flatten.select { |o| o.instance_of? String } module_names.each do |module_name| diff --git a/spec/lib/ruby_language_server/completion_spec.rb b/spec/lib/ruby_language_server/completion_spec.rb index b77926e..0960758 100644 --- a/spec/lib/ruby_language_server/completion_spec.rb +++ b/spec/lib/ruby_language_server/completion_spec.rb @@ -220,7 +220,7 @@ def bar_method it 'should track included modules in scope' do all_scopes = @include_parser.root_scope.self_and_descendants bar_class = all_scopes.find_by_path('BarClass') - + assert bar_class, 'BarClass should exist' included_modules = bar_class.parsed_included_modules assert_equal(['FooModule'], included_modules, 'BarClass should have FooModule in its included modules') @@ -229,14 +229,14 @@ def bar_method it 'should include methods from included modules in completions' do all_scopes = @include_parser.root_scope.self_and_descendants bar_method_scope = all_scopes.find_by_path('BarClass#bar_method') - + assert bar_method_scope, 'bar_method scope should exist' - + # Get completions for 'foo' prefix within BarClass#bar_method context = ['foo'] position_scopes = @include_parser.root_scope.self_and_descendants.for_line(bar_method_scope.top_line + 1) completions = scope_completions(context.last, position_scopes) - + # Should include foo_method from the included FooModule module completion_names = completions.map(&:first) assert_includes(completion_names, 'foo_method', 'Should find foo_method from included module FooModule') @@ -246,14 +246,14 @@ def bar_method it 'should include method parameters from included modules' do all_scopes = @include_parser.root_scope.self_and_descendants bar_method_scope = all_scopes.find_by_path('BarClass#bar_method') - + # Get completions for 'another_foo' prefix context = ['another_foo'] completions = RubyLanguageServer::Completion.completion(context, bar_method_scope, all_scopes) - + # Find the another_foo_method completion another_foo_item = completions[:items].find { |item| item[:label].start_with?('another_foo_method') } - + assert another_foo_item, 'another_foo_method should be in completions' assert_equal('another_foo_method(param1)', another_foo_item[:label], 'should include parameters in label') assert_equal('another_foo_method(${1:param1})', another_foo_item[:insertText], 'should include parameter snippet')