Skip to content

Commit 9e8d313

Browse files
Copilotkwerle
andcommitted
Add support for module inclusion in completions
Co-authored-by: kwerle <23320+kwerle@users.noreply.github.com>
1 parent b3e59ad commit 9e8d313

5 files changed

Lines changed: 133 additions & 0 deletions

File tree

lib/db/schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def write(*args)
2020
t.string :class_type, null: false
2121
t.text :parameters # JSON string of method parameters
2222
t.boolean :class_method, default: false # true for class methods (def self.method)
23+
t.text :included_modules # JSON array of included module names
2324
end
2425

2526
add_index :scopes, :name

lib/ruby_language_server/completion.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ def scope_completions(word, scopes)
153153

154154
scope_ids = scopes.map(&:id)
155155
word_scopes = scopes.to_a + RubyLanguageServer::ScopeData::Scope.where(parent_id: scope_ids).closest_to(word).limit(5)
156+
157+
# Add methods from included modules
158+
scopes.each do |scope|
159+
included_module_scopes = get_included_module_scopes(scope)
160+
word_scopes += included_module_scopes
161+
end
162+
156163
scope_words = word_scopes.select(&:named_scope?).sort_by(&:depth).map { |scope| [scope.name, scope] }
157164
variable_words = RubyLanguageServer::ScopeData::Variable.where(scope_id: scope_ids).closest_to(word).limit(5).map { |variable| [variable.name, variable.scope] }
158165
words = (scope_words + variable_words).to_h
@@ -164,6 +171,41 @@ def scope_completions(word, scopes)
164171
hash[w][:parameters] = scope.parsed_parameters if scope.method? && scope.parameters.present?
165172
end
166173
end
174+
175+
# Get all scopes from included modules
176+
def get_included_module_scopes(scope)
177+
included_scopes = []
178+
module_names = scope.parsed_included_modules
179+
180+
module_names.each do |module_name|
181+
# Try to find the module by full path first
182+
full_module_path = resolve_module_path(module_name, scope)
183+
module_scope = RubyLanguageServer::ScopeData::Scope.find_by(path: full_module_path, class_type: RubyLanguageServer::ScopeData::Base::TYPE_MODULE)
184+
185+
# If not found by full path, try searching by name
186+
module_scope ||= RubyLanguageServer::ScopeData::Scope.where(name: module_name, class_type: RubyLanguageServer::ScopeData::Base::TYPE_MODULE).first
187+
188+
if module_scope
189+
# Get all methods defined in the module
190+
included_scopes += module_scope.children.where(class_type: RubyLanguageServer::ScopeData::Base::TYPE_METHOD).to_a
191+
end
192+
end
193+
194+
included_scopes
195+
end
196+
197+
# Resolve the full module path considering the current scope
198+
def resolve_module_path(module_name, scope)
199+
# If it starts with ::, it's an absolute path
200+
return module_name.sub(/^::/, '') if module_name.start_with?('::')
201+
202+
# Try to resolve relative to current scope
203+
if scope.parent && scope.parent.path.present?
204+
"#{scope.parent.path}::#{module_name}"
205+
else
206+
module_name
207+
end
208+
end
167209
end
168210
end
169211
end

lib/ruby_language_server/scope_data/scope.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ def set_parameters(params_array)
9494
self.parameters = params_array.to_json if params_array.present?
9595
end
9696

97+
# Get included modules as an array of strings
98+
def parsed_included_modules
99+
return [] unless included_modules.present?
100+
101+
JSON.parse(included_modules)
102+
rescue JSON::ParserError
103+
[]
104+
end
105+
106+
# Add an included module name
107+
def add_included_module(module_name)
108+
current = parsed_included_modules
109+
current << module_name unless current.include?(module_name)
110+
self.included_modules = current.to_json
111+
end
112+
97113
# Called from ScopeParser to cleanup empty blocks.
98114
def close
99115
destroy! if block_scope? && variables.none?

lib/ruby_language_server/scope_parser_commands/ruby_commands.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ def on_attr_writer_command(line, args, rest)
4141
ruby_command_add_attr(line, column, names, false, true)
4242
end
4343

44+
def on_include_command(line, args, rest)
45+
# Extract the module name(s) from the include statement
46+
module_names = rest.flatten.select { |o| o.instance_of? String }
47+
module_names.each do |module_name|
48+
@current_scope.add_included_module(module_name)
49+
@current_scope.save!
50+
end
51+
end
52+
4453
private
4554

4655
def ruby_command_names(rest)

spec/lib/ruby_language_server/completion_spec.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,69 @@ def method_mixed(required, optional = nil, *rest, keyword:, **kwargs, &block)
194194
assert_equal('arg1 (required), arg2 (required)', simple_method_item[:detail], 'should include parameter details')
195195
end
196196
end
197+
198+
describe 'module inclusion' do
199+
before do
200+
@code_with_include = <<-SOURCE
201+
module FooModule
202+
def foo_method
203+
end
204+
205+
def another_foo_method(param1)
206+
end
207+
end
208+
209+
class BarClass
210+
include FooModule
211+
212+
def bar_method
213+
foo # completion should find foo_method from included FooModule module
214+
end
215+
end
216+
SOURCE
217+
@include_parser = RubyLanguageServer::ScopeParser.new(@code_with_include)
218+
end
219+
220+
it 'should track included modules in scope' do
221+
all_scopes = @include_parser.root_scope.self_and_descendants
222+
bar_class = all_scopes.find_by_path('BarClass')
223+
224+
assert bar_class, 'BarClass should exist'
225+
included_modules = bar_class.parsed_included_modules
226+
assert_equal(['FooModule'], included_modules, 'BarClass should have FooModule in its included modules')
227+
end
228+
229+
it 'should include methods from included modules in completions' do
230+
all_scopes = @include_parser.root_scope.self_and_descendants
231+
bar_method_scope = all_scopes.find_by_path('BarClass#bar_method')
232+
233+
assert bar_method_scope, 'bar_method scope should exist'
234+
235+
# Get completions for 'foo' prefix within BarClass#bar_method
236+
context = ['foo']
237+
position_scopes = @include_parser.root_scope.self_and_descendants.for_line(bar_method_scope.top_line + 1)
238+
completions = scope_completions(context.last, position_scopes)
239+
240+
# Should include foo_method from the included FooModule module
241+
completion_names = completions.map(&:first)
242+
assert_includes(completion_names, 'foo_method', 'Should find foo_method from included module FooModule')
243+
assert_includes(completion_names, 'another_foo_method', 'Should find another_foo_method from included module FooModule')
244+
end
245+
246+
it 'should include method parameters from included modules' do
247+
all_scopes = @include_parser.root_scope.self_and_descendants
248+
bar_method_scope = all_scopes.find_by_path('BarClass#bar_method')
249+
250+
# Get completions for 'another_foo' prefix
251+
context = ['another_foo']
252+
completions = RubyLanguageServer::Completion.completion(context, bar_method_scope, all_scopes)
253+
254+
# Find the another_foo_method completion
255+
another_foo_item = completions[:items].find { |item| item[:label].start_with?('another_foo_method') }
256+
257+
assert another_foo_item, 'another_foo_method should be in completions'
258+
assert_equal('another_foo_method(param1)', another_foo_item[:label], 'should include parameters in label')
259+
assert_equal('another_foo_method(${1:param1})', another_foo_item[:insertText], 'should include parameter snippet')
260+
end
261+
end
197262
end

0 commit comments

Comments
 (0)