Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions lib/ruby_language_server/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
16 changes: 16 additions & 0 deletions lib/ruby_language_server/scope_data/scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions spec/lib/ruby_language_server/completion_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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