mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 20:08:19 +00:00 
			
		
		
		
	[ty] Function argument inlay hints (#19269)
	
		
			
	
		
	
	
		
	
		
			Some checks are pending
		
		
	
	
		
			
				
	
				CI / Determine changes (push) Waiting to run
				
			
		
			
				
	
				CI / cargo fmt (push) Waiting to run
				
			
		
			
				
	
				CI / cargo clippy (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo test (linux) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo test (linux, release) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo test (windows) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo test (wasm) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo build (release) (push) Waiting to run
				
			
		
			
				
	
				CI / mkdocs (push) Waiting to run
				
			
		
			
				
	
				CI / cargo build (msrv) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo fuzz build (push) Blocked by required conditions
				
			
		
			
				
	
				CI / fuzz parser (push) Blocked by required conditions
				
			
		
			
				
	
				CI / test scripts (push) Blocked by required conditions
				
			
		
			
				
	
				CI / ecosystem (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Fuzz for new ty panics (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo shear (push) Blocked by required conditions
				
			
		
			
				
	
				CI / python package (push) Waiting to run
				
			
		
			
				
	
				CI / pre-commit (push) Waiting to run
				
			
		
			
				
	
				CI / formatter instabilities and black similarity (push) Blocked by required conditions
				
			
		
			
				
	
				CI / test ruff-lsp (push) Blocked by required conditions
				
			
		
			
				
	
				CI / check playground (push) Blocked by required conditions
				
			
		
			
				
	
				CI / benchmarks-instrumented (push) Blocked by required conditions
				
			
		
			
				
	
				CI / benchmarks-walltime (push) Blocked by required conditions
				
			
		
			
				
	
				[ty Playground] Release / publish (push) Waiting to run
				
			
		
		
	
	
				
					
				
			
		
			Some checks are pending
		
		
	
	CI / Determine changes (push) Waiting to run
				
			CI / cargo fmt (push) Waiting to run
				
			CI / cargo clippy (push) Blocked by required conditions
				
			CI / cargo test (linux) (push) Blocked by required conditions
				
			CI / cargo test (linux, release) (push) Blocked by required conditions
				
			CI / cargo test (windows) (push) Blocked by required conditions
				
			CI / cargo test (wasm) (push) Blocked by required conditions
				
			CI / cargo build (release) (push) Waiting to run
				
			CI / mkdocs (push) Waiting to run
				
			CI / cargo build (msrv) (push) Blocked by required conditions
				
			CI / cargo fuzz build (push) Blocked by required conditions
				
			CI / fuzz parser (push) Blocked by required conditions
				
			CI / test scripts (push) Blocked by required conditions
				
			CI / ecosystem (push) Blocked by required conditions
				
			CI / Fuzz for new ty panics (push) Blocked by required conditions
				
			CI / cargo shear (push) Blocked by required conditions
				
			CI / python package (push) Waiting to run
				
			CI / pre-commit (push) Waiting to run
				
			CI / formatter instabilities and black similarity (push) Blocked by required conditions
				
			CI / test ruff-lsp (push) Blocked by required conditions
				
			CI / check playground (push) Blocked by required conditions
				
			CI / benchmarks-instrumented (push) Blocked by required conditions
				
			CI / benchmarks-walltime (push) Blocked by required conditions
				
			[ty Playground] Release / publish (push) Waiting to run
				
			This commit is contained in:
		
							parent
							
								
									3458f365da
								
							
						
					
					
						commit
						ad28b80f96
					
				
					 6 changed files with 692 additions and 67 deletions
				
			
		|  | @ -6,7 +6,7 @@ use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; | ||||||
| use ruff_text_size::{Ranged, TextRange, TextSize}; | use ruff_text_size::{Ranged, TextRange, TextSize}; | ||||||
| use std::fmt; | use std::fmt; | ||||||
| use std::fmt::Formatter; | use std::fmt::Formatter; | ||||||
| use ty_python_semantic::types::Type; | use ty_python_semantic::types::{Type, inlay_hint_function_argument_details}; | ||||||
| use ty_python_semantic::{HasType, SemanticModel}; | use ty_python_semantic::{HasType, SemanticModel}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone, Eq, PartialEq)] | #[derive(Debug, Clone, Eq, PartialEq)] | ||||||
|  | @ -24,7 +24,7 @@ impl<'db> InlayHint<'db> { | ||||||
| #[derive(Debug, Clone, Eq, PartialEq)] | #[derive(Debug, Clone, Eq, PartialEq)] | ||||||
| pub enum InlayHintContent<'db> { | pub enum InlayHintContent<'db> { | ||||||
|     Type(Type<'db>), |     Type(Type<'db>), | ||||||
|     ReturnType(Type<'db>), |     FunctionArgumentName(String), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<'db> InlayHintContent<'db> { | impl<'db> InlayHintContent<'db> { | ||||||
|  | @ -44,8 +44,8 @@ impl fmt::Display for DisplayInlayHint<'_, '_> { | ||||||
|             InlayHintContent::Type(ty) => { |             InlayHintContent::Type(ty) => { | ||||||
|                 write!(f, ": {}", ty.display(self.db)) |                 write!(f, ": {}", ty.display(self.db)) | ||||||
|             } |             } | ||||||
|             InlayHintContent::ReturnType(ty) => { |             InlayHintContent::FunctionArgumentName(name) => { | ||||||
|                 write!(f, " -> {}", ty.display(self.db)) |                 write!(f, "{name}=") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -76,9 +76,18 @@ pub struct InlayHintSettings { | ||||||
|     /// x": Literal[1]" = 1
 |     /// x": Literal[1]" = 1
 | ||||||
|     /// ```
 |     /// ```
 | ||||||
|     pub variable_types: bool, |     pub variable_types: bool, | ||||||
|  |     /// Whether to show function argument names.
 | ||||||
|  |     ///
 | ||||||
|  |     /// For example, this would enable / disable hints like the ones quoted below:
 | ||||||
|  |     /// ```python
 | ||||||
|  |     /// def foo(x: int): pass
 | ||||||
|  |     /// foo("x="1)
 | ||||||
|  |     /// ```
 | ||||||
|  |     pub function_argument_names: bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| struct InlayHintVisitor<'a, 'db> { | struct InlayHintVisitor<'a, 'db> { | ||||||
|  |     db: &'db dyn Db, | ||||||
|     model: SemanticModel<'db>, |     model: SemanticModel<'db>, | ||||||
|     hints: Vec<InlayHint<'db>>, |     hints: Vec<InlayHint<'db>>, | ||||||
|     in_assignment: bool, |     in_assignment: bool, | ||||||
|  | @ -89,6 +98,7 @@ struct InlayHintVisitor<'a, 'db> { | ||||||
| impl<'a, 'db> InlayHintVisitor<'a, 'db> { | impl<'a, 'db> InlayHintVisitor<'a, 'db> { | ||||||
|     fn new(db: &'db dyn Db, file: File, range: TextRange, settings: &'a InlayHintSettings) -> Self { |     fn new(db: &'db dyn Db, file: File, range: TextRange, settings: &'a InlayHintSettings) -> Self { | ||||||
|         Self { |         Self { | ||||||
|  |             db, | ||||||
|             model: SemanticModel::new(db, file), |             model: SemanticModel::new(db, file), | ||||||
|             hints: Vec::new(), |             hints: Vec::new(), | ||||||
|             in_assignment: false, |             in_assignment: false, | ||||||
|  | @ -98,11 +108,29 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) { |     fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) { | ||||||
|  |         if !self.settings.variable_types { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         self.hints.push(InlayHint { |         self.hints.push(InlayHint { | ||||||
|             position, |             position, | ||||||
|             content: InlayHintContent::Type(ty), |             content: InlayHintContent::Type(ty), | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     fn add_function_argument_name(&mut self, position: TextSize, name: String) { | ||||||
|  |         if !self.settings.function_argument_names { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if name.starts_with('_') { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.hints.push(InlayHint { | ||||||
|  |             position, | ||||||
|  |             content: InlayHintContent::FunctionArgumentName(name), | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { | impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { | ||||||
|  | @ -123,25 +151,23 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { | ||||||
| 
 | 
 | ||||||
|         match stmt { |         match stmt { | ||||||
|             Stmt::Assign(assign) => { |             Stmt::Assign(assign) => { | ||||||
|                 if !self.settings.variable_types { |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 self.in_assignment = true; |                 self.in_assignment = true; | ||||||
|                 for target in &assign.targets { |                 for target in &assign.targets { | ||||||
|                     self.visit_expr(target); |                     self.visit_expr(target); | ||||||
|                 } |                 } | ||||||
|                 self.in_assignment = false; |                 self.in_assignment = false; | ||||||
| 
 | 
 | ||||||
|  |                 self.visit_expr(&assign.value); | ||||||
|  | 
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             Stmt::Expr(expr) => { | ||||||
|  |                 self.visit_expr(&expr.value); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             // TODO
 |             // TODO
 | ||||||
|             Stmt::FunctionDef(_) => {} |             Stmt::FunctionDef(_) => {} | ||||||
|             Stmt::For(_) => {} |             Stmt::For(_) => {} | ||||||
|             Stmt::Expr(_) => { |  | ||||||
|                 // Don't traverse into expression statements because we don't show any hints.
 |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             _ => {} |             _ => {} | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -149,15 +175,32 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn visit_expr(&mut self, expr: &'_ Expr) { |     fn visit_expr(&mut self, expr: &'_ Expr) { | ||||||
|         if !self.in_assignment { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         match expr { |         match expr { | ||||||
|             Expr::Name(name) => { |             Expr::Name(name) => { | ||||||
|                 if name.ctx.is_store() { |                 if self.in_assignment { | ||||||
|                     let ty = expr.inferred_type(&self.model); |                     if name.ctx.is_store() { | ||||||
|                     self.add_type_hint(expr.range().end(), ty); |                         let ty = expr.inferred_type(&self.model); | ||||||
|  |                         self.add_type_hint(expr.range().end(), ty); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 source_order::walk_expr(self, expr); | ||||||
|  |             } | ||||||
|  |             Expr::Call(call) => { | ||||||
|  |                 let argument_names = | ||||||
|  |                     inlay_hint_function_argument_details(self.db, &self.model, call) | ||||||
|  |                         .map(|details| details.argument_names) | ||||||
|  |                         .unwrap_or_default(); | ||||||
|  | 
 | ||||||
|  |                 self.visit_expr(&call.func); | ||||||
|  | 
 | ||||||
|  |                 for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() { | ||||||
|  |                     if let Some(name) = argument_names.get(&index) { | ||||||
|  |                         self.add_function_argument_name( | ||||||
|  |                             arg_or_keyword.range().start(), | ||||||
|  |                             name.to_string(), | ||||||
|  |                         ); | ||||||
|  |                     } | ||||||
|  |                     self.visit_expr(arg_or_keyword.value()); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             _ => { |             _ => { | ||||||
|  | @ -177,6 +220,7 @@ mod tests { | ||||||
|         files::{File, system_path_to_file}, |         files::{File, system_path_to_file}, | ||||||
|         source::source_text, |         source::source_text, | ||||||
|     }; |     }; | ||||||
|  |     use ruff_python_trivia::textwrap::dedent; | ||||||
|     use ruff_text_size::TextSize; |     use ruff_text_size::TextSize; | ||||||
| 
 | 
 | ||||||
|     use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; |     use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; | ||||||
|  | @ -194,6 +238,8 @@ mod tests { | ||||||
|             SystemPathBuf::from("/"), |             SystemPathBuf::from("/"), | ||||||
|         )); |         )); | ||||||
| 
 | 
 | ||||||
|  |         let source = dedent(source); | ||||||
|  | 
 | ||||||
|         let start = source.find(START); |         let start = source.find(START); | ||||||
|         let end = source |         let end = source | ||||||
|             .find(END) |             .find(END) | ||||||
|  | @ -245,6 +291,7 @@ mod tests { | ||||||
|         fn inlay_hints(&self) -> String { |         fn inlay_hints(&self) -> String { | ||||||
|             self.inlay_hints_with_settings(&InlayHintSettings { |             self.inlay_hints_with_settings(&InlayHintSettings { | ||||||
|                 variable_types: true, |                 variable_types: true, | ||||||
|  |                 function_argument_names: true, | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -314,16 +361,529 @@ mod tests { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #[test] |     #[test] | ||||||
|     fn disabled_variable_types() { |     fn test_disabled_variable_types() { | ||||||
|         let test = inlay_hint_test("x = 1"); |         let test = inlay_hint_test("x = 1"); | ||||||
| 
 | 
 | ||||||
|         assert_snapshot!( |         assert_snapshot!( | ||||||
|             test.inlay_hints_with_settings(&InlayHintSettings { |             test.inlay_hints_with_settings(&InlayHintSettings { | ||||||
|                 variable_types: false, |                 variable_types: false, | ||||||
|  |                 ..Default::default() | ||||||
|             }), |             }), | ||||||
|             @r" |             @r" | ||||||
|         x = 1 |         x = 1 | ||||||
|         " |         " | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_positional_or_keyword_parameter() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int): pass | ||||||
|  |             foo(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int): pass | ||||||
|  |         foo([x=]1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_positional_only_parameter() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int, /): pass | ||||||
|  |             foo(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int, /): pass | ||||||
|  |         foo(1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_variadic_parameter() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(*args: int): pass | ||||||
|  |             foo(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(*args: int): pass | ||||||
|  |         foo(1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_keyword_variadic_parameter() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(**kwargs: int): pass | ||||||
|  |             foo(x=1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(**kwargs: int): pass | ||||||
|  |         foo(x=1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_keyword_only_parameter() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(*, x: int): pass | ||||||
|  |             foo(x=1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(*, x: int): pass | ||||||
|  |         foo(x=1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_positional_only_and_positional_or_keyword_parameters() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int, /, y: int): pass | ||||||
|  |             foo(1, 2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int, /, y: int): pass | ||||||
|  |         foo(1, [y=]2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_positional_only_and_variadic_parameters() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int, /, *args: int): pass | ||||||
|  |             foo(1, 2, 3)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int, /, *args: int): pass | ||||||
|  |         foo(1, 2, 3) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_positional_only_and_keyword_variadic_parameters() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int, /, **kwargs: int): pass | ||||||
|  |             foo(1, x=2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int, /, **kwargs: int): pass | ||||||
|  |         foo(1, x=2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_class_constructor_call_init() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             class Foo: | ||||||
|  |                 def __init__(self, x: int): pass | ||||||
|  |             Foo(1) | ||||||
|  |             f = Foo(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         class Foo: | ||||||
|  |             def __init__(self, x: int): pass | ||||||
|  |         Foo([x=]1) | ||||||
|  |         f[: Foo] = Foo([x=]1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_class_constructor_call_new() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             class Foo: | ||||||
|  |                 def __new__(cls, x: int): pass | ||||||
|  |             Foo(1) | ||||||
|  |             f = Foo(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         class Foo: | ||||||
|  |             def __new__(cls, x: int): pass | ||||||
|  |         Foo([x=]1) | ||||||
|  |         f[: Foo] = Foo([x=]1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_class_constructor_call_meta_class_call() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             class MetaFoo: | ||||||
|  |                 def __call__(self, x: int): pass | ||||||
|  |             class Foo(metaclass=MetaFoo): | ||||||
|  |                 pass | ||||||
|  |             Foo(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         class MetaFoo: | ||||||
|  |             def __call__(self, x: int): pass | ||||||
|  |         class Foo(metaclass=MetaFoo): | ||||||
|  |             pass | ||||||
|  |         Foo([x=]1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_callable_call() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             from typing import Callable | ||||||
|  |             def foo(x: Callable[[int], int]): | ||||||
|  |                 x(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         from typing import Callable | ||||||
|  |         def foo(x: Callable[[int], int]): | ||||||
|  |             x(1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_instance_method_call() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             class Foo: | ||||||
|  |                 def bar(self, y: int): pass | ||||||
|  |             Foo().bar(2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         class Foo: | ||||||
|  |             def bar(self, y: int): pass | ||||||
|  |         Foo().bar([y=]2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_class_method_call() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             class Foo: | ||||||
|  |                 @classmethod | ||||||
|  |                 def bar(cls, y: int): pass | ||||||
|  |             Foo.bar(2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         class Foo: | ||||||
|  |             @classmethod | ||||||
|  |             def bar(cls, y: int): pass | ||||||
|  |         Foo.bar([y=]2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_static_method_call() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             class Foo: | ||||||
|  |                 @staticmethod | ||||||
|  |                 def bar(y: int): pass | ||||||
|  |             Foo.bar(2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         class Foo: | ||||||
|  |             @staticmethod | ||||||
|  |             def bar(y: int): pass | ||||||
|  |         Foo.bar([y=]2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_union_type() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int | str): pass | ||||||
|  |             foo(1) | ||||||
|  |             foo('abc')",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int | str): pass | ||||||
|  |         foo([x=]1) | ||||||
|  |         foo([x=]'abc') | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_multiple_positional_arguments() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int, y: str, z: bool): pass | ||||||
|  |             foo(1, 'hello', True)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int, y: str, z: bool): pass | ||||||
|  |         foo([x=]1, [y=]'hello', [z=]True) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_mixed_positional_and_keyword() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int, y: str, z: bool): pass | ||||||
|  |             foo(1, z=True, y='hello')",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int, y: str, z: bool): pass | ||||||
|  |         foo([x=]1, z=True, y='hello') | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_default_parameters() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int, y: str = 'default', z: bool = False): pass | ||||||
|  |             foo(1) | ||||||
|  |             foo(1, 'custom') | ||||||
|  |             foo(1, 'custom', True)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int, y: str = 'default', z: bool = False): pass | ||||||
|  |         foo([x=]1) | ||||||
|  |         foo([x=]1, [y=]'custom') | ||||||
|  |         foo([x=]1, [y=]'custom', [z=]True) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_nested_function_calls() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: int) -> int: | ||||||
|  |                 return x * 2 | ||||||
|  | 
 | ||||||
|  |             def bar(y: str) -> str: | ||||||
|  |                 return y | ||||||
|  | 
 | ||||||
|  |             def baz(a: int, b: str, c: bool): pass | ||||||
|  | 
 | ||||||
|  |             baz(foo(5), bar(bar('test')), True)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int) -> int: | ||||||
|  |             return x * 2 | ||||||
|  | 
 | ||||||
|  |         def bar(y: str) -> str: | ||||||
|  |             return y | ||||||
|  | 
 | ||||||
|  |         def baz(a: int, b: str, c: bool): pass | ||||||
|  | 
 | ||||||
|  |         baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_method_chaining() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             class A: | ||||||
|  |                 def foo(self, value: int) -> 'A': | ||||||
|  |                     return self | ||||||
|  |                 def bar(self, name: str) -> 'A': | ||||||
|  |                     return self | ||||||
|  |                 def baz(self): pass | ||||||
|  |             A().foo(42).bar('test').baz()",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         class A: | ||||||
|  |             def foo(self, value: int) -> 'A': | ||||||
|  |                 return self | ||||||
|  |             def bar(self, name: str) -> 'A': | ||||||
|  |                 return self | ||||||
|  |             def baz(self): pass | ||||||
|  |         A().foo([value=]42).bar([name=]'test').baz() | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_nexted_keyword_function_calls() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(x: str) -> str: | ||||||
|  |                 return x | ||||||
|  |             def bar(y: int): pass | ||||||
|  |             bar(y=foo('test')) | ||||||
|  |             ",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: str) -> str: | ||||||
|  |             return x | ||||||
|  |         def bar(y: int): pass | ||||||
|  |         bar(y=foo([x=]'test')) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_lambda_function_calls() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             foo = lambda x: x * 2 | ||||||
|  |             bar = lambda a, b: a + b | ||||||
|  |             foo(5) | ||||||
|  |             bar(1, 2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         foo[: (x) -> Unknown] = lambda x: x * 2 | ||||||
|  |         bar[: (a, b) -> Unknown] = lambda a, b: a + b | ||||||
|  |         foo([x=]5) | ||||||
|  |         bar([a=]1, [b=]2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_complex_parameter_combinations() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ||||||
|  |             foo(1, 'pos', 3.14, False, e=42) | ||||||
|  |             foo(1, 'pos', 3.14, e=42, f='custom')",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ||||||
|  |         foo(1, 'pos', [c=]3.14, [d=]False, e=42) | ||||||
|  |         foo(1, 'pos', [c=]3.14, e=42, f='custom') | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_generic_function_calls() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             from typing import TypeVar, Generic | ||||||
|  | 
 | ||||||
|  |             T = TypeVar('T') | ||||||
|  | 
 | ||||||
|  |             def identity(x: T) -> T: | ||||||
|  |                 return x | ||||||
|  | 
 | ||||||
|  |             identity(42) | ||||||
|  |             identity('hello')",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r#" | ||||||
|  |         from typing import TypeVar, Generic | ||||||
|  | 
 | ||||||
|  |         T[: typing.TypeVar("T")] = TypeVar([name=]'T') | ||||||
|  | 
 | ||||||
|  |         def identity(x: T) -> T: | ||||||
|  |             return x | ||||||
|  | 
 | ||||||
|  |         identity([x=]42) | ||||||
|  |         identity([x=]'hello') | ||||||
|  |         "#);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_overloaded_function_calls() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             from typing import overload | ||||||
|  | 
 | ||||||
|  |             @overload | ||||||
|  |             def foo(x: int) -> str: ... | ||||||
|  |             @overload | ||||||
|  |             def foo(x: str) -> int: ... | ||||||
|  |             def foo(x): | ||||||
|  |                 return x | ||||||
|  |             
 | ||||||
|  |             foo(42) | ||||||
|  |             foo('hello')",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         from typing import overload | ||||||
|  | 
 | ||||||
|  |         @overload | ||||||
|  |         def foo(x: int) -> str: ... | ||||||
|  |         @overload | ||||||
|  |         def foo(x: str) -> int: ... | ||||||
|  |         def foo(x): | ||||||
|  |             return x | ||||||
|  | 
 | ||||||
|  |         foo([x=]42) | ||||||
|  |         foo([x=]'hello') | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_disabled_function_argument_names() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |         def foo(x: int): pass | ||||||
|  |         foo(1)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints_with_settings(&InlayHintSettings { | ||||||
|  |             function_argument_names: false, | ||||||
|  |             ..Default::default() | ||||||
|  |         }), @r" | ||||||
|  |         def foo(x: int): pass | ||||||
|  |         foo(1) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_out_of_range() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             <START>def foo(x: int): pass | ||||||
|  |             def bar(y: int): pass | ||||||
|  |             foo(1)<END> | ||||||
|  |             bar(2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(x: int): pass | ||||||
|  |         def bar(y: int): pass | ||||||
|  |         foo([x=]1) | ||||||
|  |         bar(2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_function_call_with_argument_name_starting_with_underscore() { | ||||||
|  |         let test = inlay_hint_test( | ||||||
|  |             " | ||||||
|  |             def foo(_x: int, y: int): pass | ||||||
|  |             foo(1, 2)",
 | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_snapshot!(test.inlay_hints(), @r" | ||||||
|  |         def foo(_x: int, y: int): pass | ||||||
|  |         foo(1, [y=]2) | ||||||
|  |         ");
 | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,8 +14,11 @@ use ruff_db::parsed::parsed_module; | ||||||
| use ruff_python_ast::{self as ast, AnyNodeRef}; | use ruff_python_ast::{self as ast, AnyNodeRef}; | ||||||
| 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::semantic_index::definition::Definition; | use ty_python_semantic::semantic_index::definition::Definition; | ||||||
| use ty_python_semantic::types::{CallSignatureDetails, call_signature_details}; | use ty_python_semantic::types::{ | ||||||
|  |     CallSignatureDetails, call_signature_details, find_active_signature_from_details, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| // TODO: We may want to add special-case handling for calls to constructors
 | // TODO: We may want to add special-case handling for calls to constructors
 | ||||||
| // so the class docstring is used in place of (or inaddition to) any docstring
 | // so the class docstring is used in place of (or inaddition to) any docstring
 | ||||||
|  | @ -66,9 +69,11 @@ pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<Signa | ||||||
|     // Get the call expression at the given position.
 |     // Get the call expression at the given position.
 | ||||||
|     let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?; |     let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?; | ||||||
| 
 | 
 | ||||||
|  |     let model = SemanticModel::new(db, file); | ||||||
|  | 
 | ||||||
|     // Get signature details from the semantic analyzer.
 |     // Get signature details from the semantic analyzer.
 | ||||||
|     let signature_details: Vec<CallSignatureDetails<'_>> = |     let signature_details: Vec<CallSignatureDetails<'_>> = | ||||||
|         call_signature_details(db, file, call_expr); |         call_signature_details(db, &model, call_expr); | ||||||
| 
 | 
 | ||||||
|     if signature_details.is_empty() { |     if signature_details.is_empty() { | ||||||
|         return None; |         return None; | ||||||
|  | @ -258,45 +263,6 @@ fn create_parameters_from_offsets( | ||||||
|         .collect() |         .collect() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Find the active signature index from `CallSignatureDetails`.
 |  | ||||||
| /// The active signature is the first signature where all arguments present in the call
 |  | ||||||
| /// have valid mappings to parameters (i.e., none of the mappings are None).
 |  | ||||||
| fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]) -> Option<usize> { |  | ||||||
|     let first = signature_details.first()?; |  | ||||||
| 
 |  | ||||||
|     // If there are no arguments in the mapping, just return the first signature.
 |  | ||||||
|     if first.argument_to_parameter_mapping.is_empty() { |  | ||||||
|         return Some(0); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // First, try to find a signature where all arguments have valid parameter mappings.
 |  | ||||||
|     let perfect_match = signature_details.iter().position(|details| { |  | ||||||
|         // Check if all arguments have valid parameter mappings.
 |  | ||||||
|         details |  | ||||||
|             .argument_to_parameter_mapping |  | ||||||
|             .iter() |  | ||||||
|             .all(|mapping| mapping.matched) |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     if let Some(index) = perfect_match { |  | ||||||
|         return Some(index); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // If no perfect match, find the signature with the most valid argument mappings.
 |  | ||||||
|     let (best_index, _) = signature_details |  | ||||||
|         .iter() |  | ||||||
|         .enumerate() |  | ||||||
|         .max_by_key(|(_, details)| { |  | ||||||
|             details |  | ||||||
|                 .argument_to_parameter_mapping |  | ||||||
|                 .iter() |  | ||||||
|                 .filter(|mapping| mapping.matched) |  | ||||||
|                 .count() |  | ||||||
|         })?; |  | ||||||
| 
 |  | ||||||
|     Some(best_index) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use crate::signature_help::SignatureHelpInfo; |     use crate::signature_help::SignatureHelpInfo; | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ use crate::types::generics::{ | ||||||
| pub use crate::types::ide_support::{ | pub use crate::types::ide_support::{ | ||||||
|     CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name, |     CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name, | ||||||
|     definitions_for_attribute, definitions_for_imported_symbol, definitions_for_keyword_argument, |     definitions_for_attribute, definitions_for_imported_symbol, definitions_for_keyword_argument, | ||||||
|     definitions_for_name, |     definitions_for_name, find_active_signature_from_details, inlay_hint_function_argument_details, | ||||||
| }; | }; | ||||||
| use crate::types::infer::infer_unpack_types; | use crate::types::infer::infer_unpack_types; | ||||||
| use crate::types::mro::{Mro, MroError, MroIterator}; | use crate::types::mro::{Mro, MroError, MroIterator}; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| use std::cmp::Ordering; | use std::cmp::Ordering; | ||||||
|  | use std::collections::HashMap; | ||||||
| 
 | 
 | ||||||
| use crate::place::{ | use crate::place::{ | ||||||
|     Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations, |     Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations, | ||||||
|  | @ -15,8 +16,8 @@ use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstan | ||||||
| use crate::{Db, HasType, NameKind, SemanticModel}; | use crate::{Db, HasType, NameKind, SemanticModel}; | ||||||
| use ruff_db::files::{File, FileRange}; | use ruff_db::files::{File, FileRange}; | ||||||
| use ruff_db::parsed::parsed_module; | use ruff_db::parsed::parsed_module; | ||||||
| use ruff_python_ast as ast; |  | ||||||
| use ruff_python_ast::name::Name; | use ruff_python_ast::name::Name; | ||||||
|  | use ruff_python_ast::{self as ast}; | ||||||
| use ruff_text_size::{Ranged, TextRange}; | use ruff_text_size::{Ranged, TextRange}; | ||||||
| use rustc_hash::FxHashSet; | use rustc_hash::FxHashSet; | ||||||
| 
 | 
 | ||||||
|  | @ -805,17 +806,16 @@ pub struct CallSignatureDetails<'db> { | ||||||
| /// (in case of overloads or union types).
 | /// (in case of overloads or union types).
 | ||||||
| pub fn call_signature_details<'db>( | pub fn call_signature_details<'db>( | ||||||
|     db: &'db dyn Db, |     db: &'db dyn Db, | ||||||
|     file: File, |     model: &SemanticModel<'db>, | ||||||
|     call_expr: &ast::ExprCall, |     call_expr: &ast::ExprCall, | ||||||
| ) -> Vec<CallSignatureDetails<'db>> { | ) -> Vec<CallSignatureDetails<'db>> { | ||||||
|     let model = SemanticModel::new(db, file); |     let func_type = call_expr.func.inferred_type(model); | ||||||
|     let func_type = call_expr.func.inferred_type(&model); |  | ||||||
| 
 | 
 | ||||||
|     // Use into_callable to handle all the complex type conversions
 |     // Use into_callable to handle all the complex type conversions
 | ||||||
|     if let Some(callable_type) = func_type.into_callable(db) { |     if let Some(callable_type) = func_type.into_callable(db) { | ||||||
|         let call_arguments = |         let call_arguments = | ||||||
|             CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| { |             CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| { | ||||||
|                 splatted_value.inferred_type(&model) |                 splatted_value.inferred_type(model) | ||||||
|             }); |             }); | ||||||
|         let bindings = callable_type.bindings(db).match_parameters(&call_arguments); |         let bindings = callable_type.bindings(db).match_parameters(&call_arguments); | ||||||
| 
 | 
 | ||||||
|  | @ -845,6 +845,102 @@ pub fn call_signature_details<'db>( | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Find the active signature index from `CallSignatureDetails`.
 | ||||||
|  | /// The active signature is the first signature where all arguments present in the call
 | ||||||
|  | /// have valid mappings to parameters (i.e., none of the mappings are None).
 | ||||||
|  | pub fn find_active_signature_from_details( | ||||||
|  |     signature_details: &[CallSignatureDetails], | ||||||
|  | ) -> Option<usize> { | ||||||
|  |     let first = signature_details.first()?; | ||||||
|  | 
 | ||||||
|  |     // If there are no arguments in the mapping, just return the first signature.
 | ||||||
|  |     if first.argument_to_parameter_mapping.is_empty() { | ||||||
|  |         return Some(0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // First, try to find a signature where all arguments have valid parameter mappings.
 | ||||||
|  |     let perfect_match = signature_details.iter().position(|details| { | ||||||
|  |         // Check if all arguments have valid parameter mappings.
 | ||||||
|  |         details | ||||||
|  |             .argument_to_parameter_mapping | ||||||
|  |             .iter() | ||||||
|  |             .all(|mapping| mapping.matched) | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if let Some(index) = perfect_match { | ||||||
|  |         return Some(index); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // If no perfect match, find the signature with the most valid argument mappings.
 | ||||||
|  |     let (best_index, _) = signature_details | ||||||
|  |         .iter() | ||||||
|  |         .enumerate() | ||||||
|  |         .max_by_key(|(_, details)| { | ||||||
|  |             details | ||||||
|  |                 .argument_to_parameter_mapping | ||||||
|  |                 .iter() | ||||||
|  |                 .filter(|mapping| mapping.matched) | ||||||
|  |                 .count() | ||||||
|  |         })?; | ||||||
|  | 
 | ||||||
|  |     Some(best_index) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Default)] | ||||||
|  | pub struct InlayHintFunctionArgumentDetails { | ||||||
|  |     pub argument_names: HashMap<usize, String>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn inlay_hint_function_argument_details<'db>( | ||||||
|  |     db: &'db dyn Db, | ||||||
|  |     model: &SemanticModel<'db>, | ||||||
|  |     call_expr: &ast::ExprCall, | ||||||
|  | ) -> Option<InlayHintFunctionArgumentDetails> { | ||||||
|  |     let signature_details = call_signature_details(db, model, call_expr); | ||||||
|  | 
 | ||||||
|  |     if signature_details.is_empty() { | ||||||
|  |         return None; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let active_signature_index = find_active_signature_from_details(&signature_details)?; | ||||||
|  | 
 | ||||||
|  |     let call_signature_details = signature_details.get(active_signature_index)?; | ||||||
|  | 
 | ||||||
|  |     let parameters = call_signature_details.signature.parameters(); | ||||||
|  |     let mut argument_names = HashMap::new(); | ||||||
|  | 
 | ||||||
|  |     for arg_index in 0..call_expr.arguments.args.len() { | ||||||
|  |         let Some(arg_mapping) = call_signature_details | ||||||
|  |             .argument_to_parameter_mapping | ||||||
|  |             .get(arg_index) | ||||||
|  |         else { | ||||||
|  |             continue; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if !arg_mapping.matched { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let Some(param_index) = arg_mapping.parameters.first() else { | ||||||
|  |             continue; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let Some(param) = parameters.get(*param_index) else { | ||||||
|  |             continue; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Only add hints for parameters that can be specified by name
 | ||||||
|  |         if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() { | ||||||
|  |             let Some(name) = param.name() else { | ||||||
|  |                 continue; | ||||||
|  |             }; | ||||||
|  |             argument_names.insert(arg_index, name.to_string()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Some(InlayHintFunctionArgumentDetails { argument_names }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /// Find the text range of a specific parameter in function parameters by name.
 | /// Find the text range of a specific parameter in function parameters by name.
 | ||||||
| /// Only searches for parameters that can be addressed by name in keyword arguments.
 | /// Only searches for parameters that can be addressed by name in keyword arguments.
 | ||||||
| fn find_parameter_range(parameters: &ast::Parameters, parameter_name: &str) -> Option<TextRange> { | fn find_parameter_range(parameters: &ast::Parameters, parameter_name: &str) -> Option<TextRange> { | ||||||
|  |  | ||||||
|  | @ -237,12 +237,14 @@ impl WorkspaceOptions { | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| struct InlayHintOptions { | struct InlayHintOptions { | ||||||
|     variable_types: Option<bool>, |     variable_types: Option<bool>, | ||||||
|  |     function_argument_names: Option<bool>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl InlayHintOptions { | impl InlayHintOptions { | ||||||
|     fn into_settings(self) -> InlayHintSettings { |     fn into_settings(self) -> InlayHintSettings { | ||||||
|         InlayHintSettings { |         InlayHintSettings { | ||||||
|             variable_types: self.variable_types.unwrap_or_default(), |             variable_types: self.variable_types.unwrap_or_default(), | ||||||
|  |             function_argument_names: self.function_argument_names.unwrap_or_default(), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -438,6 +438,7 @@ impl Workspace { | ||||||
|             // TODO: Provide a way to configure this
 |             // TODO: Provide a way to configure this
 | ||||||
|             &InlayHintSettings { |             &InlayHintSettings { | ||||||
|                 variable_types: true, |                 variable_types: true, | ||||||
|  |                 function_argument_names: true, | ||||||
|             }, |             }, | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Matthew Mckee
						Matthew Mckee