diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index b4937ca438..4533175828 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -14,6 +14,7 @@ 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_project::ProjectMetadata; use ty_project::metadata::options::Options; @@ -385,6 +386,51 @@ impl Workspace { Ok(result) } + + #[wasm_bindgen(js_name = "signatureHelp")] + pub fn signature_help( + &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(signature_help_info) = signature_help(&self.db, file_id.file, offset) else { + return Ok(None); + }; + + let signatures = signature_help_info + .signatures + .into_iter() + .map(|sig| { + let parameters = sig + .parameters + .into_iter() + .map(|param| ParameterInformation { + label: param.label, + documentation: param.documentation, + }) + .collect(); + + SignatureInformation { + label: sig.label, + documentation: sig.documentation, + parameters, + active_parameter: sig.active_parameter.and_then(|p| u32::try_from(p).ok()), + } + }) + .collect(); + + Ok(Some(SignatureHelp { + signatures, + active_signature: signature_help_info + .active_signature + .and_then(|s| u32::try_from(s).ok()), + })) + } } pub(crate) fn into_error(err: E) -> Error { @@ -749,6 +795,35 @@ pub struct SemanticToken { pub range: Range, } +#[wasm_bindgen] +#[derive(Clone)] +pub struct SignatureHelp { + #[wasm_bindgen(getter_with_clone)] + pub signatures: Vec, + pub active_signature: Option, +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct SignatureInformation { + #[wasm_bindgen(getter_with_clone)] + pub label: String, + #[wasm_bindgen(getter_with_clone)] + pub documentation: Option, + #[wasm_bindgen(getter_with_clone)] + pub parameters: Vec, + pub active_parameter: Option, +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct ParameterInformation { + #[wasm_bindgen(getter_with_clone)] + pub label: String, + #[wasm_bindgen(getter_with_clone)] + pub documentation: Option, +} + #[wasm_bindgen] impl SemanticToken { pub fn kinds() -> Vec { diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index b6be4ff602..0fe8d5149a 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -152,7 +152,8 @@ class PlaygroundServer languages.DocumentFormattingEditProvider, languages.CompletionItemProvider, languages.DocumentSemanticTokensProvider, - languages.DocumentRangeSemanticTokensProvider + languages.DocumentRangeSemanticTokensProvider, + languages.SignatureHelpProvider { private typeDefinitionProviderDisposable: IDisposable; private editorOpenerDisposable: IDisposable; @@ -162,6 +163,7 @@ class PlaygroundServer private completionDisposable: IDisposable; private semanticTokensDisposable: IDisposable; private rangeSemanticTokensDisposable: IDisposable; + private signatureHelpDisposable: IDisposable; constructor( private monaco: Monaco, @@ -191,9 +193,13 @@ class PlaygroundServer this.editorOpenerDisposable = monaco.editor.registerEditorOpener(this); this.formatDisposable = monaco.languages.registerDocumentFormattingEditProvider("python", this); + this.signatureHelpDisposable = + monaco.languages.registerSignatureHelpProvider("python", this); } triggerCharacters: string[] = ["."]; + signatureHelpTriggerCharacters: string[] = ["(", ","]; + signatureHelpRetriggerCharacters: string[] = [")"]; getLegend(): languages.SemanticTokensLegend { return { @@ -292,6 +298,61 @@ class PlaygroundServer resolveCompletionItem: undefined; + provideSignatureHelp( + model: editor.ITextModel, + position: Position, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _token: CancellationToken, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _context: languages.SignatureHelpContext, + ): languages.ProviderResult { + const selectedFile = this.props.files.selected; + + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const signatureHelp = this.props.workspace.signatureHelp( + selectedHandle, + new TyPosition(position.lineNumber, position.column), + ); + + if (signatureHelp == null) { + return undefined; + } + + return { + dispose() {}, + value: { + signatures: signatureHelp.signatures.map((sig) => ({ + label: sig.label, + documentation: sig.documentation + ? { value: sig.documentation } + : undefined, + parameters: sig.parameters.map((param) => ({ + label: param.label, + documentation: param.documentation + ? { value: param.documentation } + : undefined, + })), + activeParameter: sig.active_parameter, + })), + activeSignature: signatureHelp.active_signature ?? 0, + activeParameter: + signatureHelp.active_signature != null + ? (signatureHelp.signatures[signatureHelp.active_signature] + ?.active_parameter ?? 0) + : 0, + }, + }; + } + provideInlayHints( _model: editor.ITextModel, range: Range, @@ -569,6 +630,7 @@ class PlaygroundServer this.rangeSemanticTokensDisposable.dispose(); this.semanticTokensDisposable.dispose(); this.completionDisposable.dispose(); + this.signatureHelpDisposable.dispose(); } }