From 67d94d9ec8f3d369af2e2acd8942978ea5c869d1 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 3 Jun 2025 10:11:39 +0200 Subject: [PATCH] Use ty's completions in playground (#18425) --- crates/ty_wasm/src/lib.rs | 30 +++++++++++++++ playground/package-lock.json | 2 +- playground/ty/src/Editor/Editor.tsx | 57 ++++++++++++++++++++++++++--- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 8394428526..20dde86bc6 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -293,6 +293,27 @@ impl Workspace { })) } + #[wasm_bindgen] + pub fn completions( + &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 completions = ty_ide::completion(&self.db, file_id.file, offset); + + Ok(completions + .into_iter() + .map(|completion| Completion { + label: completion.label, + }) + .collect()) + } + #[wasm_bindgen(js_name = "inlayHints")] pub fn inlay_hints(&self, file_id: &FileHandle, range: Range) -> Result, Error> { let index = line_index(&self.db, file_id.file); @@ -586,6 +607,7 @@ pub struct LocationLink { } #[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Hover { #[wasm_bindgen(getter_with_clone)] pub markdown: String, @@ -594,6 +616,14 @@ pub struct Hover { } #[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Completion { + #[wasm_bindgen(getter_with_clone)] + pub label: String, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { #[wasm_bindgen(getter_with_clone)] pub markdown: String, diff --git a/playground/package-lock.json b/playground/package-lock.json index 91d06193b1..e695815876 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -6484,7 +6484,7 @@ } }, "ruff/ruff_wasm": { - "version": "0.11.8", + "version": "0.11.10", "license": "MIT" }, "shared": { diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index 7936ce5ac8..fca03cd08b 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -18,16 +18,16 @@ import { import { useCallback, useEffect, useRef } from "react"; import { Theme } from "shared"; import { + Position as TyPosition, Range as TyRange, Severity, type Workspace, - Position as TyPosition, } from "ty_wasm"; - -import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; import { FileId, ReadonlyFiles } from "../Playground"; import { isPythonFile } from "./Files"; import { Diagnostic } from "./Diagnostics"; +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; +import CompletionItemKind = languages.CompletionItemKind; type Props = { visible: boolean; @@ -146,13 +146,15 @@ class PlaygroundServer editor.ICodeEditorOpener, languages.HoverProvider, languages.InlayHintsProvider, - languages.DocumentFormattingEditProvider + languages.DocumentFormattingEditProvider, + languages.CompletionItemProvider { private typeDefinitionProviderDisposable: IDisposable; private editorOpenerDisposable: IDisposable; private hoverDisposable: IDisposable; private inlayHintsDisposable: IDisposable; private formatDisposable: IDisposable; + private completionDisposable: IDisposable; constructor( private monaco: Monaco, @@ -168,11 +170,53 @@ class PlaygroundServer "python", this, ); + this.completionDisposable = monaco.languages.registerCompletionItemProvider( + "python", + this, + ); this.editorOpenerDisposable = monaco.editor.registerEditorOpener(this); this.formatDisposable = monaco.languages.registerDocumentFormattingEditProvider("python", this); } + triggerCharacters: undefined; + + provideCompletionItems( + model: editor.ITextModel, + position: Position, + ): languages.ProviderResult { + const selectedFile = this.props.files.selected; + + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const completions = this.props.workspace.completions( + selectedHandle, + new TyPosition(position.lineNumber, position.column), + ); + + return { + suggestions: completions.map((completion) => ({ + label: completion.label, + kind: CompletionItemKind.Variable, + insertText: completion.label, + // TODO(micha): It's unclear why this field is required for monaco but not VS Code. + // and omitting it works just fine? The LSP doesn't expose this information right now + // which is why we go with undefined for now. + range: undefined as any, + })), + }; + } + + resolveCompletionItem: undefined; + provideInlayHints( _model: editor.ITextModel, range: Range, @@ -194,7 +238,7 @@ class PlaygroundServer const inlayHints = workspace.inlayHints( selectedHandle, - MonacoRangeToTyRange(range), + monacoRangeToTyRange(range), ); if (inlayHints.length === 0) { @@ -447,6 +491,7 @@ class PlaygroundServer this.typeDefinitionProviderDisposable.dispose(); this.inlayHintsDisposable.dispose(); this.formatDisposable.dispose(); + this.editorOpenerDisposable.dispose(); } } @@ -459,7 +504,7 @@ function tyRangeToMonacoRange(range: TyRange): IRange { }; } -function MonacoRangeToTyRange(range: IRange): TyRange { +function monacoRangeToTyRange(range: IRange): TyRange { return new TyRange( new TyPosition(range.startLineNumber, range.startColumn), new TyPosition(range.endLineNumber, range.endColumn),