diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 196e11da96..8ac83bf965 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -1,34 +1,16 @@ +pub use crate::goto_declaration::goto_declaration; +pub use crate::goto_definition::goto_definition; +pub use crate::goto_type_definition::goto_type_definition; + use crate::find_node::covering_node; -use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; -use ruff_db::files::{File, FileRange}; -use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use crate::stub_mapping::StubMapper; +use ruff_db::parsed::ParsedModuleRef; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::types::Type; use ty_python_semantic::{HasType, SemanticModel}; -pub fn goto_type_definition( - db: &dyn Db, - file: File, - offset: TextSize, -) -> Option> { - let module = parsed_module(db, file).load(db); - let goto_target = find_goto_target(&module, offset)?; - - let model = SemanticModel::new(db, file); - let ty = goto_target.inferred_type(&model)?; - - tracing::debug!("Inferred type of covering node is {}", ty.display(db)); - - let navigation_targets = ty.navigation_targets(db); - - Some(RangedValue { - range: FileRange::new(file, goto_target.range()), - value: navigation_targets, - }) -} - #[derive(Clone, Copy, Debug)] pub(crate) enum GotoTarget<'a> { Expression(ast::ExprRef<'a>), @@ -154,6 +136,100 @@ impl GotoTarget<'_> { Some(ty) } + + /// Gets the navigation ranges for this goto target. + /// If a stub mapper is provided, definitions from stub files will be mapped to + /// their corresponding source file implementations. + pub(crate) fn get_definition_targets( + self, + file: ruff_db::files::File, + db: &dyn crate::Db, + stub_mapper: Option<&StubMapper>, + ) -> Option { + use crate::NavigationTarget; + use ruff_python_ast as ast; + + match self { + // For names, find the definitions of the symbol + GotoTarget::Expression(expression) => { + if let ast::ExprRef::Name(name) = expression { + Self::get_name_definition_targets(name, file, db, stub_mapper) + } else { + // For other expressions, we can't find definitions + None + } + } + + // For already-defined symbols, they are their own definitions + GotoTarget::FunctionDef(function) => { + let range = function.name.range; + Some(crate::NavigationTargets::single(NavigationTarget { + file, + focus_range: range, + full_range: function.range(), + })) + } + + GotoTarget::ClassDef(class) => { + let range = class.name.range; + Some(crate::NavigationTargets::single(NavigationTarget { + file, + focus_range: range, + full_range: class.range(), + })) + } + + GotoTarget::Parameter(parameter) => { + let range = parameter.name.range; + Some(crate::NavigationTargets::single(NavigationTarget { + file, + focus_range: range, + full_range: parameter.range(), + })) + } + + // For imports, find the symbol being imported + GotoTarget::Alias(_alias) => { + // For aliases, we don't have the ExprName node, so we can't get the scope + // For now, return None. In the future, we could look up the imported symbol + None + } + + // TODO: Handle attribute and method accesses (y in `x.y` expressions) + // TODO: Handle keyword arguments in call expression + // TODO: Handle multi-part module names in import statements + // TODO: Handle imported symbol in y in `from x import y as z` statement + // TODO: Handle string literals that map to TypedDict fields + _ => None, + } + } + + /// Get navigation targets for definitions associated with a name expression + fn get_name_definition_targets( + name: &ruff_python_ast::ExprName, + file: ruff_db::files::File, + db: &dyn crate::Db, + stub_mapper: Option<&StubMapper>, + ) -> Option { + use ty_python_semantic::definitions_for_name; + + // Get all definitions for this name + let mut definitions = definitions_for_name(db, file, name); + + // Apply stub mapping if a mapper is provided + if let Some(mapper) = stub_mapper { + definitions = mapper.map_definitions(definitions); + } + + if definitions.is_empty() { + return None; + } + + // Convert definitions to navigation targets + let targets = convert_resolved_definitions_to_targets(db, definitions); + + Some(crate::NavigationTargets::unique(targets)) + } } impl Ranged for GotoTarget<'_> { @@ -180,6 +256,41 @@ impl Ranged for GotoTarget<'_> { } } +/// Converts a collection of `ResolvedDefinition` items into `NavigationTarget` items. +fn convert_resolved_definitions_to_targets( + db: &dyn crate::Db, + definitions: Vec>, +) -> Vec { + definitions + .into_iter() + .map(|resolved_definition| match resolved_definition { + ty_python_semantic::ResolvedDefinition::Definition(definition) => { + // Get the parsed module for range calculation + let definition_file = definition.file(db); + let module = ruff_db::parsed::parsed_module(db, definition_file).load(db); + + // Get the ranges for this definition + let focus_range = definition.focus_range(db, &module); + let full_range = definition.full_range(db, &module); + + crate::NavigationTarget { + file: focus_range.file(), + focus_range: focus_range.range(), + full_range: full_range.range(), + } + } + ty_python_semantic::ResolvedDefinition::ModuleFile(module_file) => { + // For module files, navigate to the beginning of the file + crate::NavigationTarget { + file: module_file, + focus_range: ruff_text_size::TextRange::default(), // Start of file + full_range: ruff_text_size::TextRange::default(), // Start of file + } + } + }) + .collect() +} + pub(crate) fn find_goto_target( parsed: &ParsedModuleRef, offset: TextSize, @@ -250,637 +361,3 @@ pub(crate) fn find_goto_target( node => node.as_expr_ref().map(GotoTarget::Expression), } } - -#[cfg(test)] -mod tests { - use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; - use crate::{NavigationTarget, goto_type_definition}; - use insta::assert_snapshot; - use ruff_db::diagnostic::{ - Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, - }; - use ruff_db::files::FileRange; - use ruff_text_size::Ranged; - - #[test] - fn goto_type_of_expression_with_class_type() { - let test = cursor_test( - r#" - class Test: ... - - ab = Test() - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:19 - | - 2 | class Test: ... - | ^^^^ - 3 | - 4 | ab = Test() - | - info: Source - --> main.py:4:13 - | - 2 | class Test: ... - 3 | - 4 | ab = Test() - | ^^ - | - "); - } - - #[test] - fn goto_type_of_expression_with_function_type() { - let test = cursor_test( - r#" - def foo(a, b): ... - - ab = foo - - ab - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:17 - | - 2 | def foo(a, b): ... - | ^^^ - 3 | - 4 | ab = foo - | - info: Source - --> main.py:6:13 - | - 4 | ab = foo - 5 | - 6 | ab - | ^^ - | - "); - } - - #[test] - fn goto_type_of_expression_with_union_type() { - let test = cursor_test( - r#" - - def foo(a, b): ... - - def bar(a, b): ... - - if random.choice(): - a = foo - else: - a = bar - - a - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:3:17 - | - 3 | def foo(a, b): ... - | ^^^ - 4 | - 5 | def bar(a, b): ... - | - info: Source - --> main.py:12:13 - | - 10 | a = bar - 11 | - 12 | a - | ^ - | - - info[goto-type-definition]: Type definition - --> main.py:5:17 - | - 3 | def foo(a, b): ... - 4 | - 5 | def bar(a, b): ... - | ^^^ - 6 | - 7 | if random.choice(): - | - info: Source - --> main.py:12:13 - | - 10 | a = bar - 11 | - 12 | a - | ^ - | - "); - } - - #[test] - fn goto_type_of_expression_with_module() { - let mut test = cursor_test( - r#" - import lib - - lib - "#, - ); - - test.write_file("lib.py", "a = 10").unwrap(); - - assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> lib.py:1:1 - | - 1 | a = 10 - | ^^^^^^ - | - info: Source - --> main.py:4:13 - | - 2 | import lib - 3 | - 4 | lib - | ^^^ - | - "); - } - - #[test] - fn goto_type_of_expression_with_literal_type() { - let test = cursor_test( - r#" - a: str = "test" - - a - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:892:7 - | - 890 | def __getitem__(self, key: int, /) -> str | int | None: ... - 891 | - 892 | class str(Sequence[str]): - | ^^^ - 893 | """str(object='') -> str - 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source - --> main.py:4:13 - | - 2 | a: str = "test" - 3 | - 4 | a - | ^ - | - "#); - } - #[test] - fn goto_type_of_expression_with_literal_node() { - let test = cursor_test( - r#" - a: str = "test" - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:892:7 - | - 890 | def __getitem__(self, key: int, /) -> str | int | None: ... - 891 | - 892 | class str(Sequence[str]): - | ^^^ - 893 | """str(object='') -> str - 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source - --> main.py:2:22 - | - 2 | a: str = "test" - | ^^^^^^ - | - "#); - } - - #[test] - fn goto_type_of_expression_with_type_var_type() { - let test = cursor_test( - r#" - type Alias[T: int = bool] = list[T] - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:24 - | - 2 | type Alias[T: int = bool] = list[T] - | ^ - | - info: Source - --> main.py:2:46 - | - 2 | type Alias[T: int = bool] = list[T] - | ^ - | - "); - } - - #[test] - fn goto_type_of_expression_with_type_param_spec() { - let test = cursor_test( - r#" - type Alias[**P = [int, str]] = Callable[P, int] - "#, - ); - - // TODO: Goto type definition currently doesn't work for type param specs - // because the inference doesn't support them yet. - // This snapshot should show a single target pointing to `T` - assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); - } - - #[test] - fn goto_type_of_expression_with_type_var_tuple() { - let test = cursor_test( - r#" - type Alias[*Ts = ()] = tuple[*Ts] - "#, - ); - - // TODO: Goto type definition currently doesn't work for type var tuples - // because the inference doesn't support them yet. - // This snapshot should show a single target pointing to `T` - assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); - } - - #[test] - fn goto_type_of_bare_type_alias_type() { - let test = cursor_test( - r#" - from typing_extensions import TypeAliasType - - Alias = TypeAliasType("Alias", tuple[int, int]) - - Alias - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:4:13 - | - 2 | from typing_extensions import TypeAliasType - 3 | - 4 | Alias = TypeAliasType("Alias", tuple[int, int]) - | ^^^^^ - 5 | - 6 | Alias - | - info: Source - --> main.py:6:13 - | - 4 | Alias = TypeAliasType("Alias", tuple[int, int]) - 5 | - 6 | Alias - | ^^^^^ - | - "#); - } - - #[test] - fn goto_type_on_keyword_argument() { - let test = cursor_test( - r#" - def test(a: str): ... - - test(a= "123") - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:892:7 - | - 890 | def __getitem__(self, key: int, /) -> str | int | None: ... - 891 | - 892 | class str(Sequence[str]): - | ^^^ - 893 | """str(object='') -> str - 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source - --> main.py:4:18 - | - 2 | def test(a: str): ... - 3 | - 4 | test(a= "123") - | ^ - | - "#); - } - - #[test] - fn goto_type_on_incorrectly_typed_keyword_argument() { - let test = cursor_test( - r#" - def test(a: str): ... - - test(a= 123) - "#, - ); - - // TODO: This should jump to `str` and not `int` because - // the keyword is typed as a string. It's only the passed argument that - // is an int. Navigating to `str` would match pyright's behavior. - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:338:7 - | - 336 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed - 337 | - 338 | class int: - | ^^^ - 339 | """int([x]) -> integer - 340 | int(x, base=10) -> integer - | - info: Source - --> main.py:4:18 - | - 2 | def test(a: str): ... - 3 | - 4 | test(a= 123) - | ^ - | - "#); - } - - #[test] - fn goto_type_on_kwargs() { - let test = cursor_test( - r#" - def f(name: str): ... - -kwargs = { "name": "test"} - -f(**kwargs) - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:2892:7 - | - 2890 | """See PEP 585""" - 2891 | - 2892 | class dict(MutableMapping[_KT, _VT]): - | ^^^^ - 2893 | """dict() -> new empty dictionary - 2894 | dict(mapping) -> new dictionary initialized from a mapping object's - | - info: Source - --> main.py:6:5 - | - 4 | kwargs = { "name": "test"} - 5 | - 6 | f(**kwargs) - | ^^^^^^ - | - "#); - } - - #[test] - fn goto_type_of_expression_with_builtin() { - let test = cursor_test( - r#" - def foo(a: str): - a - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:892:7 - | - 890 | def __getitem__(self, key: int, /) -> str | int | None: ... - 891 | - 892 | class str(Sequence[str]): - | ^^^ - 893 | """str(object='') -> str - 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source - --> main.py:3:17 - | - 2 | def foo(a: str): - 3 | a - | ^ - | - "#); - } - - #[test] - fn goto_type_definition_cursor_between_object_and_attribute() { - let test = cursor_test( - r#" - class X: - def foo(a, b): ... - - x = X() - - x.foo() - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:19 - | - 2 | class X: - | ^ - 3 | def foo(a, b): ... - | - info: Source - --> main.py:7:13 - | - 5 | x = X() - 6 | - 7 | x.foo() - | ^ - | - "); - } - - #[test] - fn goto_between_call_arguments() { - let test = cursor_test( - r#" - def foo(a, b): ... - - foo() - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r" - info[goto-type-definition]: Type definition - --> main.py:2:17 - | - 2 | def foo(a, b): ... - | ^^^ - 3 | - 4 | foo() - | - info: Source - --> main.py:4:13 - | - 2 | def foo(a, b): ... - 3 | - 4 | foo() - | ^^^ - | - "); - } - - #[test] - fn goto_type_narrowing() { - let test = cursor_test( - r#" - def foo(a: str | None, b): - if a is not None: - print(a) - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:892:7 - | - 890 | def __getitem__(self, key: int, /) -> str | int | None: ... - 891 | - 892 | class str(Sequence[str]): - | ^^^ - 893 | """str(object='') -> str - 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source - --> main.py:4:27 - | - 2 | def foo(a: str | None, b): - 3 | if a is not None: - 4 | print(a) - | ^ - | - "#); - } - - #[test] - fn goto_type_none() { - let test = cursor_test( - r#" - def foo(a: str | None, b): - a - "#, - ); - - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> stdlib/types.pyi:922:11 - | - 920 | if sys.version_info >= (3, 10): - 921 | @final - 922 | class NoneType: - | ^^^^^^^^ - 923 | """The type of the None singleton.""" - | - info: Source - --> main.py:3:17 - | - 2 | def foo(a: str | None, b): - 3 | a - | ^ - | - - info[goto-type-definition]: Type definition - --> stdlib/builtins.pyi:892:7 - | - 890 | def __getitem__(self, key: int, /) -> str | int | None: ... - 891 | - 892 | class str(Sequence[str]): - | ^^^ - 893 | """str(object='') -> str - 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str - | - info: Source - --> main.py:3:17 - | - 2 | def foo(a: str | None, b): - 3 | a - | ^ - | - "#); - } - - impl CursorTest { - fn goto_type_definition(&self) -> String { - let Some(targets) = - goto_type_definition(&self.db, self.cursor.file, self.cursor.offset) - else { - return "No goto target found".to_string(); - }; - - if targets.is_empty() { - return "No type definitions found".to_string(); - } - - let source = targets.range; - self.render_diagnostics( - targets - .into_iter() - .map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)), - ) - } - } - - struct GotoTypeDefinitionDiagnostic { - source: FileRange, - target: FileRange, - } - - impl GotoTypeDefinitionDiagnostic { - fn new(source: FileRange, target: &NavigationTarget) -> Self { - Self { - source, - target: FileRange::new(target.file(), target.focus_range()), - } - } - } - - impl IntoDiagnostic for GotoTypeDefinitionDiagnostic { - fn into_diagnostic(self) -> Diagnostic { - let mut source = SubDiagnostic::new(Severity::Info, "Source"); - source.annotate(Annotation::primary( - Span::from(self.source.file()).with_range(self.source.range()), - )); - - let mut main = Diagnostic::new( - DiagnosticId::Lint(LintName::of("goto-type-definition")), - Severity::Info, - "Type definition".to_string(), - ); - main.annotate(Annotation::primary( - Span::from(self.target.file()).with_range(self.target.range()), - )); - main.sub(source); - - main - } - } -} diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs new file mode 100644 index 0000000000..c1f01d94b2 --- /dev/null +++ b/crates/ty_ide/src/goto_declaration.rs @@ -0,0 +1,813 @@ +use crate::goto::find_goto_target; +use crate::{Db, NavigationTargets, RangedValue}; +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::parsed_module; +use ruff_text_size::{Ranged, TextSize}; + +/// Navigate to the declaration of a symbol. +/// +/// A "declaration" includes both formal declarations (class statements, def statements, +/// and variable annotations) but also variable assignments. This expansive definition +/// is needed because Python doesn't require formal declarations of variables like most languages do. +pub fn goto_declaration( + db: &dyn Db, + file: File, + offset: TextSize, +) -> Option> { + let module = parsed_module(db, file).load(db); + let goto_target = find_goto_target(&module, offset)?; + + let declaration_targets = goto_target.get_definition_targets(file, db, None)?; + + Some(RangedValue { + range: FileRange::new(file, goto_target.range()), + value: declaration_targets, + }) +} + +#[cfg(test)] +mod tests { + use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; + use crate::{NavigationTarget, goto_declaration}; + use insta::assert_snapshot; + use ruff_db::diagnostic::{ + Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, + }; + use ruff_db::files::FileRange; + use ruff_text_size::Ranged; + + #[test] + fn goto_declaration_function_call_to_definition() { + let test = cursor_test( + " + def my_function(x, y): + return x + y + + result = my_function(1, 2) + ", + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:17 + | + 2 | def my_function(x, y): + | ^^^^^^^^^^^ + 3 | return x + y + | + info: Source + --> main.py:5:22 + | + 3 | return x + y + 4 | + 5 | result = my_function(1, 2) + | ^^^^^^^^^^^ + | + "); + } + + #[test] + fn goto_declaration_variable_assignment() { + let test = cursor_test( + " + x = 42 + y = x + ", + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:13 + | + 2 | x = 42 + | ^ + 3 | y = x + | + info: Source + --> main.py:3:17 + | + 2 | x = 42 + 3 | y = x + | ^ + | + "); + } + + #[test] + fn goto_declaration_class_instantiation() { + let test = cursor_test( + " + class MyClass: + def __init__(self): + pass + + instance = MyClass() + ", + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:19 + | + 2 | class MyClass: + | ^^^^^^^ + 3 | def __init__(self): + 4 | pass + | + info: Source + --> main.py:6:24 + | + 4 | pass + 5 | + 6 | instance = MyClass() + | ^^^^^^^ + | + "); + } + + #[test] + fn goto_declaration_parameter_usage() { + let test = cursor_test( + " + def foo(param): + return param * 2 + ", + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:21 + | + 2 | def foo(param): + | ^^^^^ + 3 | return param * 2 + | + info: Source + --> main.py:3:24 + | + 2 | def foo(param): + 3 | return param * 2 + | ^^^^^ + | + "); + } + + #[test] + fn goto_declaration_type_parameter() { + let test = cursor_test( + " + def generic_func[T](value: T) -> T: + v: T = value + return v + ", + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:30 + | + 2 | def generic_func[T](value: T) -> T: + | ^ + 3 | v: T = value + 4 | return v + | + info: Source + --> main.py:3:20 + | + 2 | def generic_func[T](value: T) -> T: + 3 | v: T = value + | ^ + 4 | return v + | + "); + } + + #[test] + fn goto_declaration_type_parameter_class() { + let test = cursor_test( + " + class GenericClass[T]: + def __init__(self, value: T): + self.value = value + ", + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:32 + | + 2 | class GenericClass[T]: + | ^ + 3 | def __init__(self, value: T): + 4 | self.value = value + | + info: Source + --> main.py:3:43 + | + 2 | class GenericClass[T]: + 3 | def __init__(self, value: T): + | ^ + 4 | self.value = value + | + "); + } + + #[test] + fn goto_declaration_nested_scope_variable() { + let test = cursor_test( + " + x = \"outer\" + def outer_func(): + def inner_func(): + return x # Should find outer x + return inner_func + ", + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:2:13 + | + 2 | x = "outer" + | ^ + 3 | def outer_func(): + 4 | def inner_func(): + | + info: Source + --> main.py:5:28 + | + 3 | def outer_func(): + 4 | def inner_func(): + 5 | return x # Should find outer x + | ^ + 6 | return inner_func + | + "#); + } + + #[test] + fn goto_declaration_class_scope_skipped() { + let test = cursor_test( + r#" +class A: + x = 1 + + def method(self): + def inner(): + return x # Should NOT find class variable x + return inner +"#, + ); + + // Should not find the class variable 'x' due to Python's scoping rules + assert_snapshot!(test.goto_declaration(), @"No goto target found"); + } + + #[test] + fn goto_declaration_import_simple() { + let test = CursorTest::builder() + .source( + "main.py", + " +import mymodule +print(mymodule.function()) +", + ) + .source( + "mymodule.py", + r#" +def function(): + return "hello from mymodule" + +variable = 42 +"#, + ) + .build(); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> mymodule.py:1:1 + | + 1 | + | ^ + 2 | def function(): + 3 | return "hello from mymodule" + | + info: Source + --> main.py:3:7 + | + 2 | import mymodule + 3 | print(mymodule.function()) + | ^^^^^^^^ + | + "#); + } + + #[test] + fn goto_declaration_import_from() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import my_function +print(my_function()) +", + ) + .source( + "mymodule.py", + r#" +def my_function(): + return "hello" + +def other_function(): + return "other" +"#, + ) + .build(); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> mymodule.py:2:5 + | + 2 | def my_function(): + | ^^^^^^^^^^^ + 3 | return "hello" + | + info: Source + --> main.py:3:7 + | + 2 | from mymodule import my_function + 3 | print(my_function()) + | ^^^^^^^^^^^ + | + "#); + } + + #[test] + fn goto_declaration_import_as() { + let test = CursorTest::builder() + .source( + "main.py", + " +import mymodule.submodule as sub +print(sub.helper()) +", + ) + .source( + "mymodule/__init__.py", + " +# Main module init +", + ) + .source( + "mymodule/submodule.py", + r#" +FOO = 0 +"#, + ) + .build(); + + // Should find the submodule file itself + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> mymodule/submodule.py:1:1 + | + 1 | + | ^ + 2 | FOO = 0 + | + info: Source + --> main.py:3:7 + | + 2 | import mymodule.submodule as sub + 3 | print(sub.helper()) + | ^^^ + | + "#); + } + + #[test] + fn goto_declaration_from_import_as() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +from utils import func as h +print(h("test")) +"#, + ) + .source( + "utils.py", + r#" +def func(arg): + return f"Processed: {arg}" +"#, + ) + .build(); + + // Should resolve to the actual function definition, not the import statement + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> utils.py:2:5 + | + 2 | def func(arg): + | ^^^^ + 3 | return f"Processed: {arg}" + | + info: Source + --> main.py:3:7 + | + 2 | from utils import func as h + 3 | print(h("test")) + | ^ + | + "#); + } + + #[test] + fn goto_declaration_from_import_chain() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +from intermediate import shared_function +print(shared_function()) +"#, + ) + .source( + "intermediate.py", + r#" +# Re-export the function from the original module +from original import shared_function +"#, + ) + .source( + "original.py", + r#" +def shared_function(): + return "from original" +"#, + ) + .build(); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> original.py:2:5 + | + 2 | def shared_function(): + | ^^^^^^^^^^^^^^^ + 3 | return "from original" + | + info: Source + --> main.py:3:7 + | + 2 | from intermediate import shared_function + 3 | print(shared_function()) + | ^^^^^^^^^^^^^^^ + | + "#); + } + + #[test] + fn goto_declaration_from_star_import() { + let test = CursorTest::builder() + .source( + "main.py", + r#" +from math_utils import * +result = add_numbers(5, 3) +"#, + ) + .source( + "math_utils.py", + r#" +def add_numbers(a, b): + """Add two numbers together.""" + return a + b + +def multiply_numbers(a, b): + """Multiply two numbers together.""" + return a * b +"#, + ) + .build(); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> math_utils.py:2:5 + | + 2 | def add_numbers(a, b): + | ^^^^^^^^^^^ + 3 | """Add two numbers together.""" + 4 | return a + b + | + info: Source + --> main.py:3:10 + | + 2 | from math_utils import * + 3 | result = add_numbers(5, 3) + | ^^^^^^^^^^^ + | + "#); + } + + #[test] + fn goto_declaration_relative_import() { + let test = CursorTest::builder() + .source( + "package/main.py", + r#" +from .utils import helper_function +result = helper_function("test") +"#, + ) + .source( + "package/__init__.py", + r#" +# Package init file +"#, + ) + .source( + "package/utils.py", + r#" +def helper_function(arg): + """A helper function in utils module.""" + return f"Processed: {arg}" + +def another_helper(): + """Another helper function.""" + pass +"#, + ) + .build(); + + // Should resolve the relative import to find the actual function definition + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> package/utils.py:2:5 + | + 2 | def helper_function(arg): + | ^^^^^^^^^^^^^^^ + 3 | """A helper function in utils module.""" + 4 | return f"Processed: {arg}" + | + info: Source + --> package/main.py:3:10 + | + 2 | from .utils import helper_function + 3 | result = helper_function("test") + | ^^^^^^^^^^^^^^^ + | + "#); + } + + #[test] + fn goto_declaration_relative_star_import() { + let test = CursorTest::builder() + .source( + "package/main.py", + r#" +from .utils import * +result = helper_function("test") +"#, + ) + .source( + "package/__init__.py", + r#" +# Package init file +"#, + ) + .source( + "package/utils.py", + r#" +def helper_function(arg): + """A helper function in utils module.""" + return f"Processed: {arg}" + +def another_helper(): + """Another helper function.""" + pass +"#, + ) + .build(); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> package/utils.py:2:5 + | + 2 | def helper_function(arg): + | ^^^^^^^^^^^^^^^ + 3 | """A helper function in utils module.""" + 4 | return f"Processed: {arg}" + | + info: Source + --> package/main.py:3:10 + | + 2 | from .utils import * + 3 | result = helper_function("test") + | ^^^^^^^^^^^^^^^ + | + "#); + } + + #[test] + fn goto_declaration_builtin_type() { + let test = cursor_test( + r#" +x: int = 42 +"#, + ); + + // Test that we can navigate to builtin types, but don't snapshot the exact content + // since typeshed stubs can change frequently + let result = test.goto_declaration(); + + // Should not be "No goto target found" - we should find the builtin int type + assert!( + !result.contains("No goto target found"), + "Should find builtin int type" + ); + assert!( + !result.contains("No declarations found"), + "Should find builtin int declarations" + ); + + // Should navigate to a stdlib file containing the int class + assert!( + result.contains("builtins.pyi"), + "Should navigate to builtins.pyi" + ); + assert!( + result.contains("class int:"), + "Should find the int class definition" + ); + assert!( + result.contains("info[goto-declaration]: Declaration"), + "Should be a goto-declaration result" + ); + } + + #[test] + fn goto_declaration_nonlocal_binding() { + let test = cursor_test( + r#" +def outer(): + x = "outer_value" + + def inner(): + nonlocal x + x = "modified" + return x # Should find the nonlocal x declaration in outer scope + + return inner +"#, + ); + + // Should find the variable declaration in the outer scope, not the nonlocal statement + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:3:5 + | + 2 | def outer(): + 3 | x = "outer_value" + | ^ + 4 | + 5 | def inner(): + | + info: Source + --> main.py:8:16 + | + 6 | nonlocal x + 7 | x = "modified" + 8 | return x # Should find the nonlocal x declaration in outer scope + | ^ + 9 | + 10 | return inner + | + "#); + } + + #[test] + fn goto_declaration_global_binding() { + let test = cursor_test( + r#" +global_var = "global_value" + +def function(): + global global_var + global_var = "modified" + return global_var # Should find the global variable declaration +"#, + ); + + // Should find the global variable declaration, not the global statement + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:2:1 + | + 2 | global_var = "global_value" + | ^^^^^^^^^^ + 3 | + 4 | def function(): + | + info: Source + --> main.py:7:12 + | + 5 | global global_var + 6 | global_var = "modified" + 7 | return global_var # Should find the global variable declaration + | ^^^^^^^^^^ + | + "#); + } + + #[test] + fn goto_declaration_generic_method_class_type() { + let test = cursor_test( + r#" +class MyClass: + ClassType = int + + def generic_method[T](self, value: ClassType) -> T: + return value +"#, + ); + + // Should find the ClassType defined in the class body, not fail to resolve + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:3:5 + | + 2 | class MyClass: + 3 | ClassType = int + | ^^^^^^^^^ + 4 | + 5 | def generic_method[T](self, value: ClassType) -> T: + | + info: Source + --> main.py:5:40 + | + 3 | ClassType = int + 4 | + 5 | def generic_method[T](self, value: ClassType) -> T: + | ^^^^^^^^^ + 6 | return value + | + "); + } + + impl CursorTest { + fn goto_declaration(&self) -> String { + let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset) + else { + return "No goto target found".to_string(); + }; + + if targets.is_empty() { + return "No declarations found".to_string(); + } + + let source = targets.range; + self.render_diagnostics( + targets + .into_iter() + .map(|target| GotoDeclarationDiagnostic::new(source, &target)), + ) + } + } + + struct GotoDeclarationDiagnostic { + source: FileRange, + target: FileRange, + } + + impl GotoDeclarationDiagnostic { + fn new(source: FileRange, target: &NavigationTarget) -> Self { + Self { + source, + target: FileRange::new(target.file(), target.focus_range()), + } + } + } + + impl IntoDiagnostic for GotoDeclarationDiagnostic { + fn into_diagnostic(self) -> Diagnostic { + let mut source = SubDiagnostic::new(Severity::Info, "Source"); + source.annotate(Annotation::primary( + Span::from(self.source.file()).with_range(self.source.range()), + )); + + let mut main = Diagnostic::new( + DiagnosticId::Lint(LintName::of("goto-declaration")), + Severity::Info, + "Declaration".to_string(), + ); + main.annotate(Annotation::primary( + Span::from(self.target.file()).with_range(self.target.range()), + )); + main.sub(source); + + main + } + } +} diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs new file mode 100644 index 0000000000..3a65d9fc3f --- /dev/null +++ b/crates/ty_ide/src/goto_definition.rs @@ -0,0 +1,31 @@ +use crate::goto::find_goto_target; +use crate::stub_mapping::StubMapper; +use crate::{Db, NavigationTargets, RangedValue}; +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::parsed_module; +use ruff_text_size::{Ranged, TextSize}; + +/// Navigate to the definition of a symbol. +/// +/// A "definition" is the actual implementation of a symbol, potentially in a source file +/// rather than a stub file. This differs from "declaration" which may navigate to stub files. +/// When possible, this function will map from stub file declarations to their corresponding +/// source file implementations using the `StubMapper`. +pub fn goto_definition( + db: &dyn Db, + file: File, + offset: TextSize, +) -> Option> { + let module = parsed_module(db, file).load(db); + let goto_target = find_goto_target(&module, offset)?; + + // Create a StubMapper to map from stub files to source files + let stub_mapper = StubMapper::new(db); + + let definition_targets = goto_target.get_definition_targets(file, db, Some(&stub_mapper))?; + + Some(RangedValue { + range: FileRange::new(file, goto_target.range()), + value: definition_targets, + }) +} diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs new file mode 100644 index 0000000000..d68a314ee6 --- /dev/null +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -0,0 +1,661 @@ +use crate::goto::find_goto_target; +use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::parsed_module; +use ruff_text_size::{Ranged, TextSize}; +use ty_python_semantic::SemanticModel; + +pub fn goto_type_definition( + db: &dyn Db, + file: File, + offset: TextSize, +) -> Option> { + let module = parsed_module(db, file).load(db); + let goto_target = find_goto_target(&module, offset)?; + + let model = SemanticModel::new(db, file); + let ty = goto_target.inferred_type(&model)?; + + tracing::debug!("Inferred type of covering node is {}", ty.display(db)); + + let navigation_targets = ty.navigation_targets(db); + + Some(RangedValue { + range: FileRange::new(file, goto_target.range()), + value: navigation_targets, + }) +} + +#[cfg(test)] +mod tests { + use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; + use crate::{NavigationTarget, goto_type_definition}; + use insta::assert_snapshot; + use ruff_db::diagnostic::{ + Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, + }; + use ruff_db::files::FileRange; + use ruff_text_size::Ranged; + + #[test] + fn goto_type_of_expression_with_class_type() { + let test = cursor_test( + r#" + class Test: ... + + ab = Test() + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:19 + | + 2 | class Test: ... + | ^^^^ + 3 | + 4 | ab = Test() + | + info: Source + --> main.py:4:13 + | + 2 | class Test: ... + 3 | + 4 | ab = Test() + | ^^ + | + "); + } + + #[test] + fn goto_type_of_expression_with_function_type() { + let test = cursor_test( + r#" + def foo(a, b): ... + + ab = foo + + ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:17 + | + 2 | def foo(a, b): ... + | ^^^ + 3 | + 4 | ab = foo + | + info: Source + --> main.py:6:13 + | + 4 | ab = foo + 5 | + 6 | ab + | ^^ + | + "); + } + + #[test] + fn goto_type_of_expression_with_union_type() { + let test = cursor_test( + r#" + + def foo(a, b): ... + + def bar(a, b): ... + + if random.choice(): + a = foo + else: + a = bar + + a + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:3:17 + | + 3 | def foo(a, b): ... + | ^^^ + 4 | + 5 | def bar(a, b): ... + | + info: Source + --> main.py:12:13 + | + 10 | a = bar + 11 | + 12 | a + | ^ + | + + info[goto-type-definition]: Type definition + --> main.py:5:17 + | + 3 | def foo(a, b): ... + 4 | + 5 | def bar(a, b): ... + | ^^^ + 6 | + 7 | if random.choice(): + | + info: Source + --> main.py:12:13 + | + 10 | a = bar + 11 | + 12 | a + | ^ + | + "); + } + + #[test] + fn goto_type_of_expression_with_module() { + let mut test = cursor_test( + r#" + import lib + + lib + "#, + ); + + test.write_file("lib.py", "a = 10").unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ^^^^^^ + | + info: Source + --> main.py:4:13 + | + 2 | import lib + 3 | + 4 | lib + | ^^^ + | + "); + } + + #[test] + fn goto_type_of_expression_with_literal_type() { + let test = cursor_test( + r#" + a: str = "test" + + a + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:892:7 + | + 890 | def __getitem__(self, key: int, /) -> str | int | None: ... + 891 | + 892 | class str(Sequence[str]): + | ^^^ + 893 | """str(object='') -> str + 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:4:13 + | + 2 | a: str = "test" + 3 | + 4 | a + | ^ + | + "#); + } + #[test] + fn goto_type_of_expression_with_literal_node() { + let test = cursor_test( + r#" + a: str = "test" + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:892:7 + | + 890 | def __getitem__(self, key: int, /) -> str | int | None: ... + 891 | + 892 | class str(Sequence[str]): + | ^^^ + 893 | """str(object='') -> str + 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:2:22 + | + 2 | a: str = "test" + | ^^^^^^ + | + "#); + } + + #[test] + fn goto_type_of_expression_with_type_var_type() { + let test = cursor_test( + r#" + type Alias[T: int = bool] = list[T] + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:24 + | + 2 | type Alias[T: int = bool] = list[T] + | ^ + | + info: Source + --> main.py:2:46 + | + 2 | type Alias[T: int = bool] = list[T] + | ^ + | + "); + } + + #[test] + fn goto_type_of_expression_with_type_param_spec() { + let test = cursor_test( + r#" + type Alias[**P = [int, str]] = Callable[P, int] + "#, + ); + + // TODO: Goto type definition currently doesn't work for type param specs + // because the inference doesn't support them yet. + // This snapshot should show a single target pointing to `T` + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + + #[test] + fn goto_type_of_expression_with_type_var_tuple() { + let test = cursor_test( + r#" + type Alias[*Ts = ()] = tuple[*Ts] + "#, + ); + + // TODO: Goto type definition currently doesn't work for type var tuples + // because the inference doesn't support them yet. + // This snapshot should show a single target pointing to `T` + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + + #[test] + fn goto_type_of_bare_type_alias_type() { + let test = cursor_test( + r#" + from typing_extensions import TypeAliasType + + Alias = TypeAliasType("Alias", tuple[int, int]) + + Alias + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> main.py:4:13 + | + 2 | from typing_extensions import TypeAliasType + 3 | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + | ^^^^^ + 5 | + 6 | Alias + | + info: Source + --> main.py:6:13 + | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + 5 | + 6 | Alias + | ^^^^^ + | + "#); + } + + #[test] + fn goto_type_on_keyword_argument() { + let test = cursor_test( + r#" + def test(a: str): ... + + test(a= "123") + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:892:7 + | + 890 | def __getitem__(self, key: int, /) -> str | int | None: ... + 891 | + 892 | class str(Sequence[str]): + | ^^^ + 893 | """str(object='') -> str + 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:4:18 + | + 2 | def test(a: str): ... + 3 | + 4 | test(a= "123") + | ^ + | + "#); + } + + #[test] + fn goto_type_on_incorrectly_typed_keyword_argument() { + let test = cursor_test( + r#" + def test(a: str): ... + + test(a= 123) + "#, + ); + + // TODO: This should jump to `str` and not `int` because + // the keyword is typed as a string. It's only the passed argument that + // is an int. Navigating to `str` would match pyright's behavior. + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:338:7 + | + 336 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed + 337 | + 338 | class int: + | ^^^ + 339 | """int([x]) -> integer + 340 | int(x, base=10) -> integer + | + info: Source + --> main.py:4:18 + | + 2 | def test(a: str): ... + 3 | + 4 | test(a= 123) + | ^ + | + "#); + } + + #[test] + fn goto_type_on_kwargs() { + let test = cursor_test( + r#" + def f(name: str): ... + +kwargs = { "name": "test"} + +f(**kwargs) + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:2892:7 + | + 2890 | """See PEP 585""" + 2891 | + 2892 | class dict(MutableMapping[_KT, _VT]): + | ^^^^ + 2893 | """dict() -> new empty dictionary + 2894 | dict(mapping) -> new dictionary initialized from a mapping object's + | + info: Source + --> main.py:6:5 + | + 4 | kwargs = { "name": "test"} + 5 | + 6 | f(**kwargs) + | ^^^^^^ + | + "#); + } + + #[test] + fn goto_type_of_expression_with_builtin() { + let test = cursor_test( + r#" + def foo(a: str): + a + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:892:7 + | + 890 | def __getitem__(self, key: int, /) -> str | int | None: ... + 891 | + 892 | class str(Sequence[str]): + | ^^^ + 893 | """str(object='') -> str + 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:3:17 + | + 2 | def foo(a: str): + 3 | a + | ^ + | + "#); + } + + #[test] + fn goto_type_definition_cursor_between_object_and_attribute() { + let test = cursor_test( + r#" + class X: + def foo(a, b): ... + + x = X() + + x.foo() + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:19 + | + 2 | class X: + | ^ + 3 | def foo(a, b): ... + | + info: Source + --> main.py:7:13 + | + 5 | x = X() + 6 | + 7 | x.foo() + | ^ + | + "); + } + + #[test] + fn goto_between_call_arguments() { + let test = cursor_test( + r#" + def foo(a, b): ... + + foo() + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:17 + | + 2 | def foo(a, b): ... + | ^^^ + 3 | + 4 | foo() + | + info: Source + --> main.py:4:13 + | + 2 | def foo(a, b): ... + 3 | + 4 | foo() + | ^^^ + | + "); + } + + #[test] + fn goto_type_narrowing() { + let test = cursor_test( + r#" + def foo(a: str | None, b): + if a is not None: + print(a) + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:892:7 + | + 890 | def __getitem__(self, key: int, /) -> str | int | None: ... + 891 | + 892 | class str(Sequence[str]): + | ^^^ + 893 | """str(object='') -> str + 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:4:27 + | + 2 | def foo(a: str | None, b): + 3 | if a is not None: + 4 | print(a) + | ^ + | + "#); + } + + #[test] + fn goto_type_none() { + let test = cursor_test( + r#" + def foo(a: str | None, b): + a + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/types.pyi:922:11 + | + 920 | if sys.version_info >= (3, 10): + 921 | @final + 922 | class NoneType: + | ^^^^^^^^ + 923 | """The type of the None singleton.""" + | + info: Source + --> main.py:3:17 + | + 2 | def foo(a: str | None, b): + 3 | a + | ^ + | + + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:892:7 + | + 890 | def __getitem__(self, key: int, /) -> str | int | None: ... + 891 | + 892 | class str(Sequence[str]): + | ^^^ + 893 | """str(object='') -> str + 894 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:3:17 + | + 2 | def foo(a: str | None, b): + 3 | a + | ^ + | + "#); + } + + impl CursorTest { + fn goto_type_definition(&self) -> String { + let Some(targets) = + goto_type_definition(&self.db, self.cursor.file, self.cursor.offset) + else { + return "No goto target found".to_string(); + }; + + if targets.is_empty() { + return "No type definitions found".to_string(); + } + + let source = targets.range; + self.render_diagnostics( + targets + .into_iter() + .map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)), + ) + } + } + + struct GotoTypeDefinitionDiagnostic { + source: FileRange, + target: FileRange, + } + + impl GotoTypeDefinitionDiagnostic { + fn new(source: FileRange, target: &NavigationTarget) -> Self { + Self { + source, + target: FileRange::new(target.file(), target.focus_range()), + } + } + } + + impl IntoDiagnostic for GotoTypeDefinitionDiagnostic { + fn into_diagnostic(self) -> Diagnostic { + let mut source = SubDiagnostic::new(Severity::Info, "Source"); + source.annotate(Annotation::primary( + Span::from(self.source.file()).with_range(self.source.range()), + )); + + let mut main = Diagnostic::new( + DiagnosticId::Lint(LintName::of("goto-type-definition")), + Severity::Info, + "Type definition".to_string(), + ); + main.annotate(Annotation::primary( + Span::from(self.target.file()).with_range(self.target.range()), + )); + main.sub(source); + + main + } + } +} diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index b3706539cb..d02523dfab 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -3,16 +3,20 @@ mod db; mod docstring; mod find_node; mod goto; +mod goto_declaration; +mod goto_definition; +mod goto_type_definition; mod hover; mod inlay_hints; mod markup; mod semantic_tokens; mod signature_help; +mod stub_mapping; pub use completion::completion; pub use db::Db; pub use docstring::get_parameter_documentation; -pub use goto::goto_type_definition; +pub use goto::{goto_declaration, goto_definition, goto_type_definition}; pub use hover::hover; pub use inlay_hints::inlay_hints; pub use markup::MarkupKind; diff --git a/crates/ty_ide/src/stub_mapping.rs b/crates/ty_ide/src/stub_mapping.rs new file mode 100644 index 0000000000..9be4ccc7d5 --- /dev/null +++ b/crates/ty_ide/src/stub_mapping.rs @@ -0,0 +1,47 @@ +use ty_python_semantic::ResolvedDefinition; + +/// Maps `ResolvedDefinitions` from stub files to corresponding definitions in source files. +/// +/// This mapper is used to implement "Go To Definition" functionality that navigates from +/// stub file declarations to their actual implementations in source files. It also allows +/// other language server providers (like hover, completion, and signature help) to find +/// docstrings for functions that resolve to stubs. +pub(crate) struct StubMapper<'db> { + #[allow(dead_code)] // Will be used when implementation is added + db: &'db dyn crate::Db, +} + +impl<'db> StubMapper<'db> { + #[allow(dead_code)] // Will be used in the future + pub(crate) fn new(db: &'db dyn crate::Db) -> Self { + Self { db } + } + + /// Map a `ResolvedDefinition` from a stub file to corresponding definitions in source files. + /// + /// If the definition is in a stub file and a corresponding source file definition exists, + /// returns the source file definition(s). Otherwise, returns the original definition. + #[allow(dead_code)] // Will be used when implementation is added + #[allow(clippy::unused_self)] // Will use self when implementation is added + pub(crate) fn map_definition( + &self, + def: ResolvedDefinition<'db>, + ) -> Vec> { + // TODO: Implement stub-to-source mapping logic + // For now, just return the original definition + vec![def] + } + + /// Map multiple `ResolvedDefinitions`, applying stub-to-source mapping to each. + /// + /// This is a convenience method that applies `map_definition` to each element + /// in the input vector and flattens the results. + pub(crate) fn map_definitions( + &self, + defs: Vec>, + ) -> Vec> { + defs.into_iter() + .flat_map(|def| self.map_definition(def)) + .collect() + } +} diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 007abd253d..63717874f9 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -17,6 +17,8 @@ pub use program::{ pub use python_platform::PythonPlatform; pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel}; pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; +pub use types::definitions_for_name; +pub use types::ide_support::ResolvedDefinition; pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic; pub mod ast_node_ref; diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index e5219dce43..54729b2d52 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -388,6 +388,24 @@ impl<'db> SemanticIndex<'db> { AncestorsIter::new(self, scope) } + /// Returns an iterator over ancestors of `scope` that are visible for name resolution, + /// starting with `scope` itself. This follows Python's lexical scoping rules where + /// class scopes are skipped during name resolution (except for the starting scope + /// if it happens to be a class scope). + /// + /// For example, in this code: + /// ```python + /// x = 1 + /// class A: + /// x = 2 + /// def method(self): + /// print(x) # Refers to global x=1, not class x=2 + /// ``` + /// The `method` function can see the global scope but not the class scope. + pub(crate) fn visible_ancestor_scopes(&self, scope: FileScopeId) -> VisibleAncestorsIter { + VisibleAncestorsIter::new(self, scope) + } + /// Returns the [`definition::Definition`] salsa ingredient(s) for `definition_key`. /// /// There will only ever be >1 `Definition` associated with a `definition_key` @@ -553,6 +571,53 @@ impl<'a> Iterator for AncestorsIter<'a> { impl FusedIterator for AncestorsIter<'_> {} +pub struct VisibleAncestorsIter<'a> { + inner: AncestorsIter<'a>, + starting_scope_kind: ScopeKind, + yielded_count: usize, +} + +impl<'a> VisibleAncestorsIter<'a> { + fn new(module_table: &'a SemanticIndex, start: FileScopeId) -> Self { + let starting_scope = &module_table.scopes[start]; + Self { + inner: AncestorsIter::new(module_table, start), + starting_scope_kind: starting_scope.kind(), + yielded_count: 0, + } + } +} + +impl<'a> Iterator for VisibleAncestorsIter<'a> { + type Item = (FileScopeId, &'a Scope); + + fn next(&mut self) -> Option { + loop { + let (scope_id, scope) = self.inner.next()?; + self.yielded_count += 1; + + // Always return the first scope (the starting scope) + if self.yielded_count == 1 { + return Some((scope_id, scope)); + } + + // Skip class scopes for subsequent scopes (following Python's lexical scoping rules) + // Exception: type parameter scopes can see names defined in an immediately-enclosing class scope + if scope.kind() == ScopeKind::Class { + // Allow type parameter scopes to see their immediately-enclosing class scope exactly once + if self.starting_scope_kind.is_type_parameter() && self.yielded_count == 2 { + return Some((scope_id, scope)); + } + continue; + } + + return Some((scope_id, scope)); + } + } +} + +impl FusedIterator for VisibleAncestorsIter<'_> {} + pub struct DescendantsIter<'a> { next_id: FileScopeId, descendants: std::slice::Iter<'a, Scope>, diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index aa9d336800..e7323ad370 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -23,7 +23,7 @@ use crate::unpack::{Unpack, UnpackPosition}; #[salsa::tracked(debug)] pub struct Definition<'db> { /// The file in which the definition occurs. - pub(crate) file: File, + pub file: File, /// The scope in which the definition occurs. pub(crate) file_scope: FileScopeId, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0f69f6e582..fd3668ccf0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -49,6 +49,7 @@ use crate::types::generics::{ }; pub use crate::types::ide_support::{ CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name, + definitions_for_name, }; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 9a98286afe..ddc673bdc5 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,6 +1,8 @@ use std::cmp::Ordering; -use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations}; +use crate::place::{ + Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations, +}; use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::DefinitionKind; use crate::semantic_index::place::ScopeId; @@ -386,6 +388,121 @@ pub fn definition_kind_for_name<'db>( None } +/// Returns all definitions for a name. If any definitions are imports, they +/// are resolved (recursively) to the original definitions or module files. +pub fn definitions_for_name<'db>( + db: &'db dyn Db, + file: File, + name: &ast::ExprName, +) -> Vec> { + let index = semantic_index(db, file); + let name_str = name.id.as_str(); + + // Get the scope for this name expression + let Some(file_scope) = index.try_expression_scope_id(&ast::Expr::Name(name.clone())) else { + return Vec::new(); + }; + + let mut all_definitions = Vec::new(); + + // Search through the scope hierarchy: start from the current scope and + // traverse up through parent scopes to find definitions + for (scope_id, _scope) in index.visible_ancestor_scopes(file_scope) { + let place_table = index.place_table(scope_id); + + let Some(place_id) = place_table.place_id_by_name(name_str) else { + continue; // Name not found in this scope, try parent scope + }; + + // Check if this place is marked as global or nonlocal + let place_expr = place_table.place_expr(place_id); + let is_global = place_expr.is_marked_global(); + let is_nonlocal = place_expr.is_marked_nonlocal(); + + // TODO: The current algorithm doesn't return definintions or bindings + // for other scopes that are outside of this scope hierarchy that target + // this name using a nonlocal or global binding. The semantic analyzer + // doesn't appear to track these in a way that we can easily access + // them from here without walking all scopes in the module. + + // If marked as global, skip to global scope + if is_global { + let global_scope_id = global_scope(db, file); + let global_place_table = crate::semantic_index::place_table(db, global_scope_id); + + if let Some(global_place_id) = global_place_table.place_id_by_name(name_str) { + let global_use_def_map = crate::semantic_index::use_def_map(db, global_scope_id); + let global_bindings = global_use_def_map.all_reachable_bindings(global_place_id); + let global_declarations = + global_use_def_map.all_reachable_declarations(global_place_id); + + for binding in global_bindings { + if let Some(def) = binding.binding.definition() { + all_definitions.push(def); + } + } + + for declaration in global_declarations { + if let Some(def) = declaration.declaration.definition() { + all_definitions.push(def); + } + } + } + break; + } + + // If marked as nonlocal, skip current scope and search in ancestor scopes + if is_nonlocal { + // Continue searching in parent scopes, but skip the current scope + continue; + } + + let use_def_map = index.use_def_map(scope_id); + + // Get all definitions (both bindings and declarations) for this place + let bindings = use_def_map.all_reachable_bindings(place_id); + let declarations = use_def_map.all_reachable_declarations(place_id); + + for binding in bindings { + if let Some(def) = binding.binding.definition() { + all_definitions.push(def); + } + } + + for declaration in declarations { + if let Some(def) = declaration.declaration.definition() { + all_definitions.push(def); + } + } + + // If we found definitions in this scope, we can stop searching + if !all_definitions.is_empty() { + break; + } + } + + // Resolve import definitions to their targets + let mut resolved_definitions = Vec::new(); + + for definition in &all_definitions { + let resolved = resolve_definition(db, *definition, Some(name_str)); + resolved_definitions.extend(resolved); + } + + // If we didn't find any definitions in scopes, fallback to builtins + if resolved_definitions.is_empty() { + let Some(builtins_scope) = builtins_module_scope(db) else { + return Vec::new(); + }; + find_symbol_in_scope(db, builtins_scope, name_str) + .into_iter() + .flat_map(|def| resolve_definition(db, def, Some(name_str))) + .collect() + } else { + resolved_definitions + } +} + /// Details about a callable signature for IDE support. #[derive(Debug, Clone)] pub struct CallSignatureDetails<'db> { @@ -455,3 +572,201 @@ pub fn call_signature_details<'db>( vec![] } } + +mod resolve_definition { + //! Resolves an Import, `ImportFrom` or `StarImport` definition to one or more + //! "resolved definitions". This is done recursively to find the original + //! definition targeted by the import. + + use ruff_db::files::File; + use ruff_db::parsed::parsed_module; + use ruff_python_ast as ast; + use rustc_hash::FxHashSet; + + use crate::semantic_index::definition::{Definition, DefinitionKind}; + use crate::semantic_index::place::ScopeId; + use crate::semantic_index::{global_scope, place_table, use_def_map}; + use crate::{Db, ModuleName, resolve_module}; + + /// Represents the result of resolving an import to either a specific definition or a module file. + /// This enum helps distinguish between cases where an import resolves to: + /// - A specific definition within a module (e.g., `from os import path` -> definition of `path`) + /// - An entire module file (e.g., `import os` -> the `os` module file itself) + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum ResolvedDefinition<'db> { + /// The import resolved to a specific definition within a module + Definition(Definition<'db>), + /// The import resolved to an entire module file + ModuleFile(File), + } + + /// Resolve import definitions to their targets. + /// Returns resolved definitions which can be either specific definitions or module files. + /// For non-import definitions, returns the definition wrapped in `ResolvedDefinition::Definition`. + /// Always returns at least the original definition as a fallback if resolution fails. + pub(crate) fn resolve_definition<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + symbol_name: Option<&str>, + ) -> Vec> { + let mut visited = FxHashSet::default(); + let resolved = resolve_definition_recursive(db, definition, &mut visited, symbol_name); + + // If resolution failed, return the original definition as fallback + if resolved.is_empty() { + vec![ResolvedDefinition::Definition(definition)] + } else { + resolved + } + } + + /// Helper function to resolve import definitions recursively. + fn resolve_definition_recursive<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + visited: &mut FxHashSet>, + symbol_name: Option<&str>, + ) -> Vec> { + // Prevent infinite recursion if there are circular imports + if visited.contains(&definition) { + return Vec::new(); // Return empty list for circular imports + } + visited.insert(definition); + + let kind = definition.kind(db); + + match kind { + DefinitionKind::Import(import_def) => { + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let alias = import_def.alias(&module); + + // Get the full module name being imported + let Some(module_name) = ModuleName::new(&alias.name) else { + return Vec::new(); // Invalid module name, return empty list + }; + + // Resolve the module to its file + let Some(resolved_module) = resolve_module(db, &module_name) else { + return Vec::new(); // Module not found, return empty list + }; + + let Some(module_file) = resolved_module.file() else { + return Vec::new(); // No file for module, return empty list + }; + + // For simple imports like "import os", we want to navigate to the module itself. + // Return the module file directly instead of trying to find definitions within it. + vec![ResolvedDefinition::ModuleFile(module_file)] + } + + DefinitionKind::ImportFrom(import_from_def) => { + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let import_node = import_from_def.import(&module); + let alias = import_from_def.alias(&module); + + // For `ImportFrom`, we need to resolve the original imported symbol name + // (alias.name), not the local alias (symbol_name) + resolve_from_import_definitions(db, file, import_node, &alias.name, visited) + } + + // For star imports, try to resolve to the specific symbol being accessed + DefinitionKind::StarImport(star_import_def) => { + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let import_node = star_import_def.import(&module); + + // If we have a symbol name, use the helper to resolve it in the target module + if let Some(symbol_name) = symbol_name { + resolve_from_import_definitions(db, file, import_node, symbol_name, visited) + } else { + // No symbol context provided, can't resolve star import + Vec::new() + } + } + + // For non-import definitions, return the definition as is + _ => vec![ResolvedDefinition::Definition(definition)], + } + } + + /// Helper function to resolve import definitions for `ImportFrom` and `StarImport` cases. + fn resolve_from_import_definitions<'db>( + db: &'db dyn Db, + file: File, + import_node: &ast::StmtImportFrom, + symbol_name: &str, + visited: &mut FxHashSet>, + ) -> Vec> { + // Resolve the target module file + let module_file = { + // Resolve the module being imported from (handles both relative and absolute imports) + let Some(module_name) = ModuleName::from_import_statement(db, file, import_node).ok() + else { + return Vec::new(); + }; + let Some(resolved_module) = resolve_module(db, &module_name) else { + return Vec::new(); + }; + resolved_module.file() + }; + + let Some(module_file) = module_file else { + return Vec::new(); // Module resolution failed + }; + + // Find the definition of this symbol in the imported module's global scope + let global_scope = global_scope(db, module_file); + let definitions_in_module = find_symbol_in_scope(db, global_scope, symbol_name); + + // Recursively resolve any import definitions found in the target module + if definitions_in_module.is_empty() { + // If we can't find the specific symbol, return empty list + Vec::new() + } else { + let mut resolved_definitions = Vec::new(); + for def in definitions_in_module { + let resolved = resolve_definition_recursive(db, def, visited, Some(symbol_name)); + resolved_definitions.extend(resolved); + } + resolved_definitions + } + } + + /// Find definitions for a symbol name in a specific scope. + pub(crate) fn find_symbol_in_scope<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + symbol_name: &str, + ) -> Vec> { + let place_table = place_table(db, scope); + let Some(place_id) = place_table.place_id_by_name(symbol_name) else { + return Vec::new(); + }; + + let use_def_map = use_def_map(db, scope); + let mut definitions = Vec::new(); + + // Get all definitions (both bindings and declarations) for this place + let bindings = use_def_map.all_reachable_bindings(place_id); + let declarations = use_def_map.all_reachable_declarations(place_id); + + for binding in bindings { + if let Some(def) = binding.binding.definition() { + definitions.push(def); + } + } + + for declaration in declarations { + if let Some(def) = declaration.declaration.definition() { + definitions.push(def); + } + } + + definitions + } +} + +pub use resolve_definition::ResolvedDefinition; +use resolve_definition::{find_symbol_in_scope, resolve_definition}; diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 298c85674f..cea779f9ff 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -5,9 +5,9 @@ use crate::PositionEncoding; use crate::session::{AllOptions, ClientOptions, DiagnosticMode, Session}; use lsp_server::Connection; use lsp_types::{ - ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, - InitializeParams, InlayHintOptions, InlayHintServerCapabilities, MessageType, - SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities, + ClientCapabilities, DeclarationCapability, DiagnosticOptions, DiagnosticServerCapabilities, + HoverProviderCapability, InitializeParams, InlayHintOptions, InlayHintServerCapabilities, + MessageType, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions, }; @@ -194,6 +194,8 @@ impl Server { }, )), type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), + definition_provider: Some(lsp_types::OneOf::Left(true)), + declaration_provider: Some(DeclarationCapability::Simple(true)), hover_provider: Some(HoverProviderCapability::Simple(true)), signature_help_provider: Some(SignatureHelpOptions { trigger_characters: Some(vec!["(".to_string(), ",".to_string()]), diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index b9dd261223..a07367ad6b 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -44,6 +44,14 @@ pub(super) fn request(req: server::Request) -> Task { >( req, BackgroundSchedule::Worker ), + requests::GotoDeclarationRequestHandler::METHOD => background_document_request_task::< + requests::GotoDeclarationRequestHandler, + >( + req, BackgroundSchedule::Worker + ), + requests::GotoDefinitionRequestHandler::METHOD => background_document_request_task::< + requests::GotoDefinitionRequestHandler, + >(req, BackgroundSchedule::Worker), requests::HoverRequestHandler::METHOD => background_document_request_task::< requests::HoverRequestHandler, >(req, BackgroundSchedule::Worker), diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 8c0278d573..89a3eff615 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -1,5 +1,7 @@ mod completion; mod diagnostic; +mod goto_declaration; +mod goto_definition; mod goto_type_definition; mod hover; mod inlay_hints; @@ -11,6 +13,8 @@ mod workspace_diagnostic; pub(super) use completion::CompletionRequestHandler; pub(super) use diagnostic::DocumentDiagnosticRequestHandler; +pub(super) use goto_declaration::GotoDeclarationRequestHandler; +pub(super) use goto_definition::GotoDefinitionRequestHandler; pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; pub(super) use hover::HoverRequestHandler; pub(super) use inlay_hints::InlayHintRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/goto_declaration.rs b/crates/ty_server/src/server/api/requests/goto_declaration.rs new file mode 100644 index 0000000000..9ab17a4182 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/goto_declaration.rs @@ -0,0 +1,75 @@ +use std::borrow::Cow; + +use lsp_types::request::{GotoDeclaration, GotoDeclarationParams}; +use lsp_types::{GotoDefinitionResponse, Url}; +use ruff_db::source::{line_index, source_text}; +use ty_ide::goto_declaration; +use ty_project::ProjectDatabase; + +use crate::document::{PositionExt, ToLink}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::DocumentSnapshot; +use crate::session::client::Client; + +pub(crate) struct GotoDeclarationRequestHandler; + +impl RequestHandler for GotoDeclarationRequestHandler { + type RequestType = GotoDeclaration; +} + +impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler { + fn document_url(params: &GotoDeclarationParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: GotoDeclarationParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + return Ok(None); + }; + + let source = source_text(db, file); + let line_index = line_index(db, file); + let offset = params.text_document_position_params.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + + let Some(ranged) = goto_declaration(db, file, offset) else { + return Ok(None); + }; + + if snapshot + .resolved_client_capabilities() + .type_definition_link_support + { + let src = Some(ranged.range); + let links: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_link(db, src, snapshot.encoding())) + .collect(); + + Ok(Some(GotoDefinitionResponse::Link(links))) + } else { + let locations: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_location(db, snapshot.encoding())) + .collect(); + + Ok(Some(GotoDefinitionResponse::Array(locations))) + } + } +} + +impl RetriableRequestHandler for GotoDeclarationRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/goto_definition.rs b/crates/ty_server/src/server/api/requests/goto_definition.rs new file mode 100644 index 0000000000..fe888b2e71 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/goto_definition.rs @@ -0,0 +1,75 @@ +use std::borrow::Cow; + +use lsp_types::request::GotoDefinition; +use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Url}; +use ruff_db::source::{line_index, source_text}; +use ty_ide::goto_definition; +use ty_project::ProjectDatabase; + +use crate::document::{PositionExt, ToLink}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::DocumentSnapshot; +use crate::session::client::Client; + +pub(crate) struct GotoDefinitionRequestHandler; + +impl RequestHandler for GotoDefinitionRequestHandler { + type RequestType = GotoDefinition; +} + +impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler { + fn document_url(params: &GotoDefinitionParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: GotoDefinitionParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + return Ok(None); + }; + + let source = source_text(db, file); + let line_index = line_index(db, file); + let offset = params.text_document_position_params.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + + let Some(ranged) = goto_definition(db, file, offset) else { + return Ok(None); + }; + + if snapshot + .resolved_client_capabilities() + .type_definition_link_support + { + let src = Some(ranged.range); + let links: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_link(db, src, snapshot.encoding())) + .collect(); + + Ok(Some(GotoDefinitionResponse::Link(links))) + } else { + let locations: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_location(db, snapshot.encoding())) + .collect(); + + Ok(Some(GotoDefinitionResponse::Array(locations))) + } + } +} + +impl RetriableRequestHandler for GotoDefinitionRequestHandler {}