From 901e9cdf49489c024f41c2576a819e218cdf0e0d Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 17 Nov 2025 10:33:09 +0000 Subject: [PATCH] [ty] Inlay hint call argument location (#20349) Co-authored-by: Micha Reiser --- crates/ty_ide/src/goto.rs | 2 +- crates/ty_ide/src/inlay_hints.rs | 1325 ++++++++++++++++- crates/ty_ide/src/lib.rs | 14 + .../src/types/ide_support.rs | 33 +- .../src/server/api/requests/inlay_hints.rs | 15 +- crates/ty_server/tests/e2e/inlay_hints.rs | 15 +- crates/ty_wasm/src/lib.rs | 66 +- playground/ty/src/Editor/Editor.tsx | 92 +- 8 files changed, 1447 insertions(+), 115 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 094d2008d2..2faacc4a86 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -855,7 +855,7 @@ fn convert_resolved_definitions_to_targets( } ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => { // For file ranges, navigate to the specific range within the file - crate::NavigationTarget::new(file_range.file(), file_range.range()) + crate::NavigationTarget::from(file_range) } }) .collect() diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 353f761e90..7e1e123228 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1,13 +1,13 @@ use std::{fmt, vec}; -use crate::Db; +use crate::{Db, NavigationTarget}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp}; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::types::Type; -use ty_python_semantic::types::ide_support::inlay_hint_function_argument_details; +use ty_python_semantic::types::ide_support::inlay_hint_call_argument_details; use ty_python_semantic::{HasType, SemanticModel}; #[derive(Debug, Clone)] @@ -31,8 +31,15 @@ impl InlayHint { } } - fn call_argument_name(position: TextSize, name: &str) -> Self { - let label_parts = vec![InlayHintLabelPart::new(name), "=".into()]; + fn call_argument_name( + position: TextSize, + name: &str, + navigation_target: Option, + ) -> Self { + let label_parts = vec![ + InlayHintLabelPart::new(name).with_target(navigation_target), + "=".into(), + ]; Self { position, @@ -61,6 +68,10 @@ impl InlayHintLabel { pub fn parts(&self) -> &[InlayHintLabelPart] { &self.parts } + + pub fn into_parts(self) -> Vec { + self.parts + } } pub struct InlayHintDisplay<'a> { @@ -80,7 +91,7 @@ impl fmt::Display for InlayHintDisplay<'_> { pub struct InlayHintLabelPart { text: String, - target: Option, + target: Option, } impl InlayHintLabelPart { @@ -95,9 +106,17 @@ impl InlayHintLabelPart { &self.text } - pub fn target(&self) -> Option<&crate::NavigationTarget> { + pub fn into_text(self) -> String { + self.text + } + + pub fn target(&self) -> Option<&NavigationTarget> { self.target.as_ref() } + + pub fn with_target(self, target: Option) -> Self { + Self { target, ..self } + } } impl From for InlayHintLabelPart { @@ -195,11 +214,18 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { if !self.settings.variable_types { return; } - self.hints - .push(InlayHint::variable_type(position, ty, self.db)); + + let inlay_hint = InlayHint::variable_type(position, ty, self.db); + + self.hints.push(inlay_hint); } - fn add_call_argument_name(&mut self, position: TextSize, name: &str) { + fn add_call_argument_name( + &mut self, + position: TextSize, + name: &str, + navigation_target: Option, + ) { if !self.settings.call_argument_names { return; } @@ -208,8 +234,9 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { return; } - self.hints - .push(InlayHint::call_argument_name(position, name)); + let inlay_hint = InlayHint::call_argument_name(position, name, navigation_target); + + self.hints.push(inlay_hint); } } @@ -275,18 +302,20 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { 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(); + let details = inlay_hint_call_argument_details(self.db, &self.model, call) + .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) + if let Some((name, parameter_label_offset)) = details.argument_names.get(&index) && !arg_matches_name(&arg_or_keyword, name) { - self.add_call_argument_name(arg_or_keyword.range().start(), name); + self.add_call_argument_name( + arg_or_keyword.range().start(), + name, + parameter_label_offset.map(NavigationTarget::from), + ); } self.visit_expr(arg_or_keyword.value()); } @@ -345,7 +374,7 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool { // This one expands to `Template` which isn't verbose but is redundant | Expr::TString(_)=> true, - // You too `+1 and `-1`, get back here + // You too `+1 and `-1`, get back here Expr::UnaryOp(ExprUnaryOp { op: UnaryOp::UAdd | UnaryOp::USub, operand, .. }) => matches!(**operand, Expr::NumberLiteral(_)), // Everything else is reasonable @@ -357,9 +386,15 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool { mod tests { use super::*; + use crate::NavigationTarget; + use crate::tests::IntoDiagnostic; use insta::assert_snapshot; use ruff_db::{ - files::{File, system_path_to_file}, + diagnostic::{ + Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, + LintName, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, + }, + files::{File, FileRange, system_path_to_file}, source::source_text, }; use ruff_python_trivia::textwrap::dedent; @@ -423,19 +458,74 @@ mod tests { }) } + fn with_extra_file(&mut self, file_name: &str, content: &str) { + self.db.write_file(file_name, content).unwrap(); + } + /// Returns the inlay hints for the given test case with custom settings. fn inlay_hints_with_settings(&self, settings: &InlayHintSettings) -> String { let hints = inlay_hints(&self.db, self.file, self.range, settings); let mut buf = source_text(&self.db, self.file).as_str().to_string(); + let mut diagnostics = Vec::new(); + let mut offset = 0; for hint in hints { + let mut hint_str = "[".to_string(); + let end_position = (hint.position.to_u32() as usize) + offset; - let hint_str = format!("[{}]", hint.display()); - buf.insert_str(end_position, &hint_str); + + for part in hint.label.parts() { + hint_str.push_str(part.text()); + + if let Some(target) = part.target() { + let label_range = TextRange::at(hint.position, TextSize::ZERO); + + let label_file_range = FileRange::new(self.file, label_range); + + diagnostics + .push(InlayHintLocationDiagnostic::new(label_file_range, target)); + } + } + + hint_str.push(']'); + offset += hint_str.len(); + + buf.insert_str(end_position, &hint_str); + } + + let mut rendered_diagnostics = self.render_diagnostics(diagnostics); + + if !rendered_diagnostics.is_empty() { + rendered_diagnostics = format!( + "{}{}", + crate::MarkupKind::PlainText.horizontal_line(), + rendered_diagnostics + ); + } + + format!("{buf}{rendered_diagnostics}",) + } + + fn render_diagnostics(&self, diagnostics: I) -> String + where + I: IntoIterator, + D: IntoDiagnostic, + { + use std::fmt::Write; + + let mut buf = String::new(); + + let config = DisplayDiagnosticConfig::default() + .color(false) + .format(DiagnosticFormat::Full); + + for diagnostic in diagnostics { + let diag = diagnostic.into_diagnostic(); + write!(buf, "{}", diag.display(&self.db, &config)).unwrap(); } buf @@ -588,7 +678,7 @@ mod tests { " def i(x: int, /) -> int: return x - + x: int = 1 y = x z: int = i(1) @@ -646,6 +736,26 @@ mod tests { a[: A] = A([y=]2) a.y[: int] = int(3) + + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class A: + 3 | def __init__(self, y): + | ^ + 4 | self.x = int(1) + 5 | self.y = y + | + info: Source + --> main.py:7:7 + | + 5 | self.y = y + 6 | + 7 | a = A(2) + | ^ + 8 | a.y = int(3) + | "); } @@ -821,7 +931,7 @@ mod tests { class MyClass: def __init__(self): self.x: int = 1 - + x = MyClass() y = (MyClass(), MyClass()) a, b = MyClass(), MyClass() @@ -849,7 +959,7 @@ mod tests { def __init__(self, x: list[T], y: tuple[U, U]): self.x = x self.y = y - + x = MyClass([42], ("a", "b")) y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) @@ -867,6 +977,271 @@ mod tests { y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) + + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:7:13 + | + 5 | self.y = y + 6 | + 7 | x = MyClass([42], ("a", "b")) + | ^ + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:36 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:7:19 + | + 5 | self.y = y + 6 | + 7 | x = MyClass([42], ("a", "b")) + | ^ + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:8:14 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:36 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:8:20 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:8:41 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:36 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:8:47 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:9:16 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + | ^ + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:36 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:9:22 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + | ^ + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:9:43 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + | ^ + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:36 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:9:49 + | + 7 | x = MyClass([42], ("a", "b")) + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + | ^ + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:10:17 + | + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:36 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:10:23 + | + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:10:44 + | + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:36 + | + 2 | class MyClass[T, U]: + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + | ^ + 4 | self.x = x + 5 | self.y = y + | + info: Source + --> main.py:10:50 + | + 8 | y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + 9 | a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + 10 | c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + | ^ + | "#); } @@ -906,6 +1281,21 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int): pass foo([x=]1) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | foo(1) + | + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int): pass + 3 | foo(1) + | ^ + | "); } @@ -926,6 +1316,23 @@ mod tests { y = 2 foo(x) foo([x=]y) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | x = 1 + 4 | y = 2 + | + info: Source + --> main.py:6:5 + | + 4 | y = 2 + 5 | foo(x) + 6 | foo(y) + | ^ + | "); } @@ -954,6 +1361,22 @@ mod tests { foo(val.x) foo([x=]val.y) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | class MyClass: + 4 | def __init__(self): + | + info: Source + --> main.py:10:5 + | + 9 | foo(val.x) + 10 | foo(val.y) + | ^ + | "); } @@ -983,6 +1406,22 @@ mod tests { foo(x.x) foo([x=]x.y) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | class MyClass: + 4 | def __init__(self): + | + info: Source + --> main.py:10:5 + | + 9 | foo(x.x) + 10 | foo(x.y) + | ^ + | "); } @@ -1015,6 +1454,22 @@ mod tests { foo(val.x()) foo([x=]val.y()) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | class MyClass: + 4 | def __init__(self): + | + info: Source + --> main.py:12:5 + | + 11 | foo(val.x()) + 12 | foo(val.y()) + | ^ + | "); } @@ -1051,6 +1506,24 @@ mod tests { foo(val.x()[0]) foo([x=]val.y()[1]) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:4:9 + | + 2 | from typing import List + 3 | + 4 | def foo(x: int): pass + | ^ + 5 | class MyClass: + 6 | def __init__(self): + | + info: Source + --> main.py:14:5 + | + 13 | foo(val.x()[0]) + 14 | foo(val.y()[1]) + | ^ + | "); } @@ -1073,6 +1546,22 @@ mod tests { foo(x[0]) foo([x=]y[0]) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | x = [1] + 4 | y = [2] + | + info: Source + --> main.py:7:5 + | + 6 | foo(x[0]) + 7 | foo(y[0]) + | ^ + | "); } @@ -1143,6 +1632,21 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, /, y: int): pass foo(1, [y=]2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:20 + | + 2 | def foo(x: int, /, y: int): pass + | ^ + 3 | foo(1, 2) + | + info: Source + --> main.py:3:8 + | + 2 | def foo(x: int, /, y: int): pass + 3 | foo(1, 2) + | ^ + | "); } @@ -1189,6 +1693,43 @@ mod tests { def __init__(self, x: int): pass Foo([x=]1) f[: Foo] = Foo([x=]1) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class Foo: + 3 | def __init__(self, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: Source + --> main.py:4:5 + | + 2 | class Foo: + 3 | def __init__(self, x: int): pass + 4 | Foo(1) + | ^ + 5 | f = Foo(1) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class Foo: + 3 | def __init__(self, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: Source + --> main.py:5:9 + | + 3 | def __init__(self, x: int): pass + 4 | Foo(1) + 5 | f = Foo(1) + | ^ + | "); } @@ -1207,6 +1748,43 @@ mod tests { def __new__(cls, x: int): pass Foo([x=]1) f[: Foo] = Foo([x=]1) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:22 + | + 2 | class Foo: + 3 | def __new__(cls, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: Source + --> main.py:4:5 + | + 2 | class Foo: + 3 | def __new__(cls, x: int): pass + 4 | Foo(1) + | ^ + 5 | f = Foo(1) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:22 + | + 2 | class Foo: + 3 | def __new__(cls, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: Source + --> main.py:5:9 + | + 3 | def __new__(cls, x: int): pass + 4 | Foo(1) + 5 | f = Foo(1) + | ^ + | "); } @@ -1227,6 +1805,24 @@ mod tests { class Foo(metaclass=MetaFoo): pass Foo([x=]1) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MetaFoo: + 3 | def __call__(self, x: int): pass + | ^ + 4 | class Foo(metaclass=MetaFoo): + 5 | pass + | + info: Source + --> main.py:6:5 + | + 4 | class Foo(metaclass=MetaFoo): + 5 | pass + 6 | Foo(1) + | ^ + | "); } @@ -1259,6 +1855,23 @@ mod tests { class Foo: def bar(self, y: int): pass Foo().bar([y=]2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:19 + | + 2 | class Foo: + 3 | def bar(self, y: int): pass + | ^ + 4 | Foo().bar(2) + | + info: Source + --> main.py:4:11 + | + 2 | class Foo: + 3 | def bar(self, y: int): pass + 4 | Foo().bar(2) + | ^ + | "); } @@ -1277,6 +1890,24 @@ mod tests { @classmethod def bar(cls, y: int): pass Foo.bar([y=]2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:4:18 + | + 2 | class Foo: + 3 | @classmethod + 4 | def bar(cls, y: int): pass + | ^ + 5 | Foo.bar(2) + | + info: Source + --> main.py:5:9 + | + 3 | @classmethod + 4 | def bar(cls, y: int): pass + 5 | Foo.bar(2) + | ^ + | "); } @@ -1295,6 +1926,24 @@ mod tests { @staticmethod def bar(y: int): pass Foo.bar([y=]2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:4:13 + | + 2 | class Foo: + 3 | @staticmethod + 4 | def bar(y: int): pass + | ^ + 5 | Foo.bar(2) + | + info: Source + --> main.py:5:9 + | + 3 | @staticmethod + 4 | def bar(y: int): pass + 5 | Foo.bar(2) + | ^ + | "); } @@ -1311,6 +1960,40 @@ mod tests { def foo(x: int | str): pass foo([x=]1) foo([x=]'abc') + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int | str): pass + | ^ + 3 | foo(1) + 4 | foo('abc') + | + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int | str): pass + 3 | foo(1) + | ^ + 4 | foo('abc') + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int | str): pass + | ^ + 3 | foo(1) + 4 | foo('abc') + | + info: Source + --> main.py:4:5 + | + 2 | def foo(x: int | str): pass + 3 | foo(1) + 4 | foo('abc') + | ^ + | "); } @@ -1325,6 +2008,51 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, y: str, z: bool): pass foo([x=]1, [y=]'hello', [z=]True) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, 'hello', True) + | + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, 'hello', True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:17 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, 'hello', True) + | + info: Source + --> main.py:3:8 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, 'hello', True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:25 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, 'hello', True) + | + info: Source + --> main.py:3:17 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, 'hello', True) + | ^ + | "); } @@ -1339,6 +2067,21 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, y: str, z: bool): pass foo([x=]1, z=True, y='hello') + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, z=True, y='hello') + | + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, z=True, y='hello') + | ^ + | "); } @@ -1357,6 +2100,111 @@ mod tests { foo([x=]1) foo([x=]1, [y=]'custom') foo([x=]1, [y=]'custom', [z=]True) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + 3 | foo(1) + | ^ + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: Source + --> main.py:4:5 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + 3 | foo(1) + 4 | foo(1, 'custom') + | ^ + 5 | foo(1, 'custom', True) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:17 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: Source + --> main.py:4:8 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + 3 | foo(1) + 4 | foo(1, 'custom') + | ^ + 5 | foo(1, 'custom', True) + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: Source + --> main.py:5:5 + | + 3 | foo(1) + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:17 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: Source + --> main.py:5:8 + | + 3 | foo(1) + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:37 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: Source + --> main.py:5:18 + | + 3 | foo(1) + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | ^ + | "); } @@ -1385,6 +2233,115 @@ mod tests { def baz(a: int, b: str, c: bool): pass baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:8:9 + | + 6 | return y + 7 | + 8 | def baz(a: int, b: str, c: bool): pass + | ^ + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | + info: Source + --> main.py:10:5 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int) -> int: + | ^ + 3 | return x * 2 + | + info: Source + --> main.py:10:9 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:8:17 + | + 6 | return y + 7 | + 8 | def baz(a: int, b: str, c: bool): pass + | ^ + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | + info: Source + --> main.py:10:13 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 3 | return x * 2 + 4 | + 5 | def bar(y: str) -> str: + | ^ + 6 | return y + | + info: Source + --> main.py:10:17 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 3 | return x * 2 + 4 | + 5 | def bar(y: str) -> str: + | ^ + 6 | return y + | + info: Source + --> main.py:10:21 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:8:25 + | + 6 | return y + 7 | + 8 | def baz(a: int, b: str, c: bool): pass + | ^ + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | + info: Source + --> main.py:10:31 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | "); } @@ -1409,6 +2366,43 @@ mod tests { return self def baz(self): pass A().foo([value=]42).bar([name=]'test').baz() + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:19 + | + 2 | class A: + 3 | def foo(self, value: int) -> 'A': + | ^^^^^ + 4 | return self + 5 | def bar(self, name: str) -> 'A': + | + info: Source + --> main.py:8:9 + | + 6 | return self + 7 | def baz(self): pass + 8 | A().foo(42).bar('test').baz() + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:19 + | + 3 | def foo(self, value: int) -> 'A': + 4 | return self + 5 | def bar(self, name: str) -> 'A': + | ^^^^ + 6 | return self + 7 | def baz(self): pass + | + info: Source + --> main.py:8:17 + | + 6 | return self + 7 | def baz(self): pass + 8 | A().foo(42).bar('test').baz() + | ^ + | "); } @@ -1428,6 +2422,24 @@ mod tests { return x def bar(y: int): pass bar(y=foo([x=]'test')) + + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: str) -> str: + | ^ + 3 | return x + 4 | def bar(y: int): pass + | + info: Source + --> main.py:5:11 + | + 3 | return x + 4 | def bar(y: int): pass + 5 | bar(y=foo('test')) + | ^ + | "); } @@ -1462,35 +2474,97 @@ mod tests { 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') + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:28 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + | ^ + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + info: Source + --> main.py:3:15 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + 3 | foo(1, 'pos', 3.14, False, e=42) + | ^ + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:38 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + | ^ + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + info: Source + --> main.py:3:21 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + 3 | foo(1, 'pos', 3.14, False, e=42) + | ^ + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:28 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + | ^ + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + info: Source + --> main.py:4:15 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | ^ + | "); } #[test] - fn test_generic_function_calls() { - let test = inlay_hint_test( + fn test_function_calls_different_file() { + let mut test = inlay_hint_test( " - from typing import TypeVar, Generic + from foo import bar - T = TypeVar('T') - - def identity(x: T) -> T: - return x - - identity(42) - identity('hello')", + bar(1)", ); - assert_snapshot!(test.inlay_hints(), @r###" - from typing import TypeVar, Generic + test.with_extra_file( + "foo.py", + " + def bar(x: int | str): + pass", + ); - T[: typing.TypeVar] = TypeVar([name=]'T') + assert_snapshot!(test.inlay_hints(), @r" + from foo import bar - def identity(x: T) -> T: - return x - - identity([x=]42) - identity([x=]'hello') - "###); + bar([x=]1) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> foo.py:2:17 + | + 2 | def bar(x: int | str): + | ^ + 3 | pass + | + info: Source + --> main.py:4:5 + | + 2 | from foo import bar + 3 | + 4 | bar(1) + | ^ + | + "); } #[test] @@ -1522,6 +2596,42 @@ mod tests { foo([x=]42) foo([x=]'hello') + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 4 | @overload + 5 | def foo(x: int) -> str: ... + | ^ + 6 | @overload + 7 | def foo(x: str) -> int: ... + | + info: Source + --> main.py:11:5 + | + 9 | return x + 10 | + 11 | foo(42) + | ^ + 12 | foo('hello') + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 4 | @overload + 5 | def foo(x: int) -> str: ... + | ^ + 6 | @overload + 7 | def foo(x: str) -> int: ... + | + info: Source + --> main.py:12:5 + | + 11 | foo(42) + 12 | foo('hello') + | ^ + | "); } @@ -1557,6 +2667,24 @@ mod tests { def bar(y: int): pass foo([x=]1) bar(2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | def bar(y: int): pass + 4 | foo(1) + | + info: Source + --> main.py:4:5 + | + 2 | def foo(x: int): pass + 3 | def bar(y: int): pass + 4 | foo(1) + | ^ + 5 | bar(2) + | "); } @@ -1571,6 +2699,117 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(_x: int, y: int): pass foo(1, [y=]2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:18 + | + 2 | def foo(_x: int, y: int): pass + | ^ + 3 | foo(1, 2) + | + info: Source + --> main.py:3:8 + | + 2 | def foo(_x: int, y: int): pass + 3 | foo(1, 2) + | ^ + | "); } + + #[test] + fn test_function_call_different_formatting() { + let test = inlay_hint_test( + " + def foo( + x: int, + y: int + ): ... + + foo(1, 2)", + ); + + assert_snapshot!(test.inlay_hints(), @r" + def foo( + x: int, + y: int + ): ... + + foo([x=]1, [y=]2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:5 + | + 2 | def foo( + 3 | x: int, + | ^ + 4 | y: int + 5 | ): ... + | + info: Source + --> main.py:7:5 + | + 5 | ): ... + 6 | + 7 | foo(1, 2) + | ^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:4:5 + | + 2 | def foo( + 3 | x: int, + 4 | y: int + | ^ + 5 | ): ... + | + info: Source + --> main.py:7:8 + | + 5 | ): ... + 6 | + 7 | foo(1, 2) + | ^ + | + "); + } + + struct InlayHintLocationDiagnostic { + source: FileRange, + target: FileRange, + } + + impl InlayHintLocationDiagnostic { + fn new(source: FileRange, target: &NavigationTarget) -> Self { + Self { + source, + target: FileRange::new(target.file(), target.focus_range()), + } + } + } + + impl IntoDiagnostic for InlayHintLocationDiagnostic { + fn into_diagnostic(self) -> Diagnostic { + let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); + + source.annotate(Annotation::primary( + Span::from(self.source.file()).with_range(self.source.range()), + )); + + let mut main = Diagnostic::new( + DiagnosticId::Lint(LintName::of("inlay-hint-location")), + Severity::Info, + "Inlay Hint Target".to_string(), + ); + + main.annotate(Annotation::primary( + Span::from(self.target.file()).with_range(self.target.range()), + )); + + main.sub(source); + + main + } + } } diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 057d75b688..92f14813d4 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -132,6 +132,20 @@ impl NavigationTarget { pub fn full_range(&self) -> TextRange { self.full_range } + + pub fn full_file_range(&self) -> FileRange { + FileRange::new(self.file, self.full_range) + } +} + +impl From for NavigationTarget { + fn from(value: FileRange) -> Self { + Self { + file: value.file(), + focus_range: value.range(), + full_range: value.range(), + } + } } /// Specifies the kind of reference operation. diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index d57869825a..7647629d88 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -958,6 +958,22 @@ pub struct CallSignatureDetails<'db> { pub argument_to_parameter_mapping: Vec>, } +impl CallSignatureDetails<'_> { + fn get_definition_parameter_range(&self, db: &dyn Db, name: &str) -> Option { + let definition = self.signature.definition()?; + let file = definition.file(db); + let module_ref = parsed_module(db, file).load(db); + + let parameters = match definition.kind(db) { + DefinitionKind::Function(node) => &node.node(&module_ref).parameters, + // TODO: lambda functions + _ => return None, + }; + + Some(FileRange::new(file, parameters.find(name)?.name().range)) + } +} + /// Extract signature details from a function call expression. /// This function analyzes the callable being invoked and returns zero or more /// `CallSignatureDetails` objects, each representing one possible signature @@ -1153,15 +1169,16 @@ pub fn find_active_signature_from_details( } #[derive(Default)] -pub struct InlayHintFunctionArgumentDetails { - pub argument_names: HashMap, +pub struct InlayHintCallArgumentDetails { + /// The position of the arguments mapped to their name and the range of the argument definition in the signature. + pub argument_names: HashMap)>, } -pub fn inlay_hint_function_argument_details<'db>( +pub fn inlay_hint_call_argument_details<'db>( db: &'db dyn Db, model: &SemanticModel<'db>, call_expr: &ast::ExprCall, -) -> Option { +) -> Option { let signature_details = call_signature_details(db, model, call_expr); if signature_details.is_empty() { @@ -1173,6 +1190,7 @@ pub fn inlay_hint_function_argument_details<'db>( 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() { @@ -1195,16 +1213,19 @@ pub fn inlay_hint_function_argument_details<'db>( continue; }; + let parameter_label_offset = + call_signature_details.get_definition_parameter_range(db, param.name()?); + // 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()); + argument_names.insert(arg_index, (name.to_string(), parameter_label_offset)); } } - Some(InlayHintFunctionArgumentDetails { argument_names }) + Some(InlayHintCallArgumentDetails { argument_names }) } /// Find the text range of a specific parameter in function parameters by name. diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index 42f51db85f..ada6f18302 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -5,7 +5,8 @@ use lsp_types::{InlayHintParams, Url}; use ty_ide::{InlayHintKind, InlayHintLabel, inlay_hints}; use ty_project::ProjectDatabase; -use crate::document::{RangeExt, TextSizeExt}; +use crate::PositionEncoding; +use crate::document::{RangeExt, TextSizeExt, ToLink}; use crate::server::api::traits::{ BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, }; @@ -57,7 +58,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { .position .to_lsp_position(db, file, snapshot.encoding())? .local_position(), - label: inlay_hint_label(&hint.label), + label: inlay_hint_label(&hint.label, db, snapshot.encoding()), kind: Some(inlay_hint_kind(&hint.kind)), tooltip: None, padding_left: None, @@ -81,12 +82,18 @@ fn inlay_hint_kind(inlay_hint_kind: &InlayHintKind) -> lsp_types::InlayHintKind } } -fn inlay_hint_label(inlay_hint_label: &InlayHintLabel) -> lsp_types::InlayHintLabel { +fn inlay_hint_label( + inlay_hint_label: &InlayHintLabel, + db: &ProjectDatabase, + encoding: PositionEncoding, +) -> lsp_types::InlayHintLabel { let mut label_parts = Vec::new(); for part in inlay_hint_label.parts() { label_parts.push(lsp_types::InlayHintLabelPart { value: part.text().into(), - location: None, + location: part + .target() + .and_then(|target| target.to_location(db, encoding)), tooltip: None, command: None, }); diff --git a/crates/ty_server/tests/e2e/inlay_hints.rs b/crates/ty_server/tests/e2e/inlay_hints.rs index 682c31c763..42152a7351 100644 --- a/crates/ty_server/tests/e2e/inlay_hints.rs +++ b/crates/ty_server/tests/e2e/inlay_hints.rs @@ -59,7 +59,20 @@ y = foo(1) }, "label": [ { - "value": "a" + "value": "a", + "location": { + "uri": "file:///src/foo.py", + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 9 + } + } + } }, { "value": "=" diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index ca16fbd36c..3faf75a4d5 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -19,7 +19,7 @@ use ty_ide::{ InlayHintSettings, MarkupKind, RangedValue, document_highlights, goto_declaration, goto_definition, goto_references, goto_type_definition, hover, inlay_hints, }; -use ty_ide::{NavigationTargets, signature_help}; +use ty_ide::{NavigationTarget, NavigationTargets, signature_help}; use ty_project::metadata::options::Options; use ty_project::metadata::value::ValueSource; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; @@ -469,7 +469,22 @@ impl Workspace { Ok(result .into_iter() .map(|hint| InlayHint { - markdown: hint.display().to_string(), + label: hint + .label + .into_parts() + .into_iter() + .map(|part| InlayHintLabelPart { + location: part.target().map(|target| { + location_link_from_navigation_target( + target, + &self.db, + self.position_encoding, + None, + ) + }), + label: part.into_text(), + }) + .collect(), position: Position::from_text_size( hint.position, &index, @@ -639,19 +654,8 @@ fn map_targets_to_links( targets .into_iter() - .map(|target| LocationLink { - path: target.file().path(db).to_string(), - full_range: Range::from_file_range( - db, - FileRange::new(target.file(), target.full_range()), - position_encoding, - ), - selection_range: Some(Range::from_file_range( - db, - FileRange::new(target.file(), target.focus_range()), - position_encoding, - )), - origin_selection_range: Some(source_range), + .map(|target| { + location_link_from_navigation_target(&target, db, position_encoding, Some(source_range)) }) .collect() } @@ -905,6 +909,7 @@ impl From for ruff_source_file::PositionEncoding { } #[wasm_bindgen] +#[derive(Clone)] pub struct LocationLink { /// The target file path #[wasm_bindgen(getter_with_clone)] @@ -918,6 +923,24 @@ pub struct LocationLink { pub origin_selection_range: Option, } +fn location_link_from_navigation_target( + target: &NavigationTarget, + db: &dyn Db, + position_encoding: PositionEncoding, + source_range: Option, +) -> LocationLink { + LocationLink { + path: target.file().path(db).to_string(), + full_range: Range::from_file_range(db, target.full_file_range(), position_encoding), + selection_range: Some(Range::from_file_range( + db, + FileRange::new(target.file(), target.focus_range()), + position_encoding, + )), + origin_selection_range: source_range, + } +} + #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Hover { @@ -1032,16 +1055,25 @@ impl From for InlayHintKind { } #[wasm_bindgen] -#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { #[wasm_bindgen(getter_with_clone)] - pub markdown: String, + pub label: Vec, pub position: Position, pub kind: InlayHintKind, } +#[wasm_bindgen] +#[derive(Clone)] +pub struct InlayHintLabelPart { + #[wasm_bindgen(getter_with_clone)] + pub label: String, + + #[wasm_bindgen(getter_with_clone)] + pub location: Option, +} + #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SemanticToken { diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index b7755d54de..ed1efd9042 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -28,6 +28,7 @@ import { DocumentHighlight, DocumentHighlightKind, InlayHintKind, + LocationLink, TextEdit, } from "ty_wasm"; import { FileId, ReadonlyFiles } from "../Playground"; @@ -445,7 +446,10 @@ class PlaygroundServer return { dispose: () => {}, hints: inlayHints.map((hint) => ({ - label: hint.markdown, + label: hint.label.map((part) => ({ + label: part.label, + // As of 2025-09-23, location isn't supported by Monaco which is why we don't set it + })), position: { lineNumber: hint.position.line, column: hint.position.column, @@ -763,57 +767,59 @@ class PlaygroundServer return null; } - private mapNavigationTargets(links: any[]): languages.LocationLink[] { - const result = links.map((link) => { - const uri = Uri.parse(link.path); + private mapNavigationTarget(link: LocationLink): languages.LocationLink { + const uri = Uri.parse(link.path); - // Pre-create models to ensure peek definition works - if (this.monaco.editor.getModel(uri) == null) { - if (uri.scheme === "vendored") { - // Handle vendored files - const vendoredPath = this.getVendoredPath(uri); - const fileHandle = this.getOrCreateVendoredFileHandle(vendoredPath); - const content = this.props.workspace.sourceText(fileHandle); - this.monaco.editor.createModel(content, "python", uri); - } else { - // Handle regular files - const fileId = this.props.files.index.find((file) => { - return Uri.file(file.name).toString() === uri.toString(); - })?.id; + // Pre-create models to ensure peek definition works + if (this.monaco.editor.getModel(uri) == null) { + if (uri.scheme === "vendored") { + // Handle vendored files + const vendoredPath = this.getVendoredPath(uri); + const fileHandle = this.getOrCreateVendoredFileHandle(vendoredPath); + const content = this.props.workspace.sourceText(fileHandle); + this.monaco.editor.createModel(content, "python", uri); + } else { + // Handle regular files + const fileId = this.props.files.index.find((file) => { + return Uri.file(file.name).toString() === uri.toString(); + })?.id; - if (fileId != null) { - const handle = this.props.files.handles[fileId]; - if (handle != null) { - const language = isPythonFile(handle) ? "python" : undefined; - this.monaco.editor.createModel( - this.props.files.contents[fileId], - language, - uri, - ); - } + if (fileId != null) { + const handle = this.props.files.handles[fileId]; + if (handle != null) { + const language = isPythonFile(handle) ? "python" : undefined; + this.monaco.editor.createModel( + this.props.files.contents[fileId], + language, + uri, + ); } } } + } - const targetSelection = - link.selection_range == null - ? undefined - : tyRangeToMonacoRange(link.selection_range); + const targetSelection = + link.selection_range == null + ? undefined + : tyRangeToMonacoRange(link.selection_range); - const originSelection = - link.origin_selection_range == null - ? undefined - : tyRangeToMonacoRange(link.origin_selection_range); + const originSelection = + link.origin_selection_range == null + ? undefined + : tyRangeToMonacoRange(link.origin_selection_range); - return { - uri: uri, - range: tyRangeToMonacoRange(link.full_range), - targetSelectionRange: targetSelection, - originSelectionRange: originSelection, - } as languages.LocationLink; - }); + return { + uri: uri, + range: tyRangeToMonacoRange(link.full_range), + targetSelectionRange: targetSelection, + originSelectionRange: originSelection, + } as languages.LocationLink; + } - return result; + private mapNavigationTargets( + links: LocationLink[], + ): languages.LocationLink[] { + return links.map((link) => this.mapNavigationTarget(link)); } dispose() {