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

completion: Support completion for optional chaining (&.) #827

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
75 changes: 72 additions & 3 deletions lib/steep/services/completion_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,16 @@ def run(line:, column:)
Steep.logger.info "recovering syntax error: #{exn.inspect}"
case possible_trigger
when "."
source_text[index-1] = " "
type_check!(source_text, line: line, column: column)
items_for_dot(position: position)
if source_text[index-2] == "&"
source_text[index-1] = " "
source_text[index-2] = " "
type_check!(source_text, line: line, column: column)
items_for_qcall(position: position)
else
source_text[index-1] = " "
type_check!(source_text, line: line, column: column)
items_for_dot(position: position)
end
when "@"
source_text[index-1] = " "
type_check!(source_text, line: line, column: column)
Expand Down Expand Up @@ -329,6 +336,19 @@ def items_for_trigger(position:)

method_items_for_receiver_type(receiver_type, include_private: false, prefix: prefix, position: position, items: items)

when node.type == :csend && node.children[0] && at_end?(position, of: (_ = node.loc).selector)
# foo&.ba ←
receiver_type =
case (type = typing.type_of(node: node.children[0]))
when AST::Types::Self
context.self_type
else
unwrap_optional(type)
end
prefix = node.children[1].to_s

method_items_for_receiver_type(receiver_type, include_private: false, prefix: prefix, position: position, items: items)

when node.type == :const && node.children[0] == nil && at_end?(position, of: node.loc)
# Foo ← (const)
prefix = node.children[1].to_s
Expand Down Expand Up @@ -364,6 +384,18 @@ def items_for_trigger(position:)
# foo::← ba
items.push(*items_for_colon2(position: position))

when node.type == :csend && at_end?(position, of: (_ = node.loc).dot) && (_ = node.loc).dot.source == "&."
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
# foo&.← ba
receiver_type =
case (type = typing.type_of(node: node.children[0]))
when AST::Types::Self
context.self_type
else
unwrap_optional(type)
end

method_items_for_receiver_type(receiver_type, include_private: false, prefix: "", position: position, items: items)

when node.type == :ivar && at_end?(position, of: node.loc)
# @fo ←
instance_variable_items_for_context(context, position: position, prefix: node.children[0].to_s, items: items)
Expand Down Expand Up @@ -408,6 +440,36 @@ def items_for_dot(position:)
end
end

def items_for_qcall(position:)
# foo&. ←
shift_pos = position-2
node, *_parents = source.find_nodes(line: shift_pos.line, column: shift_pos.column)
node ||= source.node

return [] unless node

if at_end?(shift_pos, of: node.loc)
begin
context = typing.context_at(line: position.line, column: position.column)
receiver_type =
case (type = typing.type_of(node: node))
when AST::Types::Self
context.self_type
else
unwrap_optional(type)
end

items = [] #: Array[item]
method_items_for_receiver_type(receiver_type, include_private: false, prefix: "", position: position, items: items)
items
rescue Typing::UnknownNodeError
[]
end
else
[]
end
end

def items_for_colon2(position:)
# :: ←
shift_pos = position-2
Expand Down Expand Up @@ -600,6 +662,13 @@ def disallowed_method?(name)
# an LSP option
name == :initialize
end

def unwrap_optional(type)
if type.is_a?(AST::Types::Union) && type.types.include?(AST::Builtin.nil_type)
type.types.reject! { |t| t == AST::Builtin.nil_type }
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
end
type
end
end
end
end
5 changes: 5 additions & 0 deletions sig/steep/services/completion_provider.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ module Steep

def items_for_dot: (position: Position) -> Array[item]

def items_for_qcall: (position: Position) -> Array[item]

def items_for_colon2: (position: Position) -> Array[item]

def items_for_atmark: (position: Position) -> Array[item]
Expand All @@ -227,6 +229,9 @@ module Steep
def index_for: (String, line: Integer, column: Integer) -> Integer

def disallowed_method?: (Symbol name) -> bool

def unwrap_optional: (AST::Types::t) -> AST::Types::t

end
end
end
28 changes: 28 additions & 0 deletions test/completion_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,34 @@ def test_dot_trigger
end
end

def test_qcall_trigger
with_checker do
CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider|
n = [1].first
n&.to
EOR

provider.run(line: 2, column: 3).tap do |items|
assert_equal [
:class,
:is_a?,
:itself,
:nil?,
:tap,
:to_int,
:to_s,
:zero?
],
items.map(&:identifier).sort
end

provider.run(line: 2, column: 5).tap do |items|
assert_equal [:to_int, :to_s], items.map(&:identifier).sort
end
end
end
end

def test_on_atmark
with_checker <<EOF do
class Hello
Expand Down