From debe44ba70ea095611a2df6a7409ef828b2d6d33 Mon Sep 17 00:00:00 2001 From: meilin Date: Thu, 30 Apr 2026 15:29:50 +0800 Subject: [PATCH] Fix the bug that has existed in Solargraph since version 0.54.1: When autocompleting a function and typing a parenthesis triggers signatureHelp, the feature fails (no signatureHelp is shown) because the parenthesis is not closed. After the fix, signatureHelp should be displayed even when the parenthesis is not closed. --- .../parser/parser_gem/node_methods.rb | 106 +++++++++++++++++- lib/solargraph/source/cursor.rb | 12 +- lib/solargraph/source_map/clip.rb | 7 +- 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b77c4cd47..e6f289d84 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -237,15 +237,113 @@ def find_recipient_node cursor end prev = node end - nil + find_recipient_node_by_text(source, offset) + end + + # Text-based fallback for finding a method call recipient when the AST + # is unavailable (e.g., unparseable source with syntax errors). + # + # Scans backward from cursor offset to find '(' and the method name + # before it, then creates a minimal :send node. + # + # @param source [Solargraph::Source] + # @param offset [Integer] + # @return [Parser::AST::Node, nil] + def find_recipient_node_by_text source, offset + code = source.code + return nil if offset.nil? || offset <= 0 || offset > code.length + + # The '(' could be at offset-1 (cursor after '(') or at offset (cursor on '(') + start_pos = offset - 1 + if start_pos > 0 && code[start_pos] != '(' && code[offset] == '(' + start_pos = offset + end + + # Scan backward to find the matching '(' (handle nested parens) + depth = 0 + paren_pos = nil + pos = start_pos + while pos >= 0 + case code[pos] + when ')' + depth += 1 + when '(' + if depth == 0 + paren_pos = pos + break + end + depth -= 1 + end + pos -= 1 + end + return nil if paren_pos.nil? + + # Skip whitespace before '(' to find method name + idx = paren_pos - 1 + while idx >= 0 && code[idx] =~ /\s/ + idx -= 1 + end + return nil if idx < 0 + + # Read method name (including ? and !) + name_end = idx + 1 + while idx >= 0 && code[idx] =~ /[a-zA-Z0-9_?!]/ + idx -= 1 + end + name_start = idx + 1 + return nil if name_start >= name_end + method_name = code[name_start...name_end] + return nil if method_name.empty? + + # Check for receiver pattern: receiver.method( or receiver::method( + idx = name_start - 1 + while idx >= 0 && code[idx] =~ /\s/ + idx -= 1 + end + if idx >= 0 && code[idx] == '.' + dot_pos = idx + idx -= 1 + while idx >= 0 && code[idx] =~ /\s/ + idx -= 1 + end + recv_end = idx + 1 + while idx >= 0 && code[idx] =~ /[a-zA-Z0-9_@$]/ + idx -= 1 + end + recv_start = idx + 1 + if recv_start < recv_end + recv_name = code[recv_start...recv_end] + unless recv_name.empty? + receiver_node = ::Parser::AST::Node.new(:send, [nil, recv_name.to_sym]) + return ::Parser::AST::Node.new(:send, [receiver_node, method_name.to_sym]) + end + end + elsif idx >= 0 && idx > 0 && code[idx-1] == ':' && code[idx] == ':' + const_end = idx - 1 + const_start = const_end + while const_start > 0 && code[const_start-1] =~ /[a-zA-Z0-9_]/ + const_start -= 1 + end + const_name = code[const_start...const_end] + unless const_name.empty? || method_name.empty? + const_node = ::Parser::AST::Node.new(:const, [nil, const_name.to_sym]) + return ::Parser::AST::Node.new(:send, [const_node, method_name.to_sym]) + end + end + + # Simple method call without receiver + ::Parser::AST::Node.new(:send, [nil, method_name.to_sym]) end # @param cursor [Solargraph::Source::Cursor] # @return [Parser::AST::Node, nil] def repaired_find_recipient_node cursor - cursor = cursor.source.cursor_at([cursor.position.line, cursor.position.column - 1]) - node = cursor.source.tree_at(cursor.position.line, cursor.position.column).first - return node if node && node.type == :send + c = cursor.source.cursor_at([cursor.position.line, cursor.position.column - 1]) + tree = c.source.tree_at(c.position.line, c.position.column) + tree.each do |node| + return node if node.type == :send + end + find_recipient_node_by_text(cursor.source, cursor.offset) end # diff --git a/lib/solargraph/source/cursor.rb b/lib/solargraph/source/cursor.rb index a8226eb07..9e44030b0 100644 --- a/lib/solargraph/source/cursor.rb +++ b/lib/solargraph/source/cursor.rb @@ -110,7 +110,17 @@ def string? def recipient @recipient ||= begin node = recipient_node - node ? Cursor.new(source, Range.from_node(node).ending) : nil + if node.nil? + nil + else + rng = Range.from_node(node) + if rng + Cursor.new(source, rng.ending) + else + pos = Position.new(position.line, [position.column - 1, 0].max) + Cursor.new(source, pos) + end + end end end alias receiver recipient diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index 3d198ac1e..971855bfd 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -44,8 +44,13 @@ def complete # @return [Array] def signify return [] unless cursor.argument? + return [] if cursor.recipient_node.nil? chain = Parser.chain(cursor.recipient_node, cursor.filename) - chain.define(api_map, context_pin, locals).select { |pin| pin.is_a?(Pin::Method) } + name_pin = context_pin + if name_pin.nil? + name_pin = Pin::ProxyType.anonymous(ComplexType.try_parse('::Object')) + end + chain.define(api_map, name_pin, locals).select { |pin| pin.is_a?(Pin::Method) } end # @return [ComplexType]