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

Indicate the current or next argument on signature help #850

Merged
merged 2 commits into from
Jul 11, 2023
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
2 changes: 2 additions & 0 deletions lib/steep/server/interaction_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ def process_signature_help(job)
signatures = items.map do |item|
LSP::Interface::SignatureInformation.new(
label: "(#{item.method_type.type.param_to_s})",
parameters: item.parameters.map { |param| LSP::Interface::ParameterInformation.new(label: param)},
active_parameter: item.active_parameter,
documentation: item.comment&.yield_self do |comment|
LSP::Interface::MarkupContent.new(
kind: LSP::Constant::MarkupKind::MARKDOWN,
Expand Down
103 changes: 99 additions & 4 deletions lib/steep/services/signature_help_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ module Services
class SignatureHelpProvider
MethodCall = TypeInference::MethodCall

Item = _ = Struct.new(:method_type, :comment)
Item = _ = Struct.new(:method_type, :comment, :active_parameter) do
# @implements Item

def parameters
arguments = [] #: Array[String]
arguments.push(*method_type.type.required_positionals.map(&:to_s))
arguments.push(*method_type.type.optional_positionals.map {|p| "?#{p}"})
arguments.push("*#{self.method_type.type.rest_positionals}") if method_type.type.rest_positionals
arguments.push(*method_type.type.trailing_positionals.map(&:to_s))
arguments.push(*method_type.type.required_keywords.map {|name, param| "#{name}: #{param}" })
arguments.push(*method_type.type.optional_keywords.map {|name, param| "?#{name}: #{param}" })
arguments.push("**#{method_type.type.rest_keywords}") if method_type.type.rest_keywords
arguments
end
end

attr_reader :source, :path, :subtyping, :typing, :buffer

Expand All @@ -23,12 +37,14 @@ def run(line:, column:)
return unless nodes

typing = type_check!(line: line, column: column)
argument_nodes = [] #: Array[Parser::AST::Node]

while true
node = nodes.shift()
parent = nodes.first

node or return
argument_nodes << node

if node.type == :send || node.type == :csend
pos = buffer.loc_to_pos([line, column])
Expand All @@ -45,7 +61,8 @@ def run(line:, column:)
send_node = node
end

return signature_help_for(send_node, typing)
last_argument_nodes = last_argument_nodes_for(argument_nodes: argument_nodes, line: line, column: column)
return signature_help_for(send_node, argument_nodes, last_argument_nodes, typing)
end
end
end
Expand All @@ -58,7 +75,24 @@ def type_check!(line:, column:)
TypeCheckService.type_check(source: source, subtyping: subtyping, constant_resolver: resolver)
end

def signature_help_for(node, typing)
def last_argument_nodes_for(argument_nodes:, line:, column:)
return unless argument_nodes.last.children[2] # No arguments
return argument_nodes if argument_nodes.size > 1 # Cursor is on the last argument

pos = buffer.loc_to_pos([line, column])

while true
pos -= 1
line, column = buffer.pos_to_loc(pos)
nodes = source.find_nodes(line: line, column: column)
return unless nodes

index = nodes.index { |n| n.type == :send || n.type == :csend }
return nodes[..index] if index.to_i > 0
end
end

def signature_help_for(node, argument, last_argument, typing)
call = typing.call_of(node: node)
context = typing.context_at(line: node.loc.expression.line, column: node.loc.expression.column)

Expand All @@ -82,7 +116,8 @@ def signature_help_for(node, typing)
method.method_types.each.with_index do |method_type, i|
defn = method_type.method_decls.to_a[0]&.method_def

items << Item.new(subtyping.factory.method_type_1(method_type), defn&.comment)
active_parameter = active_parameter_for(defn&.type, argument, last_argument, node)
items << Item.new(subtyping.factory.method_type_1(method_type), defn&.comment, active_parameter)

if call.is_a?(MethodCall::Typed)
if method_type.method_decls.intersect?(call.method_decls)
Expand All @@ -98,6 +133,66 @@ def signature_help_for(node, typing)

[items, index]
end

def active_parameter_for(method_type, argument_nodes, last_argument_nodes, node)
return unless method_type

positionals = method_type.type.required_positionals.size + method_type.type.optional_positionals.size + (method_type.type.rest_positionals ? 1 : 0) + method_type.type.trailing_positionals.size
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This patch did not take care of trailing_positionals because I don't know about it.


if argument_nodes.size == 1
# Cursor is not on the argument (maybe on comma after argument)
return 0 if last_argument_nodes.nil? # No arguments

case last_argument_nodes[-2].type
when :splat
method_type.type.required_positionals.size + method_type.type.optional_positionals.size + 1 if method_type.type.rest_positionals
when :kwargs
case last_argument_nodes[-3].type
when :pair
argname = last_argument_nodes[-3].children.first.children.first
if method_type.type.required_keywords[argname]
positionals + method_type.type.required_keywords.keys.index(argname).to_i + 1
elsif method_type.type.optional_keywords[argname]
positionals + method_type.type.required_keywords.size + method_type.type.optional_keywords.keys.index(argname).to_i + 1
elsif method_type.type.rest_keywords
positionals + method_type.type.required_keywords.size + method_type.type.optional_keywords.size
end
when :kwsplat
positionals + method_type.type.required_keywords.size + method_type.type.optional_keywords.size if method_type.type.rest_keywords
end
else
pos = node.children[2...].index { |c| c.location == last_argument_nodes[-2].location }.to_i
if method_type.type.rest_positionals
[pos + 1, positionals - 1].min
else
[pos + 1, positionals].min
end
end
else
# Cursor is on the argument
case argument_nodes[-2].type
when :splat
method_type.type.required_positionals.size + method_type.type.optional_positionals.size if method_type.type.rest_positionals
when :kwargs
case argument_nodes[-3].type
when :pair
argname = argument_nodes[-3].children.first.children.first
if method_type.type.required_keywords[argname]
positionals + method_type.type.required_keywords.keys.index(argname).to_i
elsif method_type.type.optional_keywords[argname]
positionals + method_type.type.required_keywords.size + method_type.type.optional_keywords.keys.index(argname).to_i
elsif method_type.type.rest_keywords
positionals + method_type.type.required_keywords.size + method_type.type.optional_keywords.size
end
when :kwsplat
positionals + method_type.type.required_keywords.size + method_type.type.optional_keywords.size if method_type.type.rest_keywords
end
else
pos = node.children[2...].index { |c| c.location == argument_nodes[-2].location }.to_i
[pos, positionals - 1].min
end
end
end
end
end
end
14 changes: 12 additions & 2 deletions sig/steep/services/signature_help_provider.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ module Steep

attr_reader comment: RBS::AST::Comment?

def initialize: (RBS::MethodType, RBS::AST::Comment?) -> void
attr_reader active_parameter: Integer?

def initialize: (RBS::MethodType, RBS::AST::Comment?, Integer?) -> void

def parameters: () -> Array[String]
end

attr_reader source: Source
Expand All @@ -31,7 +35,13 @@ module Steep

private

def signature_help_for: (Parser::AST::Node, Typing) -> [Array[Item], Integer?]?
def active_parameter_for: (RBS::MethodType?, Array[Parser::AST::Node], Array[Parser::AST::Node]?, Parser::AST::Node) -> Integer?

def arguments_for: (RBS::MethodType) -> Array[String]

def last_argument_nodes_for: (argument_nodes: Array[Parser::AST::Node], line: Integer, column: Integer) -> Array[Parser::AST::Node]?

def signature_help_for: (Parser::AST::Node, Array[Parser::AST::Node], Array[Parser::AST::Node]?, Typing) -> [Array[Item], Integer?]?

def type_check!: (line: Integer, column: Integer) -> Typing
end
Expand Down
96 changes: 96 additions & 0 deletions test/signature_help_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,100 @@ def self.foo: (String, Integer) -> Array[Symbol]
end
end
end

def test_active_parameter
with_checker(<<~RBS) do
class TestClass
def self.foo: (String, Integer, *String, kw1: String, kw2: Integer, **String) -> void
end
RBS
source = Source.parse(<<~RUBY, path: Pathname("a.rb"), factory: checker.factory)
TestClass.foo("", 123, "", "", kw1: "", kw2: 456, kw3: "", kw4: "", **kwargs)
# 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75
RUBY

SignatureHelpProvider.new(source: source, subtyping: checker).tap do |provider|
items, index = provider.run(line: 1, column: 14)
assert_equal 0, items.first.active_parameter

items, index = provider.run(line: 1, column: 18)
assert_equal 1, items.first.active_parameter

items, index = provider.run(line: 1, column: 23)
assert_equal 2, items.first.active_parameter

items, index = provider.run(line: 1, column: 27)
assert_equal 2, items.first.active_parameter

items, index = provider.run(line: 1, column: 31)
assert_equal 3, items.first.active_parameter

items, index = provider.run(line: 1, column: 40)
assert_equal 4, items.first.active_parameter

items, index = provider.run(line: 1, column: 50)
assert_equal 5, items.first.active_parameter

items, index = provider.run(line: 1, column: 59)
assert_equal 5, items.first.active_parameter

items, index = provider.run(line: 1, column: 68)
assert_equal 5, items.first.active_parameter
end
end
end

def test_active_parameter_in_typing
with_checker(<<~RBS) do
class TestClass
def self.foo: (String, Integer, *String, kw1: String, kw2: Integer, **String) -> void
end
RBS
source = Source.parse(<<~RUBY, path: Pathname("a.rb"), factory: checker.factory)
TestClass.foo()
TestClass.foo("",)
TestClass.foo("", "",)
TestClass.foo("", "", "",)
TestClass.foo("", "", "", "",)
TestClass.foo("", "", "", "", *args,)
TestClass.foo("", "", "", "", *args, kw1: true,)
TestClass.foo("", "", "", "", *args, kw2: true,)
TestClass.foo("", "", "", "", *args, kw3: true,)
TestClass.foo("", "", "", "", *args, kw1: true, **kwargs,)
# 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75
RUBY

SignatureHelpProvider.new(source: source, subtyping: checker).tap do |provider|
items, index = provider.run(line: 1, column: 14)
assert_equal 0, items.first.active_parameter

items, index = provider.run(line: 2, column: 17)
assert_equal 1, items.first.active_parameter

items, index = provider.run(line: 3, column: 21)
assert_equal 2, items.first.active_parameter

items, index = provider.run(line: 4, column: 25)
assert_equal 2, items.first.active_parameter

items, index = provider.run(line: 5, column: 29)
assert_equal 2, items.first.active_parameter

items, index = provider.run(line: 6, column: 36)
assert_equal 3, items.first.active_parameter

items, index = provider.run(line: 7, column: 47)
assert_equal 4, items.first.active_parameter

items, index = provider.run(line: 8, column: 47)
assert_equal 5, items.first.active_parameter

items, index = provider.run(line: 9, column: 47)
assert_equal 5, items.first.active_parameter

items, index = provider.run(line: 10, column: 57)
assert_equal 5, items.first.active_parameter
end
end
end
end