lib/steep/services/signature_help_provider.rb in steep-1.5.0.pre.5 vs lib/steep/services/signature_help_provider.rb in steep-1.5.0.pre.6

- old
+ new

@@ -1,12 +1,26 @@ module Steep 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 def env subtyping.factory.env end @@ -21,16 +35,18 @@ nodes = source.find_nodes(line: line, column: 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]) begin_loc = (_ = node.loc).begin #: Parser::Source::Range? end_loc = (_ = node.loc).end #: Parser::Source::Range? @@ -38,16 +54,17 @@ if begin_loc && end_loc if begin_loc.end_pos <= pos && pos <= end_loc.begin_pos # Given position is between open/close parens of args of send node if parent && (parent.type == :block || parent.type == :numblock) - send_node = parent.children[0] + send_node = parent else 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 end end @@ -56,11 +73,28 @@ source = self.source.without_unrelated_defs(line: line, column: column) resolver = RBS::Resolver::ConstantResolver.new(builder: subtyping.factory.definition_builder) 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) items = [] #: Array[Item] index = nil #: Integer? @@ -80,11 +114,12 @@ 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 - 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) index = i end @@ -95,9 +130,69 @@ when MethodCall::Untyped, MethodCall::NoMethodError return end [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