diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 33915731e797b9..98381171283b47 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -1,12 +1,13 @@ use crate::find_node::covering_node; -use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; +use crate::{Db, HasNavigationTargets, NavigationTarget, NavigationTargets, RangedValue}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_semantic::semantic_index::definition::Definition; use ty_python_semantic::types::Type; -use ty_python_semantic::{HasType, SemanticModel}; +use ty_python_semantic::{HasDefinition, HasType, SemanticModel}; pub fn goto_type_definition( db: &dyn Db, @@ -29,6 +30,35 @@ pub fn goto_type_definition( }) } +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)?; + + let model = SemanticModel::new(db, file); + let definitions = goto_target.definitions(&model)?; + + tracing::debug!("Definitions of covering node is found"); + + let targets = definitions.into_iter().map(|definition| { + let full_range = definition.full_range(db, &module); + + NavigationTarget { + file: full_range.file(), + focus_range: definition.focus_range(db, &module).range(), + full_range: full_range.range(), + } + }); + + Some(RangedValue { + range: FileRange::new(file, goto_target.range()), + value: NavigationTargets::unique(targets), + }) +} + #[derive(Clone, Copy, Debug)] pub(crate) enum GotoTarget<'a> { Expression(ast::ExprRef<'a>), @@ -154,6 +184,16 @@ impl GotoTarget<'_> { Some(ty) } + + pub(crate) fn definitions<'db>( + self, + model: &SemanticModel<'db>, + ) -> Option>> { + match self { + GotoTarget::Expression(expr_ref) => expr_ref.definitions(model), + _ => None, + } + } } impl Ranged for GotoTarget<'_> { @@ -254,7 +294,7 @@ pub(crate) fn find_goto_target( #[cfg(test)] mod tests { use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; - use crate::{NavigationTarget, goto_type_definition}; + use crate::{NavigationTarget, goto_definition, goto_type_definition}; use insta::assert_snapshot; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, @@ -828,6 +868,516 @@ f(**kwargs) "); } + #[test] + fn goto_def_function_call() { + let test = cursor_test( + r#" + def ab(a, b): ... + + ab(1, 2) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:17 + | + 2 | def ab(a, b): ... + | ^^ + 3 | + 4 | ab(1, 2) + | + info: Source + --> main.py:4:13 + | + 2 | def ab(a, b): ... + 3 | + 4 | ab(1, 2) + | ^^ + | + "); + } + + #[test] + fn goto_def_local_load() { + let test = cursor_test( + r#" + ab = 1 + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:13 + | + 2 | ab = 1 + | ^^ + 3 | print(ab) + | + info: Source + --> main.py:3:19 + | + 2 | ab = 1 + 3 | print(ab) + | ^^ + | + "); + } + + #[test] + fn goto_def_local_load_rebind() { + let test = cursor_test( + r#" + ab = 1 + ab = 2 + ab = 3 + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:4:13 + | + 2 | ab = 1 + 3 | ab = 2 + 4 | ab = 3 + | ^^ + 5 | print(ab) + | + info: Source + --> main.py:5:19 + | + 3 | ab = 2 + 4 | ab = 3 + 5 | print(ab) + | ^^ + | + "); + } + + #[test] + fn goto_def_local_load_cond_rebind() { + let test = cursor_test( + r#" + ab = 1 + if cond: + ab = 2 + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:13 + | + 2 | ab = 1 + | ^^ + 3 | if cond: + 4 | ab = 2 + | + info: Source + --> main.py:5:19 + | + 3 | if cond: + 4 | ab = 2 + 5 | print(ab) + | ^^ + | + + info[goto-type-definition]: Type definition + --> main.py:4:17 + | + 2 | ab = 1 + 3 | if cond: + 4 | ab = 2 + | ^^ + 5 | print(ab) + | + info: Source + --> main.py:5:19 + | + 3 | if cond: + 4 | ab = 2 + 5 | print(ab) + | ^^ + | + "); + } + + #[test] + fn goto_def_local_load_exhaustive_bind() { + let test = cursor_test( + r#" + if cond: + ab = 2 + else: + ab = 1 + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:3:17 + | + 2 | if cond: + 3 | ab = 2 + | ^^ + 4 | else: + 5 | ab = 1 + | + info: Source + --> main.py:6:19 + | + 4 | else: + 5 | ab = 1 + 6 | print(ab) + | ^^ + | + + info[goto-type-definition]: Type definition + --> main.py:5:17 + | + 3 | ab = 2 + 4 | else: + 5 | ab = 1 + | ^^ + 6 | print(ab) + | + info: Source + --> main.py:6:19 + | + 4 | else: + 5 | ab = 1 + 6 | print(ab) + | ^^ + | + "); + } + + #[test] + fn goto_def_local_load_only_decl() { + let test = cursor_test( + r#" + ab: int + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No definitions found"); + } + + #[test] + fn goto_def_local_load_exhaustive_bind_decl() { + let test = cursor_test( + r#" + ab: int + if cond: + ab = 2 + else: + ab = 1 + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:4:17 + | + 2 | ab: int + 3 | if cond: + 4 | ab = 2 + | ^^ + 5 | else: + 6 | ab = 1 + | + info: Source + --> main.py:7:19 + | + 5 | else: + 6 | ab = 1 + 7 | print(ab) + | ^^ + | + + info[goto-type-definition]: Type definition + --> main.py:6:17 + | + 4 | ab = 2 + 5 | else: + 6 | ab = 1 + | ^^ + 7 | print(ab) + | + info: Source + --> main.py:7:19 + | + 5 | else: + 6 | ab = 1 + 7 | print(ab) + | ^^ + | + "); + } + + #[test] + fn goto_def_local_load_bind_decl() { + let test = cursor_test( + r#" + ab: int + ab = 1 + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:3:13 + | + 2 | ab: int + 3 | ab = 1 + | ^^ + 4 | print(ab) + | + info: Source + --> main.py:4:19 + | + 2 | ab: int + 3 | ab = 1 + 4 | print(ab) + | ^^ + | + "); + } + + #[test] + fn goto_def_local_first_store() { + let test = cursor_test( + r#" + ab = 1 + print(ab) + ab = 2 + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_def_local_second_store() { + let test = cursor_test( + r#" + ab = 1 + print(ab) + ab = 2 + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_def_local_loadstore() { + let test = cursor_test( + r#" + ab = 1 + print(ab) + ab += 2 + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_def_class() { + let test = cursor_test( + r#" + class AB: + def __init__(self, val: int): + self.myval = val + + x = AB(5) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:19 + | + 2 | class AB: + | ^^ + 3 | def __init__(self, val: int): + 4 | self.myval = val + | + info: Source + --> main.py:6:17 + | + 4 | self.myval = val + 5 | + 6 | x = AB(5) + | ^^ + | + "); + } + + #[test] + fn goto_def_class_implicit_instance_variable() { + let test = cursor_test( + r#" + class AB: + def __init__(self, val: int): + self.myval = val + + x = AB(5) + print(x.myval) + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_def_class_explicit_instance_variable() { + let test = cursor_test( + r#" + class AB: + myval: int + def __init__(self, val: int): + self.myval = val + + x = AB(5) + print(x.myval) + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_def_path_parent() { + let test = cursor_test( + r#" + class AB: + def __init__(self, val: int): + self.myval = val + + xyz = AB(5) + print(xyz.myval) + "#, + ); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:6:13 + | + 4 | self.myval = val + 5 | + 6 | xyz = AB(5) + | ^^^ + 7 | print(xyz.myval) + | + info: Source + --> main.py:7:19 + | + 6 | xyz = AB(5) + 7 | print(xyz.myval) + | ^^^ + | + "); + } + + #[test] + fn goto_def_class_class_variable() { + let test = cursor_test( + r#" + class AB: + RED = "red" + BLUE = "blue" + + x = AB.RED + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_def_class_path_parent() { + let test = cursor_test( + r#" + class AB: + RED = "red" + BLUE = "blue" + + x = AB.RED + "#, + ); + + assert_snapshot!(test.goto_definition(), @r#" + info[goto-type-definition]: Type definition + --> main.py:2:19 + | + 2 | class AB: + | ^^ + 3 | RED = "red" + 4 | BLUE = "blue" + | + info: Source + --> main.py:6:17 + | + 4 | BLUE = "blue" + 5 | + 6 | x = AB.RED + | ^^ + | + "#); + } + + #[test] + fn goto_def_global_decl() { + let test = cursor_test( + r#" + ab = 1 + def myfunc(): + global ab + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + + #[test] + fn goto_def_global_load() { + let test = cursor_test( + r#" + ab = 1 + def myfunc(): + global ab + print(ab) + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No definitions found"); + } + + #[test] + fn goto_def_global_store() { + let test = cursor_test( + r#" + ab = 1 + def myfunc(): + global ab + ab = 2 + "#, + ); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + impl CursorTest { fn goto_type_definition(&self) -> String { let Some(targets) = @@ -847,6 +1397,24 @@ f(**kwargs) .map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)), ) } + + fn goto_definition(&self) -> String { + let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset) + else { + return "No goto target found".to_string(); + }; + + if targets.is_empty() { + return "No definitions found".to_string(); + } + + let source = targets.range; + self.render_diagnostics( + targets + .into_iter() + .map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)), + ) + } } struct GotoTypeDefinitionDiagnostic { diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 2080c1ca788180..5d859e2fc31190 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -8,7 +8,7 @@ mod markup; pub use completion::completion; pub use db::Db; -pub use goto::goto_type_definition; +pub use goto::{goto_definition, goto_type_definition}; pub use hover::hover; pub use inlay_hints::inlay_hints; pub use markup::MarkupKind; diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index fe8305e77ee158..32c29b0fb91407 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -15,7 +15,7 @@ pub use program::{ PythonVersionWithSource, SearchPathSettings, }; pub use python_platform::PythonPlatform; -pub use semantic_model::{Completion, HasType, NameKind, SemanticModel}; +pub use semantic_model::{Completion, HasDefinition, HasType, NameKind, SemanticModel}; pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic; diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 283549c821d7ad..d2986ef6458dc0 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -1,12 +1,14 @@ use ruff_db::files::{File, FilePath}; use ruff_db::source::line_index; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast, ExprContext}; use ruff_python_ast::{Expr, ExprRef, name::Name}; use ruff_source_file::LineIndex; use crate::Db; use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, Module, resolve_module}; +use crate::semantic_index::ast_ids::HasScopedUseId; +use crate::semantic_index::definition::Definition; use crate::semantic_index::place::FileScopeId; use crate::semantic_index::semantic_index; use crate::types::ide_support::all_declarations_and_bindings; @@ -175,6 +177,41 @@ pub struct Completion { pub builtin: bool, } +pub trait HasDefinition { + /// Returns the definitions of `self`. + /// + /// ## Panics + /// May panic if `self` is from another file than `model`. + fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option>>; +} + +impl HasDefinition for ast::ExprRef<'_> { + fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option>> { + match self { + ExprRef::Name(name) => match name.ctx { + ExprContext::Load => { + let index = semantic_index(model.db, model.file); + let file_scope = index.expression_scope_id(*self); + let scope = file_scope.to_scope_id(model.db, model.file); + let use_def = index.use_def_map(file_scope); + let use_id = self.scoped_use_id(model.db, scope); + + Some( + use_def + .bindings_at_use(use_id) + .filter_map(|binding| binding.binding.definition()) + .collect(), + ) + } + ExprContext::Store => None, + ExprContext::Del => None, + ExprContext::Invalid => None, + }, + _ => None, + } + } +} + pub trait HasType { /// Returns the inferred type of `self`. /// diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 01338053fa5656..420e716ec479b6 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -6,7 +6,7 @@ use crate::session::{AllOptions, ClientOptions, Session}; use lsp_server::Connection; use lsp_types::{ ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, - InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities, + InlayHintOptions, InlayHintServerCapabilities, MessageType, OneOf, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, }; @@ -183,6 +183,7 @@ impl Server { ..Default::default() }, )), + definition_provider: Some(OneOf::Left(true)), type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), hover_provider: Some(HoverProviderCapability::Simple(true)), inlay_hint_provider: Some(lsp_types::OneOf::Right( diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index 132476cf87f34a..8e5ed4d01a3f31 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -43,6 +43,9 @@ pub(super) fn request(req: server::Request) -> Task { >( 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 6770b0e448028a..b39764ad914081 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -1,5 +1,6 @@ mod completion; mod diagnostic; +mod goto_definition; mod goto_type_definition; mod hover; mod inlay_hints; @@ -8,6 +9,7 @@ mod workspace_diagnostic; pub(super) use completion::CompletionRequestHandler; pub(super) use diagnostic::DocumentDiagnosticRequestHandler; +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_definition.rs b/crates/ty_server/src/server/api/requests/goto_definition.rs new file mode 100644 index 00000000000000..81d1caf4e8ef15 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/goto_definition.rs @@ -0,0 +1,77 @@ +use std::borrow::Cow; + +use lsp_types::GotoDefinitionParams; +use lsp_types::request::GotoDefinition; +use lsp_types::{GotoDefinitionResponse, Url}; +use ruff_db::source::{line_index, source_text}; +use ty_ide::goto_definition; +use ty_project::ProjectDatabase; + +use crate::DocumentSnapshot; +use crate::document::{PositionExt, ToLink}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +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 { + tracing::debug!("Failed to resolve file for {:?}", params); + 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 {}