Skip to content

Commit

Permalink
Indicate the current or next argument on signature help
Browse files Browse the repository at this point in the history
  • Loading branch information
tk0miya committed Jul 8, 2023
1 parent be19744 commit 43df0f5
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 6 deletions.
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

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

0 comments on commit 43df0f5

Please sign in to comment.