mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-11-03 21:24:29 +00:00 
			
		
		
		
	[ty] Close signature help after ) (#20017)
				
					
				
			This commit is contained in:
		
							parent
							
								
									c5e05df966
								
							
						
					
					
						commit
						11f521c768
					
				
					 1 changed files with 95 additions and 23 deletions
				
			
		| 
						 | 
					@ -12,6 +12,7 @@ use crate::{Db, find_node::covering_node};
 | 
				
			||||||
use ruff_db::files::File;
 | 
					use ruff_db::files::File;
 | 
				
			||||||
use ruff_db::parsed::parsed_module;
 | 
					use ruff_db::parsed::parsed_module;
 | 
				
			||||||
use ruff_python_ast::{self as ast, AnyNodeRef};
 | 
					use ruff_python_ast::{self as ast, AnyNodeRef};
 | 
				
			||||||
 | 
					use ruff_python_parser::TokenKind;
 | 
				
			||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
 | 
					use ruff_text_size::{Ranged, TextRange, TextSize};
 | 
				
			||||||
use ty_python_semantic::ResolvedDefinition;
 | 
					use ty_python_semantic::ResolvedDefinition;
 | 
				
			||||||
use ty_python_semantic::SemanticModel;
 | 
					use ty_python_semantic::SemanticModel;
 | 
				
			||||||
| 
						 | 
					@ -25,7 +26,7 @@ use ty_python_semantic::types::{
 | 
				
			||||||
// associated with the __new__ or __init__ call.
 | 
					// associated with the __new__ or __init__ call.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Information about a function parameter
 | 
					/// Information about a function parameter
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
pub struct ParameterDetails {
 | 
					pub struct ParameterDetails {
 | 
				
			||||||
    /// The parameter name (e.g., "param1")
 | 
					    /// The parameter name (e.g., "param1")
 | 
				
			||||||
    pub name: String,
 | 
					    pub name: String,
 | 
				
			||||||
| 
						 | 
					@ -37,7 +38,7 @@ pub struct ParameterDetails {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Information about a function signature
 | 
					/// Information about a function signature
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
pub struct SignatureDetails {
 | 
					pub struct SignatureDetails {
 | 
				
			||||||
    /// Text representation of the full signature (including input parameters and return type).
 | 
					    /// Text representation of the full signature (including input parameters and return type).
 | 
				
			||||||
    pub label: String,
 | 
					    pub label: String,
 | 
				
			||||||
| 
						 | 
					@ -51,7 +52,7 @@ pub struct SignatureDetails {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Signature help information for function calls
 | 
					/// Signature help information for function calls
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
pub struct SignatureHelpInfo {
 | 
					pub struct SignatureHelpInfo {
 | 
				
			||||||
    /// Information about each of the signatures for the function call. We
 | 
					    /// Information about each of the signatures for the function call. We
 | 
				
			||||||
    /// need to handle multiple because of unions, overloads, and composite
 | 
					    /// need to handle multiple because of unions, overloads, and composite
 | 
				
			||||||
| 
						 | 
					@ -104,22 +105,42 @@ fn get_call_expr(
 | 
				
			||||||
) -> Option<(&ast::ExprCall, usize)> {
 | 
					) -> Option<(&ast::ExprCall, usize)> {
 | 
				
			||||||
    let root_node: AnyNodeRef = parsed.syntax().into();
 | 
					    let root_node: AnyNodeRef = parsed.syntax().into();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Create a range from the offset for the covering_node function.
 | 
					    // Find the token under the cursor and use its offset to find the node
 | 
				
			||||||
    // Use length 1 if it fits within the root node, otherwise use zero-length range.
 | 
					    let token = parsed
 | 
				
			||||||
    let one_char_range = TextRange::at(offset, TextSize::from(1));
 | 
					        .tokens()
 | 
				
			||||||
    let range = if root_node.range().contains_range(one_char_range) {
 | 
					        .at_offset(offset)
 | 
				
			||||||
        one_char_range
 | 
					        .max_by_key(|token| match token.kind() {
 | 
				
			||||||
    } else {
 | 
					            TokenKind::Name
 | 
				
			||||||
        TextRange::at(offset, TextSize::from(0))
 | 
					            | TokenKind::String
 | 
				
			||||||
    };
 | 
					            | TokenKind::Complex
 | 
				
			||||||
 | 
					            | TokenKind::Float
 | 
				
			||||||
 | 
					            | TokenKind::Int => 1,
 | 
				
			||||||
 | 
					            _ => 0,
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Find the covering node at the given position that is a function call.
 | 
					    // Find the covering node at the given position that is a function call.
 | 
				
			||||||
    let covering_node = covering_node(root_node, range)
 | 
					    let call = covering_node(root_node, token.range())
 | 
				
			||||||
        .find_first(|node| matches!(node, AnyNodeRef::ExprCall(_)))
 | 
					        .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()?;
 | 
					        .ok()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Get the function call expression.
 | 
					    // Get the function call expression.
 | 
				
			||||||
    let AnyNodeRef::ExprCall(call_expr) = covering_node.node() else {
 | 
					    let AnyNodeRef::ExprCall(call_expr) = call.node() else {
 | 
				
			||||||
        return None;
 | 
					        return None;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -706,6 +727,57 @@ mod tests {
 | 
				
			||||||
        assert_eq!(result.active_signature, Some(0));
 | 
					        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")<CURSOR>"#,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 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")<CURSOR> + 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")<CURSOR> + 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]
 | 
					    #[test]
 | 
				
			||||||
    fn signature_help_stub_to_implementation_mapping() {
 | 
					    fn signature_help_stub_to_implementation_mapping() {
 | 
				
			||||||
        // Test that when a function is called from a stub file with no docstring,
 | 
					        // Test that when a function is called from a stub file with no docstring,
 | 
				
			||||||
| 
						 | 
					@ -714,22 +786,22 @@ mod tests {
 | 
				
			||||||
            .source(
 | 
					            .source(
 | 
				
			||||||
                "main.py",
 | 
					                "main.py",
 | 
				
			||||||
                r#"
 | 
					                r#"
 | 
				
			||||||
from lib import func
 | 
					                from lib import func
 | 
				
			||||||
result = func(<CURSOR>
 | 
					                result = func(<CURSOR>
 | 
				
			||||||
"#,
 | 
					"#,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .source(
 | 
					            .source(
 | 
				
			||||||
                "lib.pyi",
 | 
					                "lib.pyi",
 | 
				
			||||||
                r#"
 | 
					                r#"
 | 
				
			||||||
def func() -> str: ...
 | 
					                def func() -> str: ...
 | 
				
			||||||
"#,
 | 
					"#,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .source(
 | 
					            .source(
 | 
				
			||||||
                "lib.py",
 | 
					                "lib.py",
 | 
				
			||||||
                r#"
 | 
					                r#"
 | 
				
			||||||
def func() -> str:
 | 
					                def func() -> str:
 | 
				
			||||||
    """This function does something."""
 | 
					                    """This function does something."""
 | 
				
			||||||
    return ""
 | 
					                    return ""
 | 
				
			||||||
"#,
 | 
					"#,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .build();
 | 
					            .build();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue