diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 0ede0fc2f7..5480d73b54 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; -use ty_python_semantic::SemanticModel; use ty_python_semantic::types::Type; +use ty_python_semantic::{DisplaySettings, SemanticModel}; pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option>> { let parsed = parsed_module(db, file).load(db); @@ -135,7 +135,10 @@ impl fmt::Display for DisplayHoverContent<'_, '_> { match self.content { HoverContent::Type(ty) => self .kind - .fenced_code_block(ty.display(self.db), "python") + .fenced_code_block( + ty.display_with(self.db, DisplaySettings::default().multiline()), + "python", + ) .fmt(f), HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f), } @@ -201,7 +204,10 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - def my_func(a, b) -> Unknown + def my_func( + a, + b + ) -> Unknown --------------------------------------------- This is such a great func!! @@ -211,7 +217,10 @@ mod tests { --------------------------------------------- ```python - def my_func(a, b) -> Unknown + def my_func( + a, + b + ) -> Unknown ``` --- ```text @@ -253,7 +262,10 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - def my_func(a, b) -> Unknown + def my_func( + a, + b + ) -> Unknown --------------------------------------------- This is such a great func!! @@ -263,7 +275,10 @@ mod tests { --------------------------------------------- ```python - def my_func(a, b) -> Unknown + def my_func( + a, + b + ) -> Unknown ``` --- ```text @@ -519,7 +534,10 @@ mod tests { ); 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!! @@ -529,7 +547,10 @@ mod tests { --------------------------------------------- ```python - bound method MyClass.my_method(a, b) -> Unknown + bound method MyClass.my_method( + a, + b + ) -> Unknown ``` --- ```text @@ -601,10 +622,16 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - def foo(a, b) -> Unknown + def foo( + a, + b + ) -> Unknown --------------------------------------------- ```python - def foo(a, b) -> Unknown + def foo( + a, + b + ) -> Unknown ``` --------------------------------------------- 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(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(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] fn hover_module() { let mut test = cursor_test( @@ -1231,6 +1380,110 @@ mod tests { 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: ... + + ab + "#, + ); + + 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]] = ... + + ab + "#, + ); + + 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 = ... + + ab + "#, + ); + + 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] fn hover_docstring() { let test = cursor_test( diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index ba4075ef2a..b3e4ef3c7e 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -19,6 +19,7 @@ pub use semantic_model::{ Completion, CompletionKind, HasDefinition, HasType, NameKind, SemanticModel, }; pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; +pub use types::DisplaySettings; pub use types::ide_support::{ ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_imported_symbol, definitions_for_name, map_stub_definition, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 30e01fdc97..b0d33ee097 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -41,6 +41,7 @@ use crate::types::class::{CodeGeneratorKind, Field}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; 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::function::{ DataclassTransformerParams, FunctionDecorators, FunctionSpans, FunctionType, KnownFunction, diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 3d23a8393d..dd8412f4cf 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -18,12 +18,50 @@ use crate::types::{ SubclassOfInner, Type, UnionType, WrapperDescriptorKind, }; +/// Settings for displaying types and signatures +#[derive(Debug, Copy, Clone, Default)] +pub struct DisplaySettings { + /// Whether rendering can be multiline + pub multiline: bool, +} + +impl DisplaySettings { + #[must_use] + pub fn multiline(self) -> Self { + Self { multiline: true } + } + + #[must_use] + pub fn singleline(self) -> Self { + Self { multiline: false } + } +} + impl<'db> Type<'db> { pub fn display(&self, db: &'db dyn Db) -> DisplayType<'_> { - DisplayType { ty: self, db } + DisplayType { + ty: self, + settings: DisplaySettings::default(), + db, + } } - fn representation(self, db: &'db dyn Db) -> DisplayRepresentation<'db> { - DisplayRepresentation { db, ty: self } + pub fn display_with(&self, db: &'db dyn Db, settings: DisplaySettings) -> DisplayType<'_> { + DisplayType { + ty: self, + db, + settings, + } + } + fn representation( + self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayRepresentation<'db> { + DisplayRepresentation { + db, + ty: self, + settings, + } } } @@ -31,11 +69,12 @@ impl<'db> Type<'db> { pub struct DisplayType<'db> { ty: &'db Type<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let representation = self.ty.representation(self.db); + let representation = self.ty.representation(self.db, self.settings); match self.ty { Type::ClassLiteral(literal) if literal.is_known(self.db, KnownClass::Any) => { write!(f, "typing.Any") @@ -64,6 +103,7 @@ impl fmt::Debug for DisplayType<'_> { struct DisplayRepresentation<'db> { ty: Type<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayRepresentation<'_> { @@ -81,17 +121,19 @@ impl Display for DisplayRepresentation<'_> { .specialization(self.db) .tuple(self.db) .expect("Specialization::tuple() should always return `Some()` for `KnownClass::Tuple`") - .display(self.db) + .display_with(self.db, self.settings) .fmt(f), (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), - (ClassType::Generic(alias), _) => alias.display(self.db).fmt(f), + (ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings).fmt(f), } } Type::ProtocolInstance(protocol) => match protocol.inner { Protocol::FromClass(ClassType::NonGeneric(class)) => { f.write_str(class.name(self.db)) } - Protocol::FromClass(ClassType::Generic(alias)) => alias.display(self.db).fmt(f), + Protocol::FromClass(ClassType::Generic(alias)) => { + alias.display_with(self.db, self.settings).fmt(f) + } Protocol::Synthesized(synthetic) => { f.write_str(" { Type::ClassLiteral(class) => { write!(f, "", class.name(self.db)) } - Type::GenericAlias(generic) => write!(f, "", generic.display(self.db)), + Type::GenericAlias(generic) => write!( + f, + "", + generic.display_with(self.db, self.settings.singleline()) + ), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(ClassType::NonGeneric(class)) => { write!(f, "type[{}]", class.name(self.db)) } SubclassOfInner::Class(ClassType::Generic(alias)) => { - write!(f, "type[{}]", alias.display(self.db)) + write!( + f, + "type[{}]", + alias.display_with(self.db, self.settings.singleline()) + ) } SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, Type::SpecialForm(special_form) => special_form.fmt(f), Type::KnownInstance(known_instance) => known_instance.repr(self.db).fmt(f), - Type::FunctionLiteral(function) => function.display(self.db).fmt(f), - Type::Callable(callable) => callable.display(self.db).fmt(f), + Type::FunctionLiteral(function) => function.display_with(self.db, self.settings).fmt(f), + Type::Callable(callable) => callable.display_with(self.db, self.settings).fmt(f), Type::BoundMethod(bound_method) => { let function = bound_method.function(self.db); let self_ty = bound_method.self_instance(self.db); @@ -138,31 +188,38 @@ impl Display for DisplayRepresentation<'_> { let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, + settings: self.settings, }; write!( f, "bound method {instance}.{method}{type_parameters}{signature}", method = function.name(self.db), - instance = self_ty.display(self.db), + instance = self_ty.display_with(self.db, self.settings.singleline()), type_parameters = type_parameters, signature = signature .bind_self(self.db, Some(typing_self_ty)) - .display(self.db) + .display_with(self.db, self.settings) ) } signatures => { // TODO: How to display overloads? - f.write_str("Overload[")?; - let mut join = f.join(", "); + if !self.settings.multiline { + f.write_str("Overload[")?; + } + let separator = if self.settings.multiline { "\n" } else { ", " }; + let mut join = f.join(separator); for signature in signatures { join.entry( &signature .bind_self(self.db, Some(typing_self_ty)) - .display(self.db), + .display_with(self.db, self.settings), ); } - f.write_str("]") + if !self.settings.multiline { + f.write_str("]")?; + } + Ok(()) } } } @@ -203,11 +260,13 @@ impl Display for DisplayRepresentation<'_> { Type::DataclassTransformer(_) => { f.write_str("") } - Type::Union(union) => union.display(self.db).fmt(f), - Type::Intersection(intersection) => intersection.display(self.db).fmt(f), + Type::Union(union) => union.display_with(self.db, self.settings).fmt(f), + Type::Intersection(intersection) => { + intersection.display_with(self.db, self.settings).fmt(f) + } Type::IntLiteral(n) => n.fmt(f), Type::BooleanLiteral(boolean) => f.write_str(if boolean { "True" } else { "False" }), - Type::StringLiteral(string) => string.display(self.db).fmt(f), + Type::StringLiteral(string) => string.display_with(self.db, self.settings).fmt(f), Type::LiteralString => f.write_str("LiteralString"), Type::BytesLiteral(bytes) => { let escape = AsciiEscape::with_preferred_quote(bytes.value(self.db), Quote::Double); @@ -236,13 +295,20 @@ impl Display for DisplayRepresentation<'_> { write!( f, "", - pivot = Type::from(bound_super.pivot_class(self.db)).display(self.db), - owner = bound_super.owner(self.db).into_type().display(self.db) + pivot = Type::from(bound_super.pivot_class(self.db)) + .display_with(self.db, self.settings.singleline()), + owner = bound_super + .owner(self.db) + .into_type() + .display_with(self.db, self.settings.singleline()) ) } Type::TypeIs(type_is) => { f.write_str("TypeIs[")?; - type_is.return_type(self.db).display(self.db).fmt(f)?; + type_is + .return_type(self.db) + .display_with(self.db, self.settings.singleline()) + .fmt(f)?; if let Some(name) = type_is.place_name(self.db) { f.write_str(" @ ")?; f.write_str(&name)?; @@ -256,14 +322,23 @@ impl Display for DisplayRepresentation<'_> { } impl<'db> TupleSpec<'db> { - pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayTuple<'db> { - DisplayTuple { tuple: self, db } + pub(crate) fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayTuple<'db> { + DisplayTuple { + tuple: self, + db, + settings, + } } } pub(crate) struct DisplayTuple<'db> { tuple: &'db TupleSpec<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayTuple<'_> { @@ -275,7 +350,9 @@ impl Display for DisplayTuple<'_> { if elements.is_empty() { f.write_str("()")?; } else { - elements.display(self.db).fmt(f)?; + elements + .display_with(self.db, self.settings.singleline()) + .fmt(f)?; } } @@ -295,20 +372,29 @@ impl Display for DisplayTuple<'_> { // trailing `]` are printed elsewhere. The `yyy, ...` is printed no matter what.) TupleSpec::Variable(tuple) => { if !tuple.prefix.is_empty() { - tuple.prefix.display(self.db).fmt(f)?; + tuple + .prefix + .display_with(self.db, self.settings.singleline()) + .fmt(f)?; f.write_str(", ")?; } if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() { f.write_str("*tuple[")?; } - tuple.variable.display(self.db).fmt(f)?; + tuple + .variable + .display_with(self.db, self.settings.singleline()) + .fmt(f)?; f.write_str(", ...")?; if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() { f.write_str("]")?; } if !tuple.suffix.is_empty() { f.write_str(", ")?; - tuple.suffix.display(self.db).fmt(f)?; + tuple + .suffix + .display_with(self.db, self.settings.singleline()) + .fmt(f)?; } } } @@ -320,13 +406,26 @@ impl<'db> OverloadLiteral<'db> { // Not currently used, but useful for debugging. #[expect(dead_code)] pub(crate) fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> { - DisplayOverloadLiteral { literal: self, db } + Self::display_with(self, db, DisplaySettings::default()) + } + + pub(crate) fn display_with( + self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayOverloadLiteral<'db> { + DisplayOverloadLiteral { + literal: self, + db, + settings, + } } } pub(crate) struct DisplayOverloadLiteral<'db> { literal: OverloadLiteral<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayOverloadLiteral<'_> { @@ -335,6 +434,7 @@ impl Display for DisplayOverloadLiteral<'_> { let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, + settings: self.settings, }; write!( @@ -342,20 +442,29 @@ impl Display for DisplayOverloadLiteral<'_> { "def {name}{type_parameters}{signature}", name = self.literal.name(self.db), type_parameters = type_parameters, - signature = signature.display(self.db) + signature = signature.display_with(self.db, self.settings) ) } } impl<'db> FunctionType<'db> { - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayFunctionType<'db> { - DisplayFunctionType { ty: self, db } + pub(crate) fn display_with( + self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayFunctionType<'db> { + DisplayFunctionType { + ty: self, + db, + settings, + } } } pub(crate) struct DisplayFunctionType<'db> { ty: FunctionType<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayFunctionType<'_> { @@ -367,6 +476,7 @@ impl Display for DisplayFunctionType<'_> { let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, + settings: self.settings, }; write!( @@ -374,28 +484,39 @@ impl Display for DisplayFunctionType<'_> { "def {name}{type_parameters}{signature}", name = self.ty.name(self.db), type_parameters = type_parameters, - signature = signature.display(self.db) + signature = signature.display_with(self.db, self.settings) ) } signatures => { // TODO: How to display overloads? - f.write_str("Overload[")?; - let mut join = f.join(", "); - for signature in signatures { - join.entry(&signature.display(self.db)); + if !self.settings.multiline { + f.write_str("Overload[")?; } - f.write_str("]") + let separator = if self.settings.multiline { "\n" } else { ", " }; + let mut join = f.join(separator); + for signature in signatures { + join.entry(&signature.display_with(self.db, self.settings)); + } + if !self.settings.multiline { + f.write_str("]")?; + } + Ok(()) } } } } impl<'db> GenericAlias<'db> { - pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { + pub(crate) fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayGenericAlias<'db> { DisplayGenericAlias { origin: self.origin(db), specialization: self.specialization(db), db, + settings, } } } @@ -404,12 +525,13 @@ pub(crate) struct DisplayGenericAlias<'db> { origin: ClassLiteral<'db>, specialization: Specialization<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayGenericAlias<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(tuple) = self.specialization.tuple(self.db) { - tuple.display(self.db).fmt(f) + tuple.display_with(self.db, self.settings).fmt(f) } else { write!( f, @@ -426,9 +548,17 @@ impl Display for DisplayGenericAlias<'_> { impl<'db> GenericContext<'db> { pub fn display(&'db self, db: &'db dyn Db) -> DisplayGenericContext<'db> { + Self::display_with(self, db, DisplaySettings::default()) + } + pub fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayGenericContext<'db> { DisplayGenericContext { generic_context: self, db, + settings, } } } @@ -436,6 +566,7 @@ impl<'db> GenericContext<'db> { struct DisplayOptionalGenericContext<'db> { generic_context: Option<&'db GenericContext<'db>>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayOptionalGenericContext<'_> { @@ -444,6 +575,7 @@ impl Display for DisplayOptionalGenericContext<'_> { DisplayGenericContext { generic_context, db: self.db, + settings: self.settings, } .fmt(f) } else { @@ -455,6 +587,8 @@ impl Display for DisplayOptionalGenericContext<'_> { pub struct DisplayGenericContext<'db> { generic_context: &'db GenericContext<'db>, db: &'db dyn Db, + #[expect(dead_code)] + settings: DisplaySettings, } impl Display for DisplayGenericContext<'_> { @@ -492,6 +626,7 @@ impl<'db> Specialization<'db> { types: self.types(db), db, tuple_specialization, + settings: DisplaySettings::default(), } } } @@ -500,6 +635,7 @@ pub struct DisplaySpecialization<'db> { types: &'db [Type<'db>], db: &'db dyn Db, tuple_specialization: TupleSpecialization, + settings: DisplaySettings, } impl Display for DisplaySpecialization<'_> { @@ -509,7 +645,7 @@ impl Display for DisplaySpecialization<'_> { if idx > 0 { f.write_str(", ")?; } - ty.display(self.db).fmt(f)?; + ty.display_with(self.db, self.settings).fmt(f)?; } if self.tuple_specialization.is_yes() { f.write_str(", ...")?; @@ -540,9 +676,18 @@ impl TupleSpecialization { impl<'db> CallableType<'db> { pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayCallableType<'db> { + Self::display_with(self, db, DisplaySettings::default()) + } + + pub(crate) fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayCallableType<'db> { DisplayCallableType { signatures: self.signatures(db), db, + settings, } } } @@ -550,21 +695,28 @@ impl<'db> CallableType<'db> { pub(crate) struct DisplayCallableType<'db> { signatures: &'db CallableSignature<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayCallableType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self.signatures.overloads.as_slice() { - [signature] => signature.display(self.db).fmt(f), + [signature] => signature.display_with(self.db, self.settings).fmt(f), signatures => { // TODO: How to display overloads? - f.write_str("Overload[")?; - let mut join = f.join(", "); + if !self.settings.multiline { + f.write_str("Overload[")?; + } + let separator = if self.settings.multiline { "\n" } else { ", " }; + let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display(self.db)); + join.entry(&signature.display_with(self.db, self.settings)); } join.finish()?; - f.write_char(']') + if !self.settings.multiline { + f.write_char(']')?; + } + Ok(()) } } } @@ -572,10 +724,19 @@ impl Display for DisplayCallableType<'_> { impl<'db> Signature<'db> { pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> { + Self::display_with(self, db, DisplaySettings::default()) + } + + pub(crate) fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplaySignature<'db> { DisplaySignature { parameters: self.parameters(), return_ty: self.return_ty, db, + settings, } } } @@ -584,6 +745,7 @@ pub(crate) struct DisplaySignature<'db> { parameters: &'db Parameters<'db>, return_ty: Option>, db: &'db dyn Db, + settings: DisplaySettings, } impl DisplaySignature<'_> { @@ -600,9 +762,12 @@ impl DisplaySignature<'_> { /// Internal method to write signature with the signature writer fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result { + let multiline = self.settings.multiline && self.parameters.len() > 1; // Opening parenthesis writer.write_char('(')?; - + if multiline { + writer.write_str("\n ")?; + } if self.parameters.is_gradual() { // We represent gradual form as `...` in the signature, internally the parameters still // contain `(*args, **kwargs)` parameters. @@ -611,12 +776,13 @@ impl DisplaySignature<'_> { let mut star_added = false; let mut needs_slash = false; let mut first = true; + let arg_separator = if multiline { ",\n " } else { ", " }; for parameter in self.parameters.as_slice() { // Handle special separators if !star_added && parameter.is_keyword_only() { if !first { - writer.write_str(", ")?; + writer.write_str(arg_separator)?; } writer.write_char('*')?; star_added = true; @@ -626,7 +792,7 @@ impl DisplaySignature<'_> { needs_slash = true; } else if needs_slash { if !first { - writer.write_str(", ")?; + writer.write_str(arg_separator)?; } writer.write_char('/')?; needs_slash = false; @@ -635,30 +801,36 @@ impl DisplaySignature<'_> { // Add comma before parameter if not first if !first { - writer.write_str(", ")?; + writer.write_str(arg_separator)?; } // Write parameter with range tracking let param_name = parameter.display_name(); - writer.write_parameter(¶meter.display(self.db), param_name.as_deref())?; + writer.write_parameter( + ¶meter.display_with(self.db, self.settings.singleline()), + param_name.as_deref(), + )?; first = false; } if needs_slash { if !first { - writer.write_str(", ")?; + writer.write_str(arg_separator)?; } writer.write_char('/')?; } } + if multiline { + writer.write_char('\n')?; + } // Closing parenthesis writer.write_char(')')?; // Return type let return_ty = self.return_ty.unwrap_or_else(Type::unknown); - writer.write_return_type(&return_ty.display(self.db))?; + writer.write_return_type(&return_ty.display_with(self.db, self.settings.singleline()))?; Ok(()) } @@ -774,14 +946,23 @@ pub(crate) struct SignatureDisplayDetails { } impl<'db> Parameter<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayParameter<'db> { - DisplayParameter { param: self, db } + fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayParameter<'db> { + DisplayParameter { + param: self, + db, + settings, + } } } struct DisplayParameter<'db> { param: &'db Parameter<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayParameter<'_> { @@ -789,34 +970,47 @@ impl Display for DisplayParameter<'_> { if let Some(name) = self.param.display_name() { f.write_str(&name)?; if let Some(annotated_type) = self.param.annotated_type() { - write!(f, ": {}", annotated_type.display(self.db))?; + write!( + f, + ": {}", + annotated_type.display_with(self.db, self.settings) + )?; } // Default value can only be specified if `name` is given. if let Some(default_ty) = self.param.default_type() { if self.param.annotated_type().is_some() { - write!(f, " = {}", default_ty.display(self.db))?; + write!(f, " = {}", default_ty.display_with(self.db, self.settings))?; } else { - write!(f, "={}", default_ty.display(self.db))?; + write!(f, "={}", default_ty.display_with(self.db, self.settings))?; } } } else if let Some(ty) = self.param.annotated_type() { // This case is specifically for the `Callable` signature where name and default value // cannot be provided. - ty.display(self.db).fmt(f)?; + ty.display_with(self.db, self.settings).fmt(f)?; } Ok(()) } } impl<'db> UnionType<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> { - DisplayUnionType { db, ty: self } + fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayUnionType<'db> { + DisplayUnionType { + db, + ty: self, + settings, + } } } struct DisplayUnionType<'db> { ty: &'db UnionType<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayUnionType<'_> { @@ -849,12 +1043,14 @@ impl Display for DisplayUnionType<'_> { join.entry(&DisplayLiteralGroup { literals: condensed_types, db: self.db, + settings: self.settings.singleline(), }); } } else { join.entry(&DisplayMaybeParenthesizedType { ty: *element, db: self.db, + settings: self.settings.singleline(), }); } } @@ -874,27 +1070,41 @@ impl fmt::Debug for DisplayUnionType<'_> { struct DisplayLiteralGroup<'db> { literals: Vec>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayLiteralGroup<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("Literal[")?; f.join(", ") - .entries(self.literals.iter().map(|ty| ty.representation(self.db))) + .entries( + self.literals + .iter() + .map(|ty| ty.representation(self.db, self.settings.singleline())), + ) .finish()?; f.write_str("]") } } impl<'db> IntersectionType<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayIntersectionType<'db> { - DisplayIntersectionType { db, ty: self } + fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayIntersectionType<'db> { + DisplayIntersectionType { + db, + ty: self, + settings, + } } } struct DisplayIntersectionType<'db> { ty: &'db IntersectionType<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayIntersectionType<'_> { @@ -906,6 +1116,7 @@ impl Display for DisplayIntersectionType<'_> { .map(|&ty| DisplayMaybeNegatedType { ty, db: self.db, + settings: self.settings.singleline(), negated: false, }) .chain( @@ -915,6 +1126,7 @@ impl Display for DisplayIntersectionType<'_> { .map(|&ty| DisplayMaybeNegatedType { ty, db: self.db, + settings: self.settings.singleline(), negated: true, }), ); @@ -932,6 +1144,7 @@ struct DisplayMaybeNegatedType<'db> { ty: Type<'db>, db: &'db dyn Db, negated: bool, + settings: DisplaySettings, } impl Display for DisplayMaybeNegatedType<'_> { @@ -942,6 +1155,7 @@ impl Display for DisplayMaybeNegatedType<'_> { DisplayMaybeParenthesizedType { ty: self.ty, db: self.db, + settings: self.settings, } .fmt(f) } @@ -950,11 +1164,13 @@ impl Display for DisplayMaybeNegatedType<'_> { struct DisplayMaybeParenthesizedType<'db> { ty: Type<'db>, db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayMaybeParenthesizedType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let write_parentheses = |f: &mut Formatter<'_>| write!(f, "({})", self.ty.display(self.db)); + let write_parentheses = + |f: &mut Formatter<'_>| write!(f, "({})", self.ty.display_with(self.db, self.settings)); match self.ty { Type::Callable(_) | Type::MethodWrapper(_) @@ -964,58 +1180,94 @@ impl Display for DisplayMaybeParenthesizedType<'_> { Type::Intersection(intersection) if !intersection.has_one_element(self.db) => { write_parentheses(f) } - _ => self.ty.display(self.db).fmt(f), + _ => self.ty.display_with(self.db, self.settings).fmt(f), } } } pub(crate) trait TypeArrayDisplay<'db> { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray<'_, 'db>; + fn display_with(&self, db: &'db dyn Db, settings: DisplaySettings) + -> DisplayTypeArray<'_, 'db>; } impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray<'_, 'db> { - DisplayTypeArray { types: self, db } + fn display_with( + &self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayTypeArray<'_, 'db> { + DisplayTypeArray { + types: self, + db, + settings, + } } } impl<'db> TypeArrayDisplay<'db> for Vec> { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray<'_, 'db> { - DisplayTypeArray { types: self, db } + fn display_with( + &self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayTypeArray<'_, 'db> { + DisplayTypeArray { + types: self, + db, + settings, + } } } impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray<'_, 'db> { - DisplayTypeArray { types: self, db } + fn display_with( + &self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayTypeArray<'_, 'db> { + DisplayTypeArray { + types: self, + db, + settings, + } } } pub(crate) struct DisplayTypeArray<'b, 'db> { types: &'b [Type<'db>], db: &'db dyn Db, + settings: DisplaySettings, } impl Display for DisplayTypeArray<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.join(", ") - .entries(self.types.iter().map(|ty| ty.display(self.db))) + .entries( + self.types + .iter() + .map(|ty| ty.display_with(self.db, self.settings.singleline())), + ) .finish() } } impl<'db> StringLiteralType<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> { - display_quoted_string(self.value(db)) + fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings, + ) -> DisplayStringLiteralType<'db> { + display_quoted_string(self.value(db), settings) } } -fn display_quoted_string(string: &str) -> DisplayStringLiteralType<'_> { - DisplayStringLiteralType { string } +fn display_quoted_string(string: &str, settings: DisplaySettings) -> DisplayStringLiteralType<'_> { + DisplayStringLiteralType { string, settings } } struct DisplayStringLiteralType<'db> { string: &'db str, + #[expect(dead_code)] + settings: DisplaySettings, } impl Display for DisplayStringLiteralType<'_> { @@ -1035,6 +1287,7 @@ impl Display for DisplayStringLiteralType<'_> { #[cfg(test)] mod tests { + use insta::assert_snapshot; use ruff_python_ast::name::Name; use crate::Db; @@ -1095,31 +1348,41 @@ mod tests { .to_string() } + fn display_signature_multiline<'db>( + db: &dyn Db, + parameters: impl IntoIterator>, + return_ty: Option>, + ) -> String { + Signature::new(Parameters::new(parameters), return_ty) + .display_with(db, super::DisplaySettings::default().multiline()) + .to_string() + } + #[test] fn signature_display() { let db = setup_db(); // Empty parameters with no return type. - assert_eq!(display_signature(&db, [], None), "() -> Unknown"); + assert_snapshot!(display_signature(&db, [], None), @"() -> Unknown"); // Empty parameters with a return type. - assert_eq!( + assert_snapshot!( display_signature(&db, [], Some(Type::none(&db))), - "() -> None" + @"() -> None" ); // Single parameter type (no name) with a return type. - assert_eq!( + assert_snapshot!( display_signature( &db, [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], Some(Type::none(&db)) ), - "(None, /) -> None" + @"(None, /) -> None" ); // Two parameters where one has annotation and the other doesn't. - assert_eq!( + assert_snapshot!( display_signature( &db, [ @@ -1131,11 +1394,11 @@ mod tests { ], Some(Type::none(&db)) ), - "(x=int, y: str = str) -> None" + @"(x=int, y: str = str) -> None" ); // All positional only parameters. - assert_eq!( + assert_snapshot!( display_signature( &db, [ @@ -1144,11 +1407,11 @@ mod tests { ], Some(Type::none(&db)) ), - "(x, y, /) -> None" + @"(x, y, /) -> None" ); // Positional-only parameters mixed with non-positional-only parameters. - assert_eq!( + assert_snapshot!( display_signature( &db, [ @@ -1157,11 +1420,11 @@ mod tests { ], Some(Type::none(&db)) ), - "(x, /, y) -> None" + @"(x, /, y) -> None" ); // All keyword-only parameters. - assert_eq!( + assert_snapshot!( display_signature( &db, [ @@ -1170,11 +1433,11 @@ mod tests { ], Some(Type::none(&db)) ), - "(*, x, y) -> None" + @"(*, x, y) -> None" ); // Keyword-only parameters mixed with non-keyword-only parameters. - assert_eq!( + assert_snapshot!( display_signature( &db, [ @@ -1183,11 +1446,11 @@ mod tests { ], Some(Type::none(&db)) ), - "(x, *, y) -> None" + @"(x, *, y) -> None" ); // A mix of all parameter kinds. - assert_eq!( + assert_snapshot!( display_signature( &db, [ @@ -1216,9 +1479,178 @@ mod tests { ], Some(KnownClass::Bytes.to_instance(&db)) ), - "(a, b: int, c=Literal[1], d: int = Literal[2], \ + @"(a, b: int, c=Literal[1], d: int = Literal[2], \ /, e=Literal[3], f: int = Literal[4], *args: object, \ *, g=Literal[5], h: int = Literal[6], **kwargs: str) -> bytes" ); } + + #[test] + fn signature_display_multiline() { + let db = setup_db(); + + // Empty parameters with no return type. + assert_snapshot!(display_signature_multiline(&db, [], None), @"() -> Unknown"); + + // Empty parameters with a return type. + assert_snapshot!( + display_signature_multiline(&db, [], Some(Type::none(&db))), + @"() -> None" + ); + + // Single parameter type (no name) with a return type. + assert_snapshot!( + display_signature_multiline( + &db, + [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], + Some(Type::none(&db)) + ), + @"(None, /) -> None" + ); + + // Two parameters where one has annotation and the other doesn't. + assert_snapshot!( + display_signature_multiline( + &db, + [ + Parameter::positional_or_keyword(Name::new_static("x")) + .with_default_type(KnownClass::Int.to_instance(&db)), + Parameter::positional_or_keyword(Name::new_static("y")) + .with_annotated_type(KnownClass::Str.to_instance(&db)) + .with_default_type(KnownClass::Str.to_instance(&db)), + ], + Some(Type::none(&db)) + ), + @r" + ( + x=int, + y: str = str + ) -> None + " + ); + + // All positional only parameters. + assert_snapshot!( + display_signature_multiline( + &db, + [ + Parameter::positional_only(Some(Name::new_static("x"))), + Parameter::positional_only(Some(Name::new_static("y"))), + ], + Some(Type::none(&db)) + ), + @r" + ( + x, + y, + / + ) -> None + " + ); + + // Positional-only parameters mixed with non-positional-only parameters. + assert_snapshot!( + display_signature_multiline( + &db, + [ + Parameter::positional_only(Some(Name::new_static("x"))), + Parameter::positional_or_keyword(Name::new_static("y")), + ], + Some(Type::none(&db)) + ), + @r" + ( + x, + /, + y + ) -> None + " + ); + + // All keyword-only parameters. + assert_snapshot!( + display_signature_multiline( + &db, + [ + Parameter::keyword_only(Name::new_static("x")), + Parameter::keyword_only(Name::new_static("y")), + ], + Some(Type::none(&db)) + ), + @r" + ( + *, + x, + y + ) -> None + " + ); + + // Keyword-only parameters mixed with non-keyword-only parameters. + assert_snapshot!( + display_signature_multiline( + &db, + [ + Parameter::positional_or_keyword(Name::new_static("x")), + Parameter::keyword_only(Name::new_static("y")), + ], + Some(Type::none(&db)) + ), + @r" + ( + x, + *, + y + ) -> None + " + ); + + // A mix of all parameter kinds. + assert_snapshot!( + display_signature_multiline( + &db, + [ + Parameter::positional_only(Some(Name::new_static("a"))), + Parameter::positional_only(Some(Name::new_static("b"))) + .with_annotated_type(KnownClass::Int.to_instance(&db)), + Parameter::positional_only(Some(Name::new_static("c"))) + .with_default_type(Type::IntLiteral(1)), + Parameter::positional_only(Some(Name::new_static("d"))) + .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_default_type(Type::IntLiteral(2)), + Parameter::positional_or_keyword(Name::new_static("e")) + .with_default_type(Type::IntLiteral(3)), + Parameter::positional_or_keyword(Name::new_static("f")) + .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_default_type(Type::IntLiteral(4)), + Parameter::variadic(Name::new_static("args")) + .with_annotated_type(Type::object(&db)), + Parameter::keyword_only(Name::new_static("g")) + .with_default_type(Type::IntLiteral(5)), + Parameter::keyword_only(Name::new_static("h")) + .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_default_type(Type::IntLiteral(6)), + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(KnownClass::Str.to_instance(&db)), + ], + Some(KnownClass::Bytes.to_instance(&db)) + ), + @r" + ( + a, + b: int, + c=Literal[1], + d: int = Literal[2], + /, + e=Literal[3], + f: int = Literal[4], + *args: object, + *, + g=Literal[5], + h: int = Literal[6], + **kwargs: str + ) -> bytes + " + ); + } }