Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add completion for global variables #2749

Merged
merged 3 commits into from
Oct 22, 2024
Merged
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
59 changes: 59 additions & 0 deletions lib/ruby_lsp/listeners/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ def initialize( # rubocop:disable Metrics/ParameterLists
:on_constant_path_node_enter,
:on_constant_read_node_enter,
:on_call_node_enter,
:on_global_variable_and_write_node_enter,
:on_global_variable_operator_write_node_enter,
:on_global_variable_or_write_node_enter,
:on_global_variable_read_node_enter,
:on_global_variable_target_node_enter,
:on_global_variable_write_node_enter,
:on_instance_variable_read_node_enter,
:on_instance_variable_write_node_enter,
:on_instance_variable_and_write_node_enter,
Expand Down Expand Up @@ -180,6 +186,36 @@ def on_call_node_enter(node)
end
end

sig { params(node: Prism::GlobalVariableAndWriteNode).void }
def on_global_variable_and_write_node_enter(node)
handle_global_variable_completion(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::GlobalVariableOperatorWriteNode).void }
def on_global_variable_operator_write_node_enter(node)
handle_global_variable_completion(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::GlobalVariableOrWriteNode).void }
def on_global_variable_or_write_node_enter(node)
handle_global_variable_completion(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::GlobalVariableReadNode).void }
def on_global_variable_read_node_enter(node)
handle_global_variable_completion(node.name.to_s, node.location)
end

sig { params(node: Prism::GlobalVariableTargetNode).void }
def on_global_variable_target_node_enter(node)
handle_global_variable_completion(node.name.to_s, node.location)
end

sig { params(node: Prism::GlobalVariableWriteNode).void }
def on_global_variable_write_node_enter(node)
handle_global_variable_completion(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::InstanceVariableReadNode).void }
def on_instance_variable_read_node_enter(node)
handle_instance_variable_completion(node.name.to_s, node.location)
Expand Down Expand Up @@ -267,6 +303,29 @@ def constant_path_completion(name, range)
end
end

sig { params(name: String, location: Prism::Location).void }
def handle_global_variable_completion(name, location)
candidates = @index.prefix_search(name)

return if candidates.none?

range = range_from_location(location)

candidates.flatten.uniq(&:name).each do |entry|
entry_name = entry.name

@response_builder << Interface::CompletionItem.new(
label: entry_name,
filter_text: entry_name,
label_details: Interface::CompletionItemLabelDetails.new(
description: entry.file_name,
),
text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
kind: Constant::CompletionItemKind::VARIABLE,
)
end
end

sig { params(name: String, location: Prism::Location).void }
def handle_instance_variable_completion(name, location)
# Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
Expand Down
8 changes: 7 additions & 1 deletion lib/ruby_lsp/requests/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class << self
def provider
Interface::CompletionOptions.new(
resolve_provider: true,
trigger_characters: ["/", "\"", "'", ":", "@", ".", "=", "<"],
trigger_characters: ["/", "\"", "'", ":", "@", ".", "=", "<", "$"],
completion_item: {
labelDetailsSupport: true,
},
Expand Down Expand Up @@ -50,6 +50,12 @@ def initialize(document, global_state, params, sorbet_level, dispatcher)
Prism::CallNode,
Prism::ConstantReadNode,
Prism::ConstantPathNode,
Prism::GlobalVariableAndWriteNode,
Prism::GlobalVariableOperatorWriteNode,
Prism::GlobalVariableOrWriteNode,
Prism::GlobalVariableReadNode,
Prism::GlobalVariableTargetNode,
Prism::GlobalVariableWriteNode,
Prism::InstanceVariableReadNode,
Prism::InstanceVariableAndWriteNode,
Prism::InstanceVariableOperatorWriteNode,
Expand Down
66 changes: 66 additions & 0 deletions test/requests/completion_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,72 @@ def test_completion_addons
end
end

def test_completion_for_global_variables
source = <<~RUBY
$qar &&= 1
$qaz += 1
$qux ||= 1
$quux, $qorge = 1
$qoo = 1

$q
$LOAD
$
RUBY

with_server(source) do |server, uri|
index = server.instance_variable_get(:@global_state).index
RubyIndexer::RBSIndexer.new(index).index_ruby_core

server.process_message(id: 1, method: "textDocument/completion", params: {
textDocument: { uri: uri },
position: { line: 6, character: 2 },
})

result = server.pop_response.response
assert_equal(["$qar", "$qaz", "$qux", "$quux", "$qorge", "$qoo"], result.map(&:label))

server.process_message(id: 1, method: "textDocument/completion", params: {
textDocument: { uri: uri },
position: { line: 7, character: 5 },
})

result = server.pop_response.response
assert_equal(["$LOADED_FEATURES", "$LOAD_PATH"], result.map(&:label))
assert_equal(["global_variables.rbs", "global_variables.rbs"], result.map { _1.label_details.description })

server.process_message(id: 1, method: "textDocument/completion", params: {
textDocument: { uri: uri },
position: { line: 8, character: 1 },
})

result = server.pop_response.response
assert_operator(result.size, :>, 40)
end
end

def test_completion_for_global_variables_show_only_uniq_entries
source = <<~RUBY
$qar &&= 1
$qar += 1
$qar ||= 1
$q
RUBY

with_server(source) do |server, uri|
index = server.instance_variable_get(:@global_state).index
RubyIndexer::RBSIndexer.new(index).index_ruby_core

server.process_message(id: 1, method: "textDocument/completion", params: {
textDocument: { uri: uri },
position: { line: 3, character: 2 },
})

result = server.pop_response.response
assert_equal(["$qar"], result.map(&:label))
end
end

def test_completion_for_instance_variables
source = +<<~RUBY
class Foo
Expand Down
Loading