From 6f9265d78d934291b118d5d08e1315a0af0a5ab5 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 24 Nov 2025 19:48:30 +0000 Subject: [PATCH] [ty] Double click to insert inlay hint (#21600) ## Summary Resolves https://github.com/astral-sh/ty/issues/317#issuecomment-3567398107. I can't get the auto import working great. I haven't added many places where we specify that the type display is invalid syntax. ## Test Plan Nothing yet --- crates/ty_ide/src/inlay_hints.rs | 555 +++++++++++++++++- crates/ty_ide/src/lib.rs | 4 +- .../ty_python_semantic/src/types/display.rs | 126 ++-- .../src/server/api/requests/inlay_hints.rs | 35 +- crates/ty_server/tests/e2e/inlay_hints.rs | 20 +- 5 files changed, 668 insertions(+), 72 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 161c3568ca..b905e6c066 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -6,8 +6,8 @@ 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_call_argument_details; +use ty_python_semantic::types::{Type, TypeDetail}; use ty_python_semantic::{HasType, SemanticModel}; #[derive(Debug, Clone)] @@ -15,10 +15,12 @@ pub struct InlayHint { pub position: TextSize, pub kind: InlayHintKind, pub label: InlayHintLabel, + pub text_edits: Vec, } impl InlayHint { - fn variable_type(position: TextSize, ty: Type, db: &dyn Db) -> Self { + fn variable_type(expr: &Expr, ty: Type, db: &dyn Db, allow_edits: bool) -> Self { + let position = expr.range().end(); // Render the type to a string, and get subspans for all the types that make it up let details = ty.display(db).to_string_parts(); @@ -34,7 +36,7 @@ impl InlayHint { let mut label_parts = vec![": ".into()]; for (target, detail) in details.targets.iter().zip(&details.details) { match detail { - ty_python_semantic::types::TypeDetail::Type(ty) => { + TypeDetail::Type(ty) => { let start = target.start().to_usize(); let end = target.end().to_usize(); // If we skipped over some bytes, push them with no target @@ -50,9 +52,9 @@ impl InlayHint { offset = end; } } - ty_python_semantic::types::TypeDetail::SignatureStart - | ty_python_semantic::types::TypeDetail::SignatureEnd - | ty_python_semantic::types::TypeDetail::Parameter(_) => { + TypeDetail::SignatureStart + | TypeDetail::SignatureEnd + | TypeDetail::Parameter(_) => { // Don't care about these } } @@ -62,10 +64,20 @@ impl InlayHint { label_parts.push(details.label[offset..details.label.len()].into()); } + let text_edits = if details.is_valid_syntax && allow_edits { + vec![InlayHintTextEdit { + range: TextRange::new(position, position), + new_text: format!(": {}", details.label), + }] + } else { + vec![] + }; + Self { position, kind: InlayHintKind::Type, label: InlayHintLabel { parts: label_parts }, + text_edits, } } @@ -83,6 +95,7 @@ impl InlayHint { position, kind: InlayHintKind::CallArgumentName, label: InlayHintLabel { parts: label_parts }, + text_edits: vec![], } } @@ -175,6 +188,12 @@ impl From<&str> for InlayHintLabelPart { } } +#[derive(Debug, Clone)] +pub struct InlayHintTextEdit { + pub range: TextRange, + pub new_text: String, +} + pub fn inlay_hints( db: &dyn Db, file: File, @@ -234,6 +253,7 @@ struct InlayHintVisitor<'a, 'db> { in_assignment: bool, range: TextRange, settings: &'a InlayHintSettings, + in_no_edits_allowed: bool, } impl<'a, 'db> InlayHintVisitor<'a, 'db> { @@ -245,15 +265,16 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { in_assignment: false, range, settings, + in_no_edits_allowed: false, } } - fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) { + fn add_type_hint(&mut self, expr: &Expr, ty: Type<'db>, allow_edits: bool) { if !self.settings.variable_types { return; } - let inlay_hint = InlayHint::variable_type(position, ty, self.db); + let inlay_hint = InlayHint::variable_type(expr, ty, self.db, allow_edits); self.hints.push(inlay_hint); } @@ -297,9 +318,13 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { match stmt { Stmt::Assign(assign) => { self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value); + if !annotations_are_valid_syntax(assign) { + self.in_no_edits_allowed = true; + } for target in &assign.targets { self.visit_expr(target); } + self.in_no_edits_allowed = false; self.in_assignment = false; self.visit_expr(&assign.value); @@ -325,7 +350,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { if self.in_assignment { if name.ctx.is_store() { let ty = expr.inferred_type(&self.model); - self.add_type_hint(expr.range().end(), ty); + self.add_type_hint(expr, ty, !self.in_no_edits_allowed); } } source_order::walk_expr(self, expr); @@ -334,7 +359,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { if self.in_assignment { if attribute.ctx.is_store() { let ty = expr.inferred_type(&self.model); - self.add_type_hint(expr.range().end(), ty); + self.add_type_hint(expr, ty, !self.in_no_edits_allowed); } } source_order::walk_expr(self, expr); @@ -420,6 +445,22 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool { } } +fn annotations_are_valid_syntax(stmt_assign: &ruff_python_ast::StmtAssign) -> bool { + if stmt_assign.targets.len() > 1 { + return false; + } + + if stmt_assign + .targets + .iter() + .any(|target| matches!(target, Expr::Tuple(_))) + { + return false; + } + + true +} + #[cfg(test)] mod tests { use super::*; @@ -427,6 +468,7 @@ mod tests { use crate::NavigationTarget; use crate::tests::IntoDiagnostic; use insta::{assert_snapshot, internals::SettingsBindDropGuard}; + use itertools::Itertools; use ruff_db::{ diagnostic::{ Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, @@ -517,12 +559,15 @@ mod tests { fn inlay_hints_with_settings(&mut 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 inlay_hint_buf = source_text(&self.db, self.file).as_str().to_string(); + let mut text_edit_buf = inlay_hint_buf.clone(); let mut tbd_diagnostics = Vec::new(); let mut offset = 0; + let mut edit_offset = 0; + for hint in hints { let end_position = hint.position.to_usize() + offset; let mut hint_str = "[".to_string(); @@ -538,36 +583,65 @@ mod tests { hint_str.push_str(part.text()); } + for edit in hint.text_edits { + let start = edit.range.start().to_usize() + edit_offset; + let end = edit.range.end().to_usize() + edit_offset; + + text_edit_buf.replace_range(start..end, &edit.new_text); + + if start == end { + edit_offset += edit.new_text.len(); + } else { + edit_offset += edit.new_text.len() - edit.range.len().to_usize(); + } + } + hint_str.push(']'); offset += hint_str.len(); - buf.insert_str(end_position, &hint_str); + inlay_hint_buf.insert_str(end_position, &hint_str); } - self.db.write_file("main2.py", &buf).unwrap(); + self.db.write_file("main2.py", &inlay_hint_buf).unwrap(); let inlayed_file = system_path_to_file(&self.db, "main2.py").expect("newly written file to existing"); - let diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| { + let location_diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| { InlayHintLocationDiagnostic::new(FileRange::new(inlayed_file, label_range), &target) }); - let mut rendered_diagnostics = self.render_diagnostics(diagnostics); + let mut rendered_diagnostics = location_diagnostics + .map(|diagnostic| self.render_diagnostic(diagnostic)) + .join(""); if !rendered_diagnostics.is_empty() { rendered_diagnostics = format!( "{}{}", crate::MarkupKind::PlainText.horizontal_line(), rendered_diagnostics + .strip_suffix("\n") + .unwrap_or(&rendered_diagnostics) ); } - format!("{buf}{rendered_diagnostics}",) + let rendered_edit_diagnostic = if edit_offset != 0 { + let edit_diagnostic = InlayHintEditDiagnostic::new(text_edit_buf); + let text_edit_buf = self.render_diagnostic(edit_diagnostic); + + format!( + "{}{}", + crate::MarkupKind::PlainText.horizontal_line(), + text_edit_buf + ) + } else { + String::new() + }; + + format!("{inlay_hint_buf}{rendered_diagnostics}{rendered_edit_diagnostic}",) } - fn render_diagnostics(&self, diagnostics: I) -> String + fn render_diagnostic(&self, diagnostic: D) -> String where - I: IntoIterator, D: IntoDiagnostic, { use std::fmt::Write; @@ -578,10 +652,8 @@ mod tests { .color(false) .format(DiagnosticFormat::Full); - for diagnostic in diagnostics { - let diag = diagnostic.into_diagnostic(); - write!(buf, "{}", diag.display(&self.db, &config)).unwrap(); - } + let diag = diagnostic.into_diagnostic(); + write!(buf, "{}", diag.display(&self.db, &config)).unwrap(); buf } @@ -728,6 +800,20 @@ mod tests { 10 | bb[: Literal[b"foo"]] = aa | ^^^^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def i(x: int, /) -> int: + return x + + x = 1 + y: Literal[1] = x + z: int = i(1) + w: int = z + aa = b'foo' + bb: Literal[b"foo"] = aa "#); } @@ -1321,6 +1407,20 @@ mod tests { 10 | w[: tuple[int, str]] = z | ^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def i(x: int, /) -> int: + return x + def s(x: str, /) -> str: + return x + + x = (1, 'abc') + y: tuple[Literal[1], Literal["abc"]] = x + z: tuple[int, str] = (i(1), s('abc')) + w: tuple[int, str] = z "#); } @@ -1654,6 +1754,18 @@ mod tests { 8 | w[: int] = z | ^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def i(x: int, /) -> int: + return x + + x: int = 1 + y: Literal[1] = x + z: int = i(1) + w: int = z "#); } @@ -1691,6 +1803,15 @@ mod tests { | ^^^ 5 | z = x | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def i(x: int, /) -> int: + return x + x: int = i(1) + z = x "#); } @@ -1810,6 +1931,18 @@ mod tests { 8 | a.y[: int] = int(3) | ^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + class A: + def __init__(self, y): + self.x: int = int(1) + self.y: Unknown = y + + a: A = A(2) + a.y: int = int(3) "#); } @@ -2640,6 +2773,22 @@ mod tests { 12 | k[: list[Unknown | int | float]] = [-1, -2.0] | ^^^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + a: list[Unknown | int] = [1, 2] + b: list[Unknown | float] = [1.0, 2.0] + c: list[Unknown | bool] = [True, False] + d: list[Unknown | None] = [None, None] + e: list[Unknown | str] = ["hel", "lo"] + f: list[Unknown | str] = ['the', 're'] + g: list[Unknown | str] = [f"{ft}", f"{ft}"] + h: list[Unknown | Template] = [t"wow %d", t"wow %d"] + i: list[Unknown | bytes] = [b'/x01', b'/x02'] + j: list[Unknown | int | float] = [+1, +2.0] + k: list[Unknown | int | float] = [-1, -2.0] "#); } @@ -2811,6 +2960,19 @@ mod tests { 9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass()) | ^^^^^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + class MyClass: + def __init__(self): + self.x: int = 1 + + x: MyClass = MyClass() + y: tuple[MyClass, MyClass] = (MyClass(), MyClass()) + a, b = MyClass(), MyClass() + c, d = (MyClass(), MyClass()) "#); } @@ -3681,6 +3843,20 @@ mod tests { 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + class MyClass[T, U]: + def __init__(self, x: list[T], y: tuple[U, U]): + self.x = x + self.y = y + + x: MyClass[Unknown | int, str] = MyClass([42], ("a", "b")) + y: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]] = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) + a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b")) + c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))) "#); } @@ -3836,6 +4012,20 @@ mod tests { 10 | foo([x=]val.y) | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def foo(x: int): pass + class MyClass: + def __init__(self): + self.x: int = 1 + self.y: int = 2 + val: MyClass = MyClass() + + foo(val.x) + foo(val.y) "); } @@ -3901,6 +4091,20 @@ mod tests { 10 | foo([x=]x.y) | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def foo(x: int): pass + class MyClass: + def __init__(self): + self.x: int = 1 + self.y: int = 2 + x: MyClass = MyClass() + + foo(x.x) + foo(x.y) "); } @@ -3969,6 +4173,22 @@ mod tests { 12 | foo([x=]val.y()) | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def foo(x: int): pass + class MyClass: + def __init__(self): + def x() -> int: + return 1 + def y() -> int: + return 2 + val: MyClass = MyClass() + + foo(val.x()) + foo(val.y()) "); } @@ -4043,6 +4263,24 @@ mod tests { 14 | foo([x=]val.y()[1]) | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + from typing import List + + def foo(x: int): pass + class MyClass: + def __init__(self): + def x() -> List[int]: + return 1 + def y() -> List[int]: + return 2 + val: MyClass = MyClass() + + foo(val.x()[0]) + foo(val.y()[1]) "); } @@ -4193,6 +4431,17 @@ mod tests { 7 | foo([x=]y[0]) | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def foo(x: int): pass + x: list[Unknown | int] = [1] + y: list[Unknown | int] = [2] + + foo(x[0]) + foo(y[0]) "#); } @@ -4378,6 +4627,15 @@ mod tests { 5 | f[: Foo] = Foo([x=]1) | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + class Foo: + def __init__(self, x: int): pass + Foo(1) + f: Foo = Foo(1) "); } @@ -4450,6 +4708,15 @@ mod tests { 5 | f[: Foo] = Foo([x=]1) | ^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + class Foo: + def __new__(cls, x: int): pass + Foo(1) + f: Foo = Foo(1) "); } @@ -5197,6 +5464,15 @@ mod tests { | ^^^^^^^^^^^^^ 5 | my_func(x="hello") | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + from typing import LiteralString + def my_func(x: LiteralString): + y: LiteralString = x + my_func(x="hello") "#); } @@ -5339,6 +5615,23 @@ mod tests { 13 | y[: Literal[1, 2, 3, "hello"] | None] = x | ^^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def branch(cond: int): + if cond < 10: + x = 1 + elif cond < 20: + x = 2 + elif cond < 30: + x = 3 + elif cond < 40: + x = "hello" + else: + x = None + y: Literal[1, 2, 3, "hello"] | None = x "#); } @@ -5454,6 +5747,13 @@ mod tests { 3 | y[: type[list[str]]] = type(x) | ^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + def f(x: list[str]): + y: type[list[str]] = type(x) "#); } @@ -5493,6 +5793,16 @@ mod tests { 6 | ab[: property] = F.whatever | ^^^^^^^^ | + + --------------------------------------------- + info[inlay-hint-edit]: File after edits + info: Source + + class F: + @property + def whatever(self): ... + + ab: property = F.whatever "); } @@ -5810,6 +6120,180 @@ mod tests { "); } + #[test] + fn test_function_signature_inlay_hint() { + let mut test = inlay_hint_test( + " + def foo(x: int, *y: bool, z: str | int | list[str]): ... + + a = foo", + ); + + assert_snapshot!(test.inlay_hints(), @r#" + def foo(x: int, *y: bool, z: str | int | list[str]): ... + + a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | ^^^ + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | + info: Source + --> main2.py:4:16 + | + 2 | def foo(x: int, *y: bool, z: str | int | list[str]): ... + 3 | + 4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + | ^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2591:7 + | + 2590 | @final + 2591 | class bool(int): + | ^^^^ + 2592 | """Returns True when the argument is true, False otherwise. + 2593 | The builtins True and False are the only two instances of the class bool. + | + info: Source + --> main2.py:4:25 + | + 2 | def foo(x: int, *y: bool, z: str | int | list[str]): ... + 3 | + 4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + | ^^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | ^^^ + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main2.py:4:37 + | + 2 | def foo(x: int, *y: bool, z: str | int | list[str]): ... + 3 | + 4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + | ^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | ^^^ + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | + info: Source + --> main2.py:4:43 + | + 2 | def foo(x: int, *y: bool, z: str | int | list[str]): ... + 3 | + 4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + | ^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2802:7 + | + 2801 | @disjoint_base + 2802 | class list(MutableSequence[_T]): + | ^^^^ + 2803 | """Built-in mutable sequence. + | + info: Source + --> main2.py:4:49 + | + 2 | def foo(x: int, *y: bool, z: str | int | list[str]): ... + 3 | + 4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + | ^^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | ^^^ + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main2.py:4:54 + | + 2 | def foo(x: int, *y: bool, z: str | int | list[str]): ... + 3 | + 4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + | ^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/ty_extensions.pyi:20:1 + | + 19 | # Types + 20 | Unknown = object() + | ^^^^^^^ + 21 | AlwaysTruthy = object() + 22 | AlwaysFalsy = object() + | + info: Source + --> main2.py:4:63 + | + 2 | def foo(x: int, *y: bool, z: str | int | list[str]): ... + 3 | + 4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo + | ^^^^^^^ + | + "#); + } + + #[test] + fn test_module_inlay_hint() { + let mut test = inlay_hint_test( + " + import foo + + a = foo", + ); + + test.with_extra_file("foo.py", "'''Foo module'''"); + + assert_snapshot!(test.inlay_hints(), @r" + import foo + + a[: ] = foo + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> foo.py:1:1 + | + 1 | '''Foo module''' + | ^^^^^^^^^^^^^^^^ + | + info: Source + --> main2.py:4:5 + | + 2 | import foo + 3 | + 4 | a[: ] = foo + | ^^^^^^^^^^^^^^ + | + "); + } + struct InlayHintLocationDiagnostic { source: FileRange, target: FileRange, @@ -5847,4 +6331,31 @@ mod tests { main } } + + struct InlayHintEditDiagnostic { + file_content: String, + } + + impl InlayHintEditDiagnostic { + fn new(file_content: String) -> Self { + Self { file_content } + } + } + + impl IntoDiagnostic for InlayHintEditDiagnostic { + fn into_diagnostic(self) -> Diagnostic { + let mut main = Diagnostic::new( + DiagnosticId::Lint(LintName::of("inlay-hint-edit")), + Severity::Info, + "File after edits".to_string(), + ); + + main.sub(SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format!("{}\n{}", "Source", self.file_content), + )); + + main + } + } } diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 92f14813d4..664412f881 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -33,7 +33,9 @@ pub use document_symbols::document_symbols; pub use goto::{goto_declaration, goto_definition, goto_type_definition}; pub use goto_references::goto_references; pub use hover::hover; -pub use inlay_hints::{InlayHintKind, InlayHintLabel, InlayHintSettings, inlay_hints}; +pub use inlay_hints::{ + InlayHintKind, InlayHintLabel, InlayHintSettings, InlayHintTextEdit, inlay_hints, +}; pub use markup::MarkupKind; pub use references::ReferencesMode; pub use rename::{can_rename, rename}; diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index f86adc2b1f..78bd91b795 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -107,6 +107,8 @@ pub struct TypeDisplayDetails<'db> { pub targets: Vec, /// Metadata for each range pub details: Vec>, + /// Whether the label is valid Python syntax + pub is_valid_syntax: bool, } /// Abstraction over "are we doing normal formatting, or tracking ranges with metadata?" @@ -119,6 +121,7 @@ struct TypeDetailsWriter<'db> { label: String, targets: Vec, details: Vec>, + is_valid_syntax: bool, } impl<'db> TypeDetailsWriter<'db> { @@ -127,6 +130,7 @@ impl<'db> TypeDetailsWriter<'db> { label: String::new(), targets: Vec::new(), details: Vec::new(), + is_valid_syntax: true, } } @@ -136,6 +140,7 @@ impl<'db> TypeDetailsWriter<'db> { label: self.label, targets: self.targets, details: self.details, + is_valid_syntax: self.is_valid_syntax, } } @@ -192,6 +197,13 @@ impl<'a, 'b, 'db> TypeWriter<'a, 'b, 'db> { self.with_detail(TypeDetail::Type(ty)) } + fn set_invalid_syntax(&mut self) { + match self { + TypeWriter::Formatter(_) => {} + TypeWriter::Details(details) => details.is_valid_syntax = false, + } + } + fn join<'c>(&'c mut self, separator: &'static str) -> Join<'a, 'b, 'c, 'db> { Join { fmt: self, @@ -539,6 +551,7 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> { let line_index = line_index(self.db, file); let class_offset = self.class.header_range(self.db).start(); let line_number = line_index.line_index(class_offset); + f.set_invalid_syntax(); write!(f, " @ {path}:{line_number}")?; } Ok(()) @@ -599,6 +612,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .fmt_detailed(f), }, Protocol::Synthesized(synthetic) => { + f.set_invalid_syntax(); f.write_char('<')?; f.with_type(Type::SpecialForm(SpecialFormType::Protocol)) .write_str("Protocol")?; @@ -618,6 +632,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { }, Type::PropertyInstance(_) => f.with_type(self.ty).write_str("property"), Type::ModuleLiteral(module) => { + f.set_invalid_syntax(); write!( f.with_type(self.ty), "", @@ -625,6 +640,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { ) } Type::ClassLiteral(class) => { + f.set_invalid_syntax(); let mut f = f.with_type(self.ty); f.write_str(" FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_str("'>") } Type::GenericAlias(generic) => { + f.set_invalid_syntax(); let mut f = f.with_type(self.ty); f.write_str(" FmtDetailed<'db> for DisplayRepresentation<'db> { db: self.db, settings: self.settings.clone(), }; - + f.set_invalid_syntax(); f.write_str("bound method ")?; self_ty .display_with(self.db, self.settings.singleline()) @@ -729,51 +746,57 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } } } - Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet(function)) => { - write!( - f, - "", - function = function.name(self.db), - ) + Type::KnownBoundMethod(method_type) => { + f.set_invalid_syntax(); + match method_type { + KnownBoundMethodType::FunctionTypeDunderGet(function) => { + write!( + f, + "", + function = function.name(self.db), + ) + } + KnownBoundMethodType::FunctionTypeDunderCall(function) => { + write!( + f, + "", + function = function.name(self.db), + ) + } + KnownBoundMethodType::PropertyDunderGet(_) => { + f.write_str("") + } + KnownBoundMethodType::PropertyDunderSet(_) => { + f.write_str("") + } + KnownBoundMethodType::StrStartswith(_) => { + f.write_str("") + } + KnownBoundMethodType::ConstraintSetRange => { + f.write_str("bound method `ConstraintSet.range`") + } + KnownBoundMethodType::ConstraintSetAlways => { + f.write_str("bound method `ConstraintSet.always`") + } + KnownBoundMethodType::ConstraintSetNever => { + f.write_str("bound method `ConstraintSet.never`") + } + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => { + f.write_str("bound method `ConstraintSet.implies_subtype_of`") + } + KnownBoundMethodType::ConstraintSetSatisfies(_) => { + f.write_str("bound method `ConstraintSet.satisfies`") + } + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { + f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`") + } + KnownBoundMethodType::GenericContextSpecializeConstrained(_) => { + f.write_str("bound method `GenericContext.specialize_constrained`") + } + } } - Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(function)) => { - write!( - f, - "", - function = function.name(self.db), - ) - } - Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderGet(_)) => { - f.write_str("") - } - Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(_)) => { - f.write_str("") - } - Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(_)) => { - f.write_str("") - } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => { - f.write_str("bound method `ConstraintSet.range`") - } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetAlways) => { - f.write_str("bound method `ConstraintSet.always`") - } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetNever) => { - f.write_str("bound method `ConstraintSet.never`") - } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => { - f.write_str("bound method `ConstraintSet.implies_subtype_of`") - } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfies(_)) => { - f.write_str("bound method `ConstraintSet.satisfies`") - } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars( - _, - )) => f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`"), - Type::KnownBoundMethod(KnownBoundMethodType::GenericContextSpecializeConstrained( - _, - )) => f.write_str("bound method `GenericContext.specialize_constrained`"), Type::WrapperDescriptor(kind) => { + f.set_invalid_syntax(); let (method, object) = match kind { WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"), @@ -782,9 +805,11 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { write!(f, "") } Type::DataclassDecorator(_) => { + f.set_invalid_syntax(); f.write_str("") } Type::DataclassTransformer(_) => { + f.set_invalid_syntax(); f.write_str("") } Type::Union(union) => union @@ -828,11 +853,13 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { write!(f, ".{}", enum_literal.name(self.db)) } Type::TypeVar(bound_typevar) => { + f.set_invalid_syntax(); write!(f, "{}", bound_typevar.identity(self.db).display(self.db)) } Type::AlwaysTruthy => f.with_type(self.ty).write_str("AlwaysTruthy"), Type::AlwaysFalsy => f.with_type(self.ty).write_str("AlwaysFalsy"), Type::BoundSuper(bound_super) => { + f.set_invalid_syntax(); f.write_str(" FmtDetailed<'db> for DisplayRepresentation<'db> { .display_with(self.db, self.settings.singleline()) .fmt_detailed(f)?; if let Some(name) = type_is.place_name(self.db) { + f.set_invalid_syntax(); f.write_str(" @ ")?; f.write_str(&name)?; } @@ -1029,6 +1057,7 @@ impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> { settings: self.settings.clone(), }; + f.set_invalid_syntax(); f.write_str("def ")?; write!(f, "{}", self.literal.name(self.db))?; type_parameters.fmt_detailed(f)?; @@ -1075,7 +1104,7 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { db: self.db, settings: self.settings.clone(), }; - + f.set_invalid_syntax(); f.write_str("def ")?; write!(f, "{}", self.ty.name(self.db))?; type_parameters.fmt_detailed(f)?; @@ -1256,6 +1285,7 @@ impl<'db> DisplayGenericContext<'_, 'db> { if idx > 0 { f.write_str(", ")?; } + f.set_invalid_syntax(); f.write_str(bound_typevar.typevar(self.db).name(self.db))?; } f.write_char(']') @@ -1268,6 +1298,7 @@ impl<'db> DisplayGenericContext<'_, 'db> { if idx > 0 { f.write_str(", ")?; } + f.set_invalid_syntax(); write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?; } f.write_char(']') @@ -1358,6 +1389,7 @@ impl<'db> DisplaySpecialization<'db> { if idx > 0 { f.write_str(", ")?; } + f.set_invalid_syntax(); write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?; f.write_str(" = ")?; ty.display_with(self.db, self.settings.clone()) @@ -1505,6 +1537,7 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { // Immediately write a marker signaling we're starting a signature let _ = f.with_detail(TypeDetail::SignatureStart); + f.set_invalid_syntax(); // When we exit this function, write a marker signaling we're ending a signature let mut f = f.with_detail(TypeDetail::SignatureEnd); let multiline = self.settings.multiline && self.parameters.len() > 1; @@ -1694,6 +1727,7 @@ impl<'db> FmtDetailed<'db> for DisplayOmitted { } else { self.plural }; + f.set_invalid_syntax(); write!(f, "... omitted {} {}", self.count, noun) } } @@ -1908,6 +1942,7 @@ impl<'db> FmtDetailed<'db> for DisplayIntersectionType<'_, 'db> { }), ); + f.set_invalid_syntax(); f.join(" & ").entries(tys).finish() } } @@ -1960,6 +1995,7 @@ struct DisplayMaybeParenthesizedType<'db> { impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { let write_parentheses = |f: &mut TypeWriter<'_, '_, 'db>| { + f.set_invalid_syntax(); f.write_char('(')?; self.ty .display_with(self.db, self.settings.clone()) 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 ada6f18302..8ed6ddf056 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -2,7 +2,8 @@ use std::borrow::Cow; use lsp_types::request::InlayHintRequest; use lsp_types::{InlayHintParams, Url}; -use ty_ide::{InlayHintKind, InlayHintLabel, inlay_hints}; +use ruff_db::files::File; +use ty_ide::{InlayHintKind, InlayHintLabel, InlayHintTextEdit, inlay_hints}; use ty_project::ProjectDatabase; use crate::PositionEncoding; @@ -64,7 +65,14 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { padding_left: None, padding_right: None, data: None, - text_edits: None, + text_edits: Some( + hint.text_edits + .into_iter() + .filter_map(|text_edit| { + inlay_hint_text_edit(text_edit, db, file, snapshot.encoding()) + }) + .collect(), + ), }) }) .collect(); @@ -100,3 +108,26 @@ fn inlay_hint_label( } lsp_types::InlayHintLabel::LabelParts(label_parts) } + +fn inlay_hint_text_edit( + inlay_hint_text_edit: InlayHintTextEdit, + db: &ProjectDatabase, + file: File, + encoding: PositionEncoding, +) -> Option { + Some(lsp_types::TextEdit { + range: lsp_types::Range { + start: inlay_hint_text_edit + .range + .start() + .to_lsp_position(db, file, encoding)? + .local_position(), + end: inlay_hint_text_edit + .range + .end() + .to_lsp_position(db, file, encoding)? + .local_position(), + }, + new_text: inlay_hint_text_edit.new_text, + }) +} diff --git a/crates/ty_server/tests/e2e/inlay_hints.rs b/crates/ty_server/tests/e2e/inlay_hints.rs index 3ee77ed7e4..974f97c3de 100644 --- a/crates/ty_server/tests/e2e/inlay_hints.rs +++ b/crates/ty_server/tests/e2e/inlay_hints.rs @@ -63,7 +63,22 @@ y = foo(1) } } ], - "kind": 1 + "kind": 1, + "textEdits": [ + { + "range": { + "start": { + "line": 5, + "character": 1 + }, + "end": { + "line": 5, + "character": 1 + } + }, + "newText": ": int" + } + ] }, { "position": { @@ -91,7 +106,8 @@ y = foo(1) "value": "=" } ], - "kind": 2 + "kind": 2, + "textEdits": [] } ] "#);