diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index f21d2ddaa4..de101ae3c0 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -4,7 +4,7 @@ use js_sys::{Error, JsString}; use ruff_db::Db as _; use ruff_db::diagnostic::{self, DisplayDiagnosticConfig}; use ruff_db::files::{File, FileRange, system_path_to_file}; -use ruff_db::source::{line_index, source_text}; +use ruff_db::source::{SourceText, line_index, source_text}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; use ruff_db::system::{ CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System, @@ -14,8 +14,11 @@ use ruff_notebook::Notebook; use ruff_python_formatter::formatted_file; use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; use ruff_text_size::{Ranged, TextSize}; -use ty_ide::signature_help; -use ty_ide::{MarkupKind, goto_type_definition, hover, inlay_hints}; +use ty_ide::{ + MarkupKind, RangedValue, goto_declaration, goto_definition, goto_type_definition, hover, + inlay_hints, +}; +use ty_ide::{NavigationTargets, signature_help}; use ty_project::metadata::options::Options; use ty_project::metadata::value::ValueSource; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; @@ -267,32 +270,61 @@ impl Workspace { return Ok(Vec::new()); }; - let source_range = Range::from_text_range( - targets.file_range().range(), - &index, + Ok(map_targets_to_links( + &self.db, + targets, &source, + &index, self.position_encoding, - ); + )) + } - let links: Vec<_> = targets - .into_iter() - .map(|target| LocationLink { - path: target.file().path(&self.db).to_string(), - full_range: Range::from_file_range( - &self.db, - FileRange::new(target.file(), target.full_range()), - self.position_encoding, - ), - selection_range: Some(Range::from_file_range( - &self.db, - FileRange::new(target.file(), target.focus_range()), - self.position_encoding, - )), - origin_selection_range: Some(source_range), - }) - .collect(); + #[wasm_bindgen(js_name = "gotoDeclaration")] + pub fn goto_declaration( + &self, + file_id: &FileHandle, + position: Position, + ) -> Result, Error> { + let source = source_text(&self.db, file_id.file); + let index = line_index(&self.db, file_id.file); - Ok(links) + let offset = position.to_text_size(&source, &index, self.position_encoding)?; + + let Some(targets) = goto_declaration(&self.db, file_id.file, offset) else { + return Ok(Vec::new()); + }; + + Ok(map_targets_to_links( + &self.db, + targets, + &source, + &index, + self.position_encoding, + )) + } + + #[wasm_bindgen(js_name = "gotoDefinition")] + pub fn goto_definition( + &self, + file_id: &FileHandle, + position: Position, + ) -> Result, Error> { + let source = source_text(&self.db, file_id.file); + let index = line_index(&self.db, file_id.file); + + let offset = position.to_text_size(&source, &index, self.position_encoding)?; + + let Some(targets) = goto_definition(&self.db, file_id.file, offset) else { + return Ok(Vec::new()); + }; + + Ok(map_targets_to_links( + &self.db, + targets, + &source, + &index, + self.position_encoding, + )) } #[wasm_bindgen] @@ -464,6 +496,39 @@ pub(crate) fn into_error(err: E) -> Error { Error::new(&err.to_string()) } +fn map_targets_to_links( + db: &dyn Db, + targets: RangedValue, + source: &SourceText, + index: &LineIndex, + position_encoding: PositionEncoding, +) -> Vec { + let source_range = Range::from_text_range( + targets.file_range().range(), + index, + source, + position_encoding, + ); + + targets + .into_iter() + .map(|target| LocationLink { + path: target.file().path(db).to_string(), + full_range: Range::from_file_range( + db, + FileRange::new(target.file(), target.full_range()), + position_encoding, + ), + selection_range: Some(Range::from_file_range( + db, + FileRange::new(target.file(), target.focus_range()), + position_encoding, + )), + origin_selection_range: Some(source_range), + }) + .collect() +} + #[derive(Debug, Eq, PartialEq)] #[wasm_bindgen(inspectable)] pub struct FileHandle { diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index 0fe8d5149a..9f1528120b 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -146,6 +146,8 @@ interface PlaygroundServerProps { class PlaygroundServer implements languages.TypeDefinitionProvider, + languages.DeclarationProvider, + languages.DefinitionProvider, editor.ICodeEditorOpener, languages.HoverProvider, languages.InlayHintsProvider, @@ -156,6 +158,8 @@ class PlaygroundServer languages.SignatureHelpProvider { private typeDefinitionProviderDisposable: IDisposable; + private declarationProviderDisposable: IDisposable; + private definitionProviderDisposable: IDisposable; private editorOpenerDisposable: IDisposable; private hoverDisposable: IDisposable; private inlayHintsDisposable: IDisposable; @@ -171,6 +175,10 @@ class PlaygroundServer ) { this.typeDefinitionProviderDisposable = monaco.languages.registerTypeDefinitionProvider("python", this); + this.declarationProviderDisposable = + monaco.languages.registerDeclarationProvider("python", this); + this.definitionProviderDisposable = + monaco.languages.registerDefinitionProvider("python", this); this.hoverDisposable = monaco.languages.registerHoverProvider( "python", this, @@ -517,29 +525,61 @@ class PlaygroundServer new TyPosition(position.lineNumber, position.column), ); - return ( - links - .map((link) => { - const targetSelection = - link.selection_range == null - ? undefined - : tyRangeToMonacoRange(link.selection_range); + return mapNavigationTargets(links); + } - const originSelection = - link.origin_selection_range == null - ? undefined - : tyRangeToMonacoRange(link.origin_selection_range); + provideDeclaration( + model: editor.ITextModel, + position: Position, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _: CancellationToken, + ): languages.ProviderResult { + const workspace = this.props.workspace; - return { - uri: Uri.parse(link.path), - range: tyRangeToMonacoRange(link.full_range), - targetSelectionRange: targetSelection, - originSelectionRange: originSelection, - } as languages.LocationLink; - }) - // Filter out vendored files because they aren't open in the editor. - .filter((link) => link.uri.scheme !== "vendored") + const selectedFile = this.props.files.selected; + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const links = workspace.gotoDeclaration( + selectedHandle, + new TyPosition(position.lineNumber, position.column), ); + + return mapNavigationTargets(links); + } + + provideDefinition( + model: editor.ITextModel, + position: Position, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _: CancellationToken, + ): languages.ProviderResult { + const workspace = this.props.workspace; + + const selectedFile = this.props.files.selected; + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const links = workspace.gotoDefinition( + selectedHandle, + new TyPosition(position.lineNumber, position.column), + ); + + return mapNavigationTargets(links); } openCodeEditor( @@ -625,6 +665,8 @@ class PlaygroundServer this.hoverDisposable.dispose(); this.editorOpenerDisposable.dispose(); this.typeDefinitionProviderDisposable.dispose(); + this.declarationProviderDisposable.dispose(); + this.definitionProviderDisposable.dispose(); this.inlayHintsDisposable.dispose(); this.formatDisposable.dispose(); this.rangeSemanticTokensDisposable.dispose(); @@ -683,6 +725,29 @@ function generateMonacoTokens( return { data: Uint32Array.from(result) }; } +function mapNavigationTargets(links: any[]): languages.LocationLink[] { + return links + .map((link) => { + const targetSelection = + link.selection_range == null + ? undefined + : tyRangeToMonacoRange(link.selection_range); + + const originSelection = + link.origin_selection_range == null + ? undefined + : tyRangeToMonacoRange(link.origin_selection_range); + + return { + uri: Uri.parse(link.path), + range: tyRangeToMonacoRange(link.full_range), + targetSelectionRange: targetSelection, + originSelectionRange: originSelection, + } as languages.LocationLink; + }) + .filter((link) => link.uri.scheme !== "vendored"); +} + function mapCompletionKind(kind: CompletionKind): CompletionItemKind { switch (kind) { case CompletionKind.Text: