From 11f521c7683cdad4b26d9116d58a7c8c861ffd4a Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 22 Aug 2025 16:09:22 +0200 Subject: [PATCH] [ty] Close signature help after `)` (#20017) --- crates/ty_ide/src/signature_help.rs | 118 ++++++++++++++++++++++------ 1 file changed, 95 insertions(+), 23 deletions(-) diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index 4700f7e21b..e9f775e6d6 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -12,6 +12,7 @@ use crate::{Db, find_node::covering_node}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::{self as ast, AnyNodeRef}; +use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::ResolvedDefinition; use ty_python_semantic::SemanticModel; @@ -25,7 +26,7 @@ use ty_python_semantic::types::{ // associated with the __new__ or __init__ call. /// Information about a function parameter -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ParameterDetails { /// The parameter name (e.g., "param1") pub name: String, @@ -37,7 +38,7 @@ pub struct ParameterDetails { } /// Information about a function signature -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SignatureDetails { /// Text representation of the full signature (including input parameters and return type). pub label: String, @@ -51,7 +52,7 @@ pub struct SignatureDetails { } /// Signature help information for function calls -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SignatureHelpInfo { /// Information about each of the signatures for the function call. We /// need to handle multiple because of unions, overloads, and composite @@ -104,22 +105,42 @@ fn get_call_expr( ) -> Option<(&ast::ExprCall, usize)> { let root_node: AnyNodeRef = parsed.syntax().into(); - // Create a range from the offset for the covering_node function. - // Use length 1 if it fits within the root node, otherwise use zero-length range. - let one_char_range = TextRange::at(offset, TextSize::from(1)); - let range = if root_node.range().contains_range(one_char_range) { - one_char_range - } else { - TextRange::at(offset, TextSize::from(0)) - }; + // Find the token under the cursor and use its offset to find the node + let token = parsed + .tokens() + .at_offset(offset) + .max_by_key(|token| match token.kind() { + TokenKind::Name + | TokenKind::String + | TokenKind::Complex + | TokenKind::Float + | TokenKind::Int => 1, + _ => 0, + })?; // Find the covering node at the given position that is a function call. - let covering_node = covering_node(root_node, range) - .find_first(|node| matches!(node, AnyNodeRef::ExprCall(_))) + let call = covering_node(root_node, token.range()) + .find_first(|node| { + if !node.is_expr_call() { + return false; + } + + // Close the signature help if the cursor is at the closing parenthesis + if token.kind() == TokenKind::Rpar && node.end() == token.end() && offset == token.end() + { + return false; + } + + if token.range().is_empty() && node.end() == token.end() { + return false; + } + + true + }) .ok()?; // Get the function call expression. - let AnyNodeRef::ExprCall(call_expr) = covering_node.node() else { + let AnyNodeRef::ExprCall(call_expr) = call.node() else { return None; }; @@ -247,11 +268,11 @@ mod tests { r#" def example_function(param1: str, param2: int) -> str: """This is a docstring for the example function. - + Args: param1: The first parameter as a string param2: The second parameter as an integer - + Returns: A formatted string combining both parameters """ @@ -627,7 +648,7 @@ mod tests { r#" def documented_function(param1: str, param2: int) -> str: """This is a function with parameter documentation. - + Args: param1: The first parameter description param2: The second parameter description @@ -706,6 +727,57 @@ mod tests { assert_eq!(result.active_signature, Some(0)); } + #[test] + fn signature_help_after_closing_paren_at_end_of_file() { + let test = cursor_test( + r#" + def test(a: int) -> int: + return 10 + + test("test")"#, + ); + + // Should not return a signature help + assert_eq!(test.signature_help(), None); + } + + #[test] + fn signature_help_after_closing_paren_in_expression() { + let test = cursor_test( + r#" + def test(a: int) -> int: + return 10 + + test("test") + 10 + "#, + ); + + // Should not return a signature help + assert_eq!(test.signature_help(), None); + } + + #[test] + fn signature_help_after_closing_paren_nested() { + let test = cursor_test( + r#" + def inner(a: int) -> int: + return 10 + + def outer(a: int) -> None: ... + + outer(inner("test") + 10) + "#, + ); + + // Should return the outer signature help + let help = test.signature_help().expect("Should have outer help"); + + assert_eq!(help.signatures.len(), 1); + + let signature = &help.signatures[0]; + assert_eq!(signature.label, "(a: int) -> None"); + } + #[test] fn signature_help_stub_to_implementation_mapping() { // Test that when a function is called from a stub file with no docstring, @@ -714,22 +786,22 @@ mod tests { .source( "main.py", r#" -from lib import func -result = func( + from lib import func + result = func( "#, ) .source( "lib.pyi", r#" -def func() -> str: ... + def func() -> str: ... "#, ) .source( "lib.py", r#" -def func() -> str: - """This function does something.""" - return "" + def func() -> str: + """This function does something.""" + return "" "#, ) .build();