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

Support completion for keyword arguments #851

Merged
merged 1 commit into from
Jul 11, 2023
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
11 changes: 11 additions & 0 deletions lib/steep/server/interaction_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,17 @@ def format_completion_item(item)
new_text: item.identifier.to_s
)
)
when Services::CompletionProvider::KeywordArgumentItem
LSP::Interface::CompletionItem.new(
label: item.identifier.to_s,
kind: LSP::Constant::CompletionItemKind::FIELD,
label_details: LSP::Interface::CompletionItemLabelDetails.new(description: 'Keyword argument'),
documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) },
text_edit: LSP::Interface::TextEdit.new(
range: range,
new_text: item.identifier.to_s
)
)
when Services::CompletionProvider::TypeNameItem
kind =
case
Expand Down
67 changes: 65 additions & 2 deletions lib/steep/services/completion_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def -(size)
Range = _ = Struct.new(:start, :end, keyword_init: true)

InstanceVariableItem = _ = Struct.new(:identifier, :range, :type, keyword_init: true)
KeywordArgumentItem = _ = Struct.new(:identifier, :range, keyword_init: true)
LocalVariableItem = _ = Struct.new(:identifier, :range, :type, keyword_init: true)
ConstantItem = _ = Struct.new(:env, :identifier, :range, :type, :full_name, keyword_init: true) do
# @implements ConstantItem
Expand Down Expand Up @@ -267,7 +268,9 @@ def run(line:, column:)
[]
end
else
[]
items = [] #: Array[item]
items_for_following_keyword_arguments(source_text, index: index, line: line, column: column, items: items)
items
end
end
end
Expand Down Expand Up @@ -302,7 +305,7 @@ def range_for(position, prefix: "")
end

def items_for_trigger(position:)
node, *_parents = source.find_nodes(line: position.line, column: position.column)
node, *parents = source.find_nodes(line: position.line, column: position.column)
node ||= source.node

return [] unless node
Expand All @@ -318,6 +321,7 @@ def items_for_trigger(position:)

method_items_for_receiver_type(context.self_type, include_private: true, prefix: prefix, position: position, items: items)
local_variable_items_for_context(context, position: position, prefix: prefix, items: items)
keyword_argument_items_for_method(node: parents&.first, position: position, prefix: prefix, items: items)

when node.type == :lvar && at_end?(position, of: node.loc)
# foo ← (lvar)
Expand Down Expand Up @@ -527,6 +531,30 @@ def items_for_rbs(position:, buffer:)
items
end

def items_for_following_keyword_arguments(text, index:, line:, column:, items:)
return if text[index - 1] !~ /[a-zA-Z0-9]/

text = text.dup
argname = [] #: Array[String]
while text[index - 1] =~ /[a-zA-Z0-9]/
argname.unshift(text[index - 1] || '')
source_text[index - 1] = " "
index -= 1
end

begin
type_check!(source_text, line: line, column: column)
rescue Parser::SyntaxError
return
end

node, *_parents = source.find_nodes(line: line, column: column)
if node && (node.type == :send || node.type == :csend)
position = Position.new(line: line, column: column)
keyword_argument_items_for_method(node: node, position: position, prefix: argname.join, items: items)
end
end

def method_items_for_receiver_type(type, include_private:, prefix:, position:, items:)
range = range_for(position, prefix: prefix)
context = typing.context_at(line: position.line, column: position.column)
Expand Down Expand Up @@ -641,6 +669,41 @@ def instance_variable_items_for_context(context, position:, prefix:, items:)
end
end

def keyword_argument_items_for_method(node:, position:, prefix:, items:)
return unless node && (node.type == :send || node.type == :csend)

call = typing.call_of(node: node)
case call
when TypeInference::MethodCall::Typed, TypeInference::MethodCall::Error
type = call.receiver_type
shape = subtyping.builder.shape(
type,
public_only: !node.children[0].nil?,
config: Interface::Builder::Config.new(self_type: type, class_type: nil, instance_type: nil, variable_bounds: {})
)
if shape
if method = shape.methods[call.method_name]
method.method_types.each.with_index do |method_type, i|
defn = method_type.method_decls.to_a[0]&.method_def
if defn
range = range_for(position, prefix: prefix)
kwargs = node.children[2...]&.find { |arg| arg.type == :kwargs }&.children || []
used_kwargs = kwargs.filter_map { |arg| arg.type == :pair && arg.children.first.children.first }

kwargs = defn.type.type.required_keywords.keys + defn.type.type.optional_keywords.keys
kwargs.each do |name|
if name.to_s.start_with?(prefix) && !used_kwargs.include?(name)
items << KeywordArgumentItem.new(identifier: "#{name}:", range: range)
end
end
end
end
end
end
end
end


def index_for(string, line:, column:)
index = 0

Expand Down
13 changes: 13 additions & 0 deletions sig/steep/services/completion_provider.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ module Steep
def initialize: (identifier: Symbol, range: Range, type: AST::Types::t) -> void
end

class KeywordArgumentItem
attr_reader identifier: String

attr_reader range: Range

def initialize: (identifier: String, range: Range) -> void
end

class LocalVariableItem
attr_reader identifier: Symbol

Expand Down Expand Up @@ -169,6 +177,7 @@ module Steep
end

type item = InstanceVariableItem
| KeywordArgumentItem
| LocalVariableItem
| ConstantItem
| SimpleMethodNameItem
Expand Down Expand Up @@ -216,6 +225,8 @@ module Steep

def items_for_rbs: (position: Position, buffer: RBS::Buffer) -> Array[item]

def items_for_following_keyword_arguments: (String text, index: Integer, line: Integer, column: Integer, items: Array[item]) -> void

def method_items_for_receiver_type: (AST::Types::t, include_private: bool, prefix: String, position: Position, items: Array[item]) -> void

def word_name?: (String name) -> bool
Expand All @@ -226,6 +237,8 @@ module Steep

def instance_variable_items_for_context: (TypeInference::Context context, position: Position, prefix: String, items: Array[item]) -> void

def keyword_argument_items_for_method: (node: Parser::AST::Node?, position: Position, prefix: String, items: Array[item]) -> void

def index_for: (String, line: Integer, column: Integer) -> Integer

def disallowed_method?: (Symbol name) -> bool
Expand Down
34 changes: 34 additions & 0 deletions test/completion_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -547,4 +547,38 @@ def test_on_steep_type_application
end
end
end

def test_first_keyword_argument
with_checker <<EOF do
class TestClass
def foo: (arg1: Integer, arg2: Integer, ?arg3: Integer, ?arg4: Integer) -> void
end
EOF
CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider|
TestClass.new.foo(a)
EOR

provider.run(line: 1, column: 19).tap do |items|
assert_equal ["arg1:", "arg2:", "arg3:", "arg4:"], items.map(&:identifier)
end
end
end
end

def test_following_keyword_argument
with_checker <<EOF do
class TestClass
def foo: (arg1: Integer, arg2: Integer, ?arg3: Integer, ?arg4: Integer) -> void
end
EOF
CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider|
TestClass.new.foo(arg1: 1, a)
EOR

provider.run(line: 1, column: 28).tap do |items|
assert_equal ["arg2:", "arg3:", "arg4:"], items.map(&:identifier)
end
end
end
end
end