diff --git a/lib/ruby_lsp/listeners/completion.rb b/lib/ruby_lsp/listeners/completion.rb index 5c379267f..84898bb15 100644 --- a/lib/ruby_lsp/listeners/completion.rb +++ b/lib/ruby_lsp/listeners/completion.rb @@ -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, @@ -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) @@ -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 diff --git a/lib/ruby_lsp/requests/completion.rb b/lib/ruby_lsp/requests/completion.rb index 7dd27a851..5ed77c0df 100644 --- a/lib/ruby_lsp/requests/completion.rb +++ b/lib/ruby_lsp/requests/completion.rb @@ -17,7 +17,7 @@ class << self def provider Interface::CompletionOptions.new( resolve_provider: true, - trigger_characters: ["/", "\"", "'", ":", "@", ".", "=", "<"], + trigger_characters: ["/", "\"", "'", ":", "@", ".", "=", "<", "$"], completion_item: { labelDetailsSupport: true, }, @@ -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, diff --git a/test/requests/completion_test.rb b/test/requests/completion_test.rb index f1087c596..66b5e34bb 100644 --- a/test/requests/completion_test.rb +++ b/test/requests/completion_test.rb @@ -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