[ty] introduce multiline pretty printer (#19979)

Requires some iteration, but this includes the most tedious part --
threading a new concept of DisplaySettings through every type display
impl. Currently it only holds a boolean for multiline, but in the future
it could also take other things like "render to markdown" or "here's
your base indent if you make a newline".

For types which have exposed display functions I've left the old
signature as a compatibility polyfill to avoid having to audit
everywhere that prints types right off the bat (notably I originally
tried doing multiline functions unconditionally and a ton of things
churned that clearly weren't ready for multi-line (diagnostics).

The only real use of this API in this PR is to multiline render function
types in hovers, which is the highest impact (see snapshot changes).

Fixes https://github.com/astral-sh/ty/issues/1000
This commit is contained in:
Aria Desires 2025-08-19 13:31:44 -04:00 committed by GitHub
parent 59b078b1bf
commit c6dcfe36d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 793 additions and 106 deletions

View file

@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};
use std::fmt; use std::fmt;
use std::fmt::Formatter; use std::fmt::Formatter;
use ty_python_semantic::SemanticModel;
use ty_python_semantic::types::Type; use ty_python_semantic::types::Type;
use ty_python_semantic::{DisplaySettings, SemanticModel};
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> { pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
let parsed = parsed_module(db, file).load(db); let parsed = parsed_module(db, file).load(db);
@ -135,7 +135,10 @@ impl fmt::Display for DisplayHoverContent<'_, '_> {
match self.content { match self.content {
HoverContent::Type(ty) => self HoverContent::Type(ty) => self
.kind .kind
.fenced_code_block(ty.display(self.db), "python") .fenced_code_block(
ty.display_with(self.db, DisplaySettings::default().multiline()),
"python",
)
.fmt(f), .fmt(f),
HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f), HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f),
} }
@ -201,7 +204,10 @@ mod tests {
); );
assert_snapshot!(test.hover(), @r" assert_snapshot!(test.hover(), @r"
def my_func(a, b) -> Unknown def my_func(
a,
b
) -> Unknown
--------------------------------------------- ---------------------------------------------
This is such a great func!! This is such a great func!!
@ -211,7 +217,10 @@ mod tests {
--------------------------------------------- ---------------------------------------------
```python ```python
def my_func(a, b) -> Unknown def my_func(
a,
b
) -> Unknown
``` ```
--- ---
```text ```text
@ -253,7 +262,10 @@ mod tests {
); );
assert_snapshot!(test.hover(), @r" assert_snapshot!(test.hover(), @r"
def my_func(a, b) -> Unknown def my_func(
a,
b
) -> Unknown
--------------------------------------------- ---------------------------------------------
This is such a great func!! This is such a great func!!
@ -263,7 +275,10 @@ mod tests {
--------------------------------------------- ---------------------------------------------
```python ```python
def my_func(a, b) -> Unknown def my_func(
a,
b
) -> Unknown
``` ```
--- ---
```text ```text
@ -519,7 +534,10 @@ mod tests {
); );
assert_snapshot!(test.hover(), @r" assert_snapshot!(test.hover(), @r"
bound method MyClass.my_method(a, b) -> Unknown bound method MyClass.my_method(
a,
b
) -> Unknown
--------------------------------------------- ---------------------------------------------
This is such a great func!! This is such a great func!!
@ -529,7 +547,10 @@ mod tests {
--------------------------------------------- ---------------------------------------------
```python ```python
bound method MyClass.my_method(a, b) -> Unknown bound method MyClass.my_method(
a,
b
) -> Unknown
``` ```
--- ---
```text ```text
@ -601,10 +622,16 @@ mod tests {
); );
assert_snapshot!(test.hover(), @r" assert_snapshot!(test.hover(), @r"
def foo(a, b) -> Unknown def foo(
a,
b
) -> Unknown
--------------------------------------------- ---------------------------------------------
```python ```python
def foo(a, b) -> Unknown def foo(
a,
b
) -> Unknown
``` ```
--------------------------------------------- ---------------------------------------------
info[hover]: Hovered content is info[hover]: Hovered content is
@ -763,6 +790,128 @@ mod tests {
"); ");
} }
#[test]
fn hover_overload() {
let test = cursor_test(
r#"
from typing import overload
@overload
def foo(a: int, b):
"""The first overload"""
return 0
@overload
def foo(a: str, b):
"""The second overload"""
return 1
if random.choice([True, False]):
a = 1
else:
a = "hello"
foo<CURSOR>(a, 2)
"#,
);
assert_snapshot!(test.hover(), @r#"
(
a: int,
b
) -> Unknown
(
a: str,
b
) -> Unknown
---------------------------------------------
The first overload
---------------------------------------------
```python
(
a: int,
b
) -> Unknown
(
a: str,
b
) -> Unknown
```
---
```text
The first overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:19:13
|
17 | a = "hello"
18 |
19 | foo(a, 2)
| ^^^- Cursor offset
| |
| source
|
"#);
}
#[test]
fn hover_overload_compact() {
let test = cursor_test(
r#"
from typing import overload
@overload
def foo(a: int):
"""The first overload"""
return 0
@overload
def foo(a: str):
"""The second overload"""
return 1
if random.choice([True, False]):
a = 1
else:
a = "hello"
foo<CURSOR>(a)
"#,
);
assert_snapshot!(test.hover(), @r#"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
The first overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
```text
The first overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:19:13
|
17 | a = "hello"
18 |
19 | foo(a)
| ^^^- Cursor offset
| |
| source
|
"#);
}
#[test] #[test]
fn hover_module() { fn hover_module() {
let mut test = cursor_test( let mut test = cursor_test(
@ -1231,6 +1380,110 @@ mod tests {
assert_snapshot!(test.hover(), @"Hover provided no content"); assert_snapshot!(test.hover(), @"Hover provided no content");
} }
#[test]
fn hover_complex_type1() {
let test = cursor_test(
r#"
from typing import Callable, Any, List
def ab(x: int, y: Callable[[int, int], Any], z: List[int]) -> int: ...
a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r"
def ab(
x: int,
y: (int, int, /) -> Any,
z: list[int]
) -> int
---------------------------------------------
```python
def ab(
x: int,
y: (int, int, /) -> Any,
z: list[int]
) -> int
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:9
|
3 | def ab(x: int, y: Callable[[int, int], Any], z: List[int]) -> int: ...
4 |
5 | ab
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_complex_type2() {
let test = cursor_test(
r#"
from typing import Callable, Tuple, Any
ab: Tuple[Any, int, Callable[[int, int], Any]] = ...
a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r"
tuple[Any, int, (int, int, /) -> Any]
---------------------------------------------
```python
tuple[Any, int, (int, int, /) -> Any]
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:9
|
3 | ab: Tuple[Any, int, Callable[[int, int], Any]] = ...
4 |
5 | ab
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_complex_type3() {
let test = cursor_test(
r#"
from typing import Callable, Any
ab: Callable[[int, int], Any] | None = ...
a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r"
((int, int, /) -> Any) | None
---------------------------------------------
```python
((int, int, /) -> Any) | None
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:9
|
3 | ab: Callable[[int, int], Any] | None = ...
4 |
5 | ab
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test] #[test]
fn hover_docstring() { fn hover_docstring() {
let test = cursor_test( let test = cursor_test(

View file

@ -19,6 +19,7 @@ pub use semantic_model::{
Completion, CompletionKind, HasDefinition, HasType, NameKind, SemanticModel, Completion, CompletionKind, HasDefinition, HasType, NameKind, SemanticModel,
}; };
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use types::DisplaySettings;
pub use types::ide_support::{ pub use types::ide_support::{
ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, ImportAliasResolution, ResolvedDefinition, definitions_for_attribute,
definitions_for_imported_symbol, definitions_for_name, map_stub_definition, definitions_for_imported_symbol, definitions_for_name, map_stub_definition,

View file

@ -41,6 +41,7 @@ use crate::types::class::{CodeGeneratorKind, Field};
pub(crate) use crate::types::class_base::ClassBase; pub(crate) use crate::types::class_base::ClassBase;
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
pub use crate::types::display::DisplaySettings;
use crate::types::enums::{enum_metadata, is_single_member_enum}; use crate::types::enums::{enum_metadata, is_single_member_enum};
use crate::types::function::{ use crate::types::function::{
DataclassTransformerParams, FunctionDecorators, FunctionSpans, FunctionType, KnownFunction, DataclassTransformerParams, FunctionDecorators, FunctionSpans, FunctionType, KnownFunction,

File diff suppressed because it is too large Load diff