Skip to content

Commit

Permalink
Add local completion
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jul 4, 2024
1 parent a4391c6 commit c05f9d9
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 29 deletions.
38 changes: 14 additions & 24 deletions lib/ruby_lsp/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,18 @@ def locate(node, char_position, node_types: [])
parent = T.let(nil, T.nilable(Prism::Node))
nesting_nodes = T.let(
[],
T::Array[T.any(Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode)],
T::Array[T.any(
Prism::ClassNode,
Prism::ModuleNode,
Prism::SingletonClassNode,
Prism::DefNode,
Prism::BlockNode,
Prism::LambdaNode,
Prism::ProgramNode,
)],
)

nesting_nodes << node if node.is_a?(Prism::ProgramNode)
call_node = T.let(nil, T.nilable(Prism::CallNode))

until queue.empty?
Expand Down Expand Up @@ -148,11 +158,8 @@ def locate(node, char_position, node_types: [])
# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
# target when it is a constant
case candidate
when Prism::ClassNode, Prism::ModuleNode
nesting_nodes << candidate
when Prism::SingletonClassNode
nesting_nodes << candidate
when Prism::DefNode
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode,
Prism::LambdaNode
nesting_nodes << candidate
end

Expand Down Expand Up @@ -193,24 +200,7 @@ def locate(node, char_position, node_types: [])
end
end

nesting = []
surrounding_method = T.let(nil, T.nilable(String))

nesting_nodes.each do |node|
case node
when Prism::ClassNode, Prism::ModuleNode
nesting << node.constant_path.slice
when Prism::SingletonClassNode
nesting << "<Class:#{nesting.last}>"
when Prism::DefNode
surrounding_method = node.name.to_s
next unless node.receiver.is_a?(Prism::SelfNode)

nesting << "<Class:#{nesting.last}>"
end
end

NodeContext.new(closest, parent, nesting, call_node, surrounding_method)
NodeContext.new(closest, parent, nesting_nodes, call_node)
end

sig { returns(T::Boolean) }
Expand Down
24 changes: 24 additions & 0 deletions lib/ruby_lsp/listeners/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ def complete_require_relative(node)

sig { params(node: Prism::CallNode, name: String).void }
def complete_methods(node, name)
add_local_completions(node, name)

type = @type_inferrer.infer_receiver_type(@node_context)
return unless type

Expand Down Expand Up @@ -317,6 +319,28 @@ def complete_methods(node, name)
# We have not indexed this namespace, so we can't provide any completions
end

sig { params(node: Prism::CallNode, name: String).void }
def add_local_completions(node, name)
return if @global_state.has_type_checker

range = range_from_location(T.must(node.message_loc))

@node_context.locals_for_scope.each do |local|
local_name = local.to_s
next unless local_name.start_with?(name)

@response_builder << Interface::CompletionItem.new(
label: local_name,
filter_text: local_name,
text_edit: Interface::TextEdit.new(range: range, new_text: local_name),
kind: Constant::CompletionItemKind::VARIABLE,
data: {
skip_resolve: true,
},
)
end
end

sig { params(label: String, node: Prism::StringNode).returns(Interface::CompletionItem) }
def build_completion(label, node)
# We should use the content location as we only replace the content and not the delimiters of the string
Expand Down
70 changes: 65 additions & 5 deletions lib/ruby_lsp/node_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,82 @@ class NodeContext
params(
node: T.nilable(Prism::Node),
parent: T.nilable(Prism::Node),
nesting: T::Array[String],
nesting_nodes: T::Array[T.any(
Prism::ClassNode,
Prism::ModuleNode,
Prism::SingletonClassNode,
Prism::DefNode,
Prism::BlockNode,
Prism::LambdaNode,
Prism::ProgramNode,
)],
call_node: T.nilable(Prism::CallNode),
surrounding_method: T.nilable(String),
).void
end
def initialize(node, parent, nesting, call_node, surrounding_method)
def initialize(node, parent, nesting_nodes, call_node)
@node = node
@parent = parent
@nesting = nesting
@nesting_nodes = nesting_nodes
@call_node = call_node
@surrounding_method = surrounding_method

nesting, surrounding_method = handle_nesting_nodes(nesting_nodes)
@nesting = T.let(nesting, T::Array[String])
@surrounding_method = T.let(surrounding_method, T.nilable(String))
end

sig { returns(String) }
def fully_qualified_name
@fully_qualified_name ||= T.let(@nesting.join("::"), T.nilable(String))
end

sig { returns(T::Array[Symbol]) }
def locals_for_scope
locals = []

@nesting_nodes.each do |node|
if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) || node.is_a?(Prism::SingletonClassNode) ||
node.is_a?(Prism::DefNode)
locals.clear
end

locals.concat(node.locals)
end

locals
end

private

sig do
params(nodes: T::Array[T.any(
Prism::ClassNode,
Prism::ModuleNode,
Prism::SingletonClassNode,
Prism::DefNode,
Prism::BlockNode,
Prism::LambdaNode,
Prism::ProgramNode,
)]).returns([T::Array[String], T.nilable(String)])
end
def handle_nesting_nodes(nodes)
nesting = []
surrounding_method = T.let(nil, T.nilable(String))

@nesting_nodes.each do |node|
case node
when Prism::ClassNode, Prism::ModuleNode
nesting << node.constant_path.slice
when Prism::SingletonClassNode
nesting << "<Class:#{nesting.last}>"
when Prism::DefNode
surrounding_method = node.name.to_s
next unless node.receiver.is_a?(Prism::SelfNode)

nesting << "<Class:#{nesting.last}>"
end
end

[nesting, surrounding_method]
end
end
end
2 changes: 2 additions & 0 deletions lib/ruby_lsp/requests/completion_resolve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def initialize(global_state, item)

sig { override.returns(T::Hash[Symbol, T.untyped]) }
def perform
return @item if @item.dig(:data, :skip_resolve)

# Based on the spec https://microsoft.github.io/language-server-protocol/specification#textDocument_completion,
# a completion resolve request must always return the original completion item without modifying ANY fields
# other than detail and documentation (NOT labelDetails). If we modify anything, the completion behaviour might
Expand Down
55 changes: 55 additions & 0 deletions test/requests/completion_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,61 @@ def do_something
end
end

def test_completion_for_locals
source = +<<~RUBY
class Child
abc0 = 42
def do_something(abc1, abc2, abc3)
a
[].each do |abc4, abc5|
a
end
end
a
end
abc = 12
a
RUBY

with_server(source, stub_no_typechecker: true) do |server, uri|
server.process_message(id: 1, method: "textDocument/completion", params: {
textDocument: { uri: uri },
position: { line: 4, character: 5 },
})

result = server.pop_response.response
assert_equal(["abc1", "abc2", "abc3"], result.map(&:label))

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

result = server.pop_response.response
assert_equal(["abc1", "abc2", "abc3", "abc4", "abc5"], result.map(&:label))

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

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

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

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

private

def with_file_structure(server, &block)
Expand Down

0 comments on commit c05f9d9

Please sign in to comment.