[ty] Double click to insert inlay hint (#21600)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## 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
This commit is contained in:
Matthew Mckee 2025-11-24 19:48:30 +00:00 committed by GitHub
parent 0c6d652b5f
commit 6f9265d78d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 668 additions and 72 deletions

View File

@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp}; use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp};
use ruff_text_size::{Ranged, TextRange, TextSize}; 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::ide_support::inlay_hint_call_argument_details;
use ty_python_semantic::types::{Type, TypeDetail};
use ty_python_semantic::{HasType, SemanticModel}; use ty_python_semantic::{HasType, SemanticModel};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -15,10 +15,12 @@ pub struct InlayHint {
pub position: TextSize, pub position: TextSize,
pub kind: InlayHintKind, pub kind: InlayHintKind,
pub label: InlayHintLabel, pub label: InlayHintLabel,
pub text_edits: Vec<InlayHintTextEdit>,
} }
impl InlayHint { 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 // 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(); let details = ty.display(db).to_string_parts();
@ -34,7 +36,7 @@ impl InlayHint {
let mut label_parts = vec![": ".into()]; let mut label_parts = vec![": ".into()];
for (target, detail) in details.targets.iter().zip(&details.details) { for (target, detail) in details.targets.iter().zip(&details.details) {
match detail { match detail {
ty_python_semantic::types::TypeDetail::Type(ty) => { TypeDetail::Type(ty) => {
let start = target.start().to_usize(); let start = target.start().to_usize();
let end = target.end().to_usize(); let end = target.end().to_usize();
// If we skipped over some bytes, push them with no target // If we skipped over some bytes, push them with no target
@ -50,9 +52,9 @@ impl InlayHint {
offset = end; offset = end;
} }
} }
ty_python_semantic::types::TypeDetail::SignatureStart TypeDetail::SignatureStart
| ty_python_semantic::types::TypeDetail::SignatureEnd | TypeDetail::SignatureEnd
| ty_python_semantic::types::TypeDetail::Parameter(_) => { | TypeDetail::Parameter(_) => {
// Don't care about these // Don't care about these
} }
} }
@ -62,10 +64,20 @@ impl InlayHint {
label_parts.push(details.label[offset..details.label.len()].into()); 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 { Self {
position, position,
kind: InlayHintKind::Type, kind: InlayHintKind::Type,
label: InlayHintLabel { parts: label_parts }, label: InlayHintLabel { parts: label_parts },
text_edits,
} }
} }
@ -83,6 +95,7 @@ impl InlayHint {
position, position,
kind: InlayHintKind::CallArgumentName, kind: InlayHintKind::CallArgumentName,
label: InlayHintLabel { parts: label_parts }, 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( pub fn inlay_hints(
db: &dyn Db, db: &dyn Db,
file: File, file: File,
@ -234,6 +253,7 @@ struct InlayHintVisitor<'a, 'db> {
in_assignment: bool, in_assignment: bool,
range: TextRange, range: TextRange,
settings: &'a InlayHintSettings, settings: &'a InlayHintSettings,
in_no_edits_allowed: bool,
} }
impl<'a, 'db> InlayHintVisitor<'a, 'db> { impl<'a, 'db> InlayHintVisitor<'a, 'db> {
@ -245,15 +265,16 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
in_assignment: false, in_assignment: false,
range, range,
settings, 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 { if !self.settings.variable_types {
return; 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); self.hints.push(inlay_hint);
} }
@ -297,9 +318,13 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
match stmt { match stmt {
Stmt::Assign(assign) => { Stmt::Assign(assign) => {
self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value); 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 { for target in &assign.targets {
self.visit_expr(target); self.visit_expr(target);
} }
self.in_no_edits_allowed = false;
self.in_assignment = false; self.in_assignment = false;
self.visit_expr(&assign.value); self.visit_expr(&assign.value);
@ -325,7 +350,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
if self.in_assignment { if self.in_assignment {
if name.ctx.is_store() { if name.ctx.is_store() {
let ty = expr.inferred_type(&self.model); 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); source_order::walk_expr(self, expr);
@ -334,7 +359,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
if self.in_assignment { if self.in_assignment {
if attribute.ctx.is_store() { if attribute.ctx.is_store() {
let ty = expr.inferred_type(&self.model); 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); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -427,6 +468,7 @@ mod tests {
use crate::NavigationTarget; use crate::NavigationTarget;
use crate::tests::IntoDiagnostic; use crate::tests::IntoDiagnostic;
use insta::{assert_snapshot, internals::SettingsBindDropGuard}; use insta::{assert_snapshot, internals::SettingsBindDropGuard};
use itertools::Itertools;
use ruff_db::{ use ruff_db::{
diagnostic::{ diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
@ -517,12 +559,15 @@ mod tests {
fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String { fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String {
let hints = inlay_hints(&self.db, self.file, self.range, settings); 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 tbd_diagnostics = Vec::new();
let mut offset = 0; let mut offset = 0;
let mut edit_offset = 0;
for hint in hints { for hint in hints {
let end_position = hint.position.to_usize() + offset; let end_position = hint.position.to_usize() + offset;
let mut hint_str = "[".to_string(); let mut hint_str = "[".to_string();
@ -538,36 +583,65 @@ mod tests {
hint_str.push_str(part.text()); 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(']'); hint_str.push(']');
offset += hint_str.len(); 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 = let inlayed_file =
system_path_to_file(&self.db, "main2.py").expect("newly written file to existing"); 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) 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() { if !rendered_diagnostics.is_empty() {
rendered_diagnostics = format!( rendered_diagnostics = format!(
"{}{}", "{}{}",
crate::MarkupKind::PlainText.horizontal_line(), crate::MarkupKind::PlainText.horizontal_line(),
rendered_diagnostics 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<I, D>(&self, diagnostics: I) -> String fn render_diagnostic<D>(&self, diagnostic: D) -> String
where where
I: IntoIterator<Item = D>,
D: IntoDiagnostic, D: IntoDiagnostic,
{ {
use std::fmt::Write; use std::fmt::Write;
@ -578,10 +652,8 @@ mod tests {
.color(false) .color(false)
.format(DiagnosticFormat::Full); .format(DiagnosticFormat::Full);
for diagnostic in diagnostics {
let diag = diagnostic.into_diagnostic(); let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap(); write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
}
buf buf
} }
@ -728,6 +800,20 @@ mod tests {
10 | bb[: Literal[b"foo"]] = aa 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 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 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 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) 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] 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()) 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", " 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) 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) 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()) 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]) 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]) 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) 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) 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") 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 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) 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 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[: <module 'foo'>] = 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[: <module 'foo'>] = foo
| ^^^^^^^^^^^^^^
|
");
}
struct InlayHintLocationDiagnostic { struct InlayHintLocationDiagnostic {
source: FileRange, source: FileRange,
target: FileRange, target: FileRange,
@ -5847,4 +6331,31 @@ mod tests {
main 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
}
}
} }

View File

@ -33,7 +33,9 @@ pub use document_symbols::document_symbols;
pub use goto::{goto_declaration, goto_definition, goto_type_definition}; pub use goto::{goto_declaration, goto_definition, goto_type_definition};
pub use goto_references::goto_references; pub use goto_references::goto_references;
pub use hover::hover; 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 markup::MarkupKind;
pub use references::ReferencesMode; pub use references::ReferencesMode;
pub use rename::{can_rename, rename}; pub use rename::{can_rename, rename};

View File

@ -107,6 +107,8 @@ pub struct TypeDisplayDetails<'db> {
pub targets: Vec<TextRange>, pub targets: Vec<TextRange>,
/// Metadata for each range /// Metadata for each range
pub details: Vec<TypeDetail<'db>>, pub details: Vec<TypeDetail<'db>>,
/// Whether the label is valid Python syntax
pub is_valid_syntax: bool,
} }
/// Abstraction over "are we doing normal formatting, or tracking ranges with metadata?" /// Abstraction over "are we doing normal formatting, or tracking ranges with metadata?"
@ -119,6 +121,7 @@ struct TypeDetailsWriter<'db> {
label: String, label: String,
targets: Vec<TextRange>, targets: Vec<TextRange>,
details: Vec<TypeDetail<'db>>, details: Vec<TypeDetail<'db>>,
is_valid_syntax: bool,
} }
impl<'db> TypeDetailsWriter<'db> { impl<'db> TypeDetailsWriter<'db> {
@ -127,6 +130,7 @@ impl<'db> TypeDetailsWriter<'db> {
label: String::new(), label: String::new(),
targets: Vec::new(), targets: Vec::new(),
details: Vec::new(), details: Vec::new(),
is_valid_syntax: true,
} }
} }
@ -136,6 +140,7 @@ impl<'db> TypeDetailsWriter<'db> {
label: self.label, label: self.label,
targets: self.targets, targets: self.targets,
details: self.details, 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)) 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> { fn join<'c>(&'c mut self, separator: &'static str) -> Join<'a, 'b, 'c, 'db> {
Join { Join {
fmt: self, fmt: self,
@ -539,6 +551,7 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> {
let line_index = line_index(self.db, file); let line_index = line_index(self.db, file);
let class_offset = self.class.header_range(self.db).start(); let class_offset = self.class.header_range(self.db).start();
let line_number = line_index.line_index(class_offset); let line_number = line_index.line_index(class_offset);
f.set_invalid_syntax();
write!(f, " @ {path}:{line_number}")?; write!(f, " @ {path}:{line_number}")?;
} }
Ok(()) Ok(())
@ -599,6 +612,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
.fmt_detailed(f), .fmt_detailed(f),
}, },
Protocol::Synthesized(synthetic) => { Protocol::Synthesized(synthetic) => {
f.set_invalid_syntax();
f.write_char('<')?; f.write_char('<')?;
f.with_type(Type::SpecialForm(SpecialFormType::Protocol)) f.with_type(Type::SpecialForm(SpecialFormType::Protocol))
.write_str("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::PropertyInstance(_) => f.with_type(self.ty).write_str("property"),
Type::ModuleLiteral(module) => { Type::ModuleLiteral(module) => {
f.set_invalid_syntax();
write!( write!(
f.with_type(self.ty), f.with_type(self.ty),
"<module '{}'>", "<module '{}'>",
@ -625,6 +640,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
) )
} }
Type::ClassLiteral(class) => { Type::ClassLiteral(class) => {
f.set_invalid_syntax();
let mut f = f.with_type(self.ty); let mut f = f.with_type(self.ty);
f.write_str("<class '")?; f.write_str("<class '")?;
class class
@ -633,6 +649,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
f.write_str("'>") f.write_str("'>")
} }
Type::GenericAlias(generic) => { Type::GenericAlias(generic) => {
f.set_invalid_syntax();
let mut f = f.with_type(self.ty); let mut f = f.with_type(self.ty);
f.write_str("<class '")?; f.write_str("<class '")?;
generic generic
@ -691,7 +708,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
db: self.db, db: self.db,
settings: self.settings.clone(), settings: self.settings.clone(),
}; };
f.set_invalid_syntax();
f.write_str("bound method ")?; f.write_str("bound method ")?;
self_ty self_ty
.display_with(self.db, self.settings.singleline()) .display_with(self.db, self.settings.singleline())
@ -729,51 +746,57 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
} }
} }
} }
Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet(function)) => { Type::KnownBoundMethod(method_type) => {
f.set_invalid_syntax();
match method_type {
KnownBoundMethodType::FunctionTypeDunderGet(function) => {
write!( write!(
f, f,
"<method-wrapper `__get__` of `{function}`>", "<method-wrapper `__get__` of `{function}`>",
function = function.name(self.db), function = function.name(self.db),
) )
} }
Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(function)) => { KnownBoundMethodType::FunctionTypeDunderCall(function) => {
write!( write!(
f, f,
"<method-wrapper `__call__` of `{function}`>", "<method-wrapper `__call__` of `{function}`>",
function = function.name(self.db), function = function.name(self.db),
) )
} }
Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderGet(_)) => { KnownBoundMethodType::PropertyDunderGet(_) => {
f.write_str("<method-wrapper `__get__` of `property` object>") f.write_str("<method-wrapper `__get__` of `property` object>")
} }
Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(_)) => { KnownBoundMethodType::PropertyDunderSet(_) => {
f.write_str("<method-wrapper `__set__` of `property` object>") f.write_str("<method-wrapper `__set__` of `property` object>")
} }
Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(_)) => { KnownBoundMethodType::StrStartswith(_) => {
f.write_str("<method-wrapper `startswith` of `str` object>") f.write_str("<method-wrapper `startswith` of `str` object>")
} }
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => { KnownBoundMethodType::ConstraintSetRange => {
f.write_str("bound method `ConstraintSet.range`") f.write_str("bound method `ConstraintSet.range`")
} }
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetAlways) => { KnownBoundMethodType::ConstraintSetAlways => {
f.write_str("bound method `ConstraintSet.always`") f.write_str("bound method `ConstraintSet.always`")
} }
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetNever) => { KnownBoundMethodType::ConstraintSetNever => {
f.write_str("bound method `ConstraintSet.never`") f.write_str("bound method `ConstraintSet.never`")
} }
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)) => { KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => {
f.write_str("bound method `ConstraintSet.implies_subtype_of`") f.write_str("bound method `ConstraintSet.implies_subtype_of`")
} }
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfies(_)) => { KnownBoundMethodType::ConstraintSetSatisfies(_) => {
f.write_str("bound method `ConstraintSet.satisfies`") f.write_str("bound method `ConstraintSet.satisfies`")
} }
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars( KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
_, f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`")
)) => f.write_str("bound method `ConstraintSet.satisfied_by_all_typevars`"), }
Type::KnownBoundMethod(KnownBoundMethodType::GenericContextSpecializeConstrained( KnownBoundMethodType::GenericContextSpecializeConstrained(_) => {
_, f.write_str("bound method `GenericContext.specialize_constrained`")
)) => f.write_str("bound method `GenericContext.specialize_constrained`"), }
}
}
Type::WrapperDescriptor(kind) => { Type::WrapperDescriptor(kind) => {
f.set_invalid_syntax();
let (method, object) = match kind { let (method, object) = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),
WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"), WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"),
@ -782,9 +805,11 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
write!(f, "<wrapper-descriptor `{method}` of `{object}` objects>") write!(f, "<wrapper-descriptor `{method}` of `{object}` objects>")
} }
Type::DataclassDecorator(_) => { Type::DataclassDecorator(_) => {
f.set_invalid_syntax();
f.write_str("<decorator produced by dataclass-like function>") f.write_str("<decorator produced by dataclass-like function>")
} }
Type::DataclassTransformer(_) => { Type::DataclassTransformer(_) => {
f.set_invalid_syntax();
f.write_str("<decorator produced by typing.dataclass_transform>") f.write_str("<decorator produced by typing.dataclass_transform>")
} }
Type::Union(union) => union Type::Union(union) => union
@ -828,11 +853,13 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
write!(f, ".{}", enum_literal.name(self.db)) write!(f, ".{}", enum_literal.name(self.db))
} }
Type::TypeVar(bound_typevar) => { Type::TypeVar(bound_typevar) => {
f.set_invalid_syntax();
write!(f, "{}", bound_typevar.identity(self.db).display(self.db)) write!(f, "{}", bound_typevar.identity(self.db).display(self.db))
} }
Type::AlwaysTruthy => f.with_type(self.ty).write_str("AlwaysTruthy"), Type::AlwaysTruthy => f.with_type(self.ty).write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.with_type(self.ty).write_str("AlwaysFalsy"), Type::AlwaysFalsy => f.with_type(self.ty).write_str("AlwaysFalsy"),
Type::BoundSuper(bound_super) => { Type::BoundSuper(bound_super) => {
f.set_invalid_syntax();
f.write_str("<super: ")?; f.write_str("<super: ")?;
Type::from(bound_super.pivot_class(self.db)) Type::from(bound_super.pivot_class(self.db))
.display_with(self.db, self.settings.singleline()) .display_with(self.db, self.settings.singleline())
@ -852,6 +879,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
.display_with(self.db, self.settings.singleline()) .display_with(self.db, self.settings.singleline())
.fmt_detailed(f)?; .fmt_detailed(f)?;
if let Some(name) = type_is.place_name(self.db) { if let Some(name) = type_is.place_name(self.db) {
f.set_invalid_syntax();
f.write_str(" @ ")?; f.write_str(" @ ")?;
f.write_str(&name)?; f.write_str(&name)?;
} }
@ -1029,6 +1057,7 @@ impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> {
settings: self.settings.clone(), settings: self.settings.clone(),
}; };
f.set_invalid_syntax();
f.write_str("def ")?; f.write_str("def ")?;
write!(f, "{}", self.literal.name(self.db))?; write!(f, "{}", self.literal.name(self.db))?;
type_parameters.fmt_detailed(f)?; type_parameters.fmt_detailed(f)?;
@ -1075,7 +1104,7 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> {
db: self.db, db: self.db,
settings: self.settings.clone(), settings: self.settings.clone(),
}; };
f.set_invalid_syntax();
f.write_str("def ")?; f.write_str("def ")?;
write!(f, "{}", self.ty.name(self.db))?; write!(f, "{}", self.ty.name(self.db))?;
type_parameters.fmt_detailed(f)?; type_parameters.fmt_detailed(f)?;
@ -1256,6 +1285,7 @@ impl<'db> DisplayGenericContext<'_, 'db> {
if idx > 0 { if idx > 0 {
f.write_str(", ")?; f.write_str(", ")?;
} }
f.set_invalid_syntax();
f.write_str(bound_typevar.typevar(self.db).name(self.db))?; f.write_str(bound_typevar.typevar(self.db).name(self.db))?;
} }
f.write_char(']') f.write_char(']')
@ -1268,6 +1298,7 @@ impl<'db> DisplayGenericContext<'_, 'db> {
if idx > 0 { if idx > 0 {
f.write_str(", ")?; f.write_str(", ")?;
} }
f.set_invalid_syntax();
write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?; write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?;
} }
f.write_char(']') f.write_char(']')
@ -1358,6 +1389,7 @@ impl<'db> DisplaySpecialization<'db> {
if idx > 0 { if idx > 0 {
f.write_str(", ")?; f.write_str(", ")?;
} }
f.set_invalid_syntax();
write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?; write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?;
f.write_str(" = ")?; f.write_str(" = ")?;
ty.display_with(self.db, self.settings.clone()) 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 { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
// Immediately write a marker signaling we're starting a signature // Immediately write a marker signaling we're starting a signature
let _ = f.with_detail(TypeDetail::SignatureStart); let _ = f.with_detail(TypeDetail::SignatureStart);
f.set_invalid_syntax();
// When we exit this function, write a marker signaling we're ending a signature // When we exit this function, write a marker signaling we're ending a signature
let mut f = f.with_detail(TypeDetail::SignatureEnd); let mut f = f.with_detail(TypeDetail::SignatureEnd);
let multiline = self.settings.multiline && self.parameters.len() > 1; let multiline = self.settings.multiline && self.parameters.len() > 1;
@ -1694,6 +1727,7 @@ impl<'db> FmtDetailed<'db> for DisplayOmitted {
} else { } else {
self.plural self.plural
}; };
f.set_invalid_syntax();
write!(f, "... omitted {} {}", self.count, noun) 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() f.join(" & ").entries(tys).finish()
} }
} }
@ -1960,6 +1995,7 @@ struct DisplayMaybeParenthesizedType<'db> {
impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> { impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> {
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
let write_parentheses = |f: &mut TypeWriter<'_, '_, 'db>| { let write_parentheses = |f: &mut TypeWriter<'_, '_, 'db>| {
f.set_invalid_syntax();
f.write_char('(')?; f.write_char('(')?;
self.ty self.ty
.display_with(self.db, self.settings.clone()) .display_with(self.db, self.settings.clone())

View File

@ -2,7 +2,8 @@ use std::borrow::Cow;
use lsp_types::request::InlayHintRequest; use lsp_types::request::InlayHintRequest;
use lsp_types::{InlayHintParams, Url}; 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 ty_project::ProjectDatabase;
use crate::PositionEncoding; use crate::PositionEncoding;
@ -64,7 +65,14 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
padding_left: None, padding_left: None,
padding_right: None, padding_right: None,
data: 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(); .collect();
@ -100,3 +108,26 @@ fn inlay_hint_label(
} }
lsp_types::InlayHintLabel::LabelParts(label_parts) lsp_types::InlayHintLabel::LabelParts(label_parts)
} }
fn inlay_hint_text_edit(
inlay_hint_text_edit: InlayHintTextEdit,
db: &ProjectDatabase,
file: File,
encoding: PositionEncoding,
) -> Option<lsp_types::TextEdit> {
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,
})
}

View File

@ -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": { "position": {
@ -91,7 +106,8 @@ y = foo(1)
"value": "=" "value": "="
} }
], ],
"kind": 2 "kind": 2,
"textEdits": []
} }
] ]
"#); "#);