[ty] Close signature help after ) (#20017)

This commit is contained in:
Micha Reiser 2025-08-22 16:09:22 +02:00 committed by GitHub
parent c5e05df966
commit 11f521c768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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;
}; };
@ -247,11 +268,11 @@ mod tests {
r#" r#"
def example_function(param1: str, param2: int) -> str: def example_function(param1: str, param2: int) -> str:
"""This is a docstring for the example function. """This is a docstring for the example function.
Args: Args:
param1: The first parameter as a string param1: The first parameter as a string
param2: The second parameter as an integer param2: The second parameter as an integer
Returns: Returns:
A formatted string combining both parameters A formatted string combining both parameters
""" """
@ -627,7 +648,7 @@ mod tests {
r#" r#"
def documented_function(param1: str, param2: int) -> str: def documented_function(param1: str, param2: int) -> str:
"""This is a function with parameter documentation. """This is a function with parameter documentation.
Args: Args:
param1: The first parameter description param1: The first parameter description
param2: The second parameter description param2: The second parameter description
@ -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();