From fed2a2edabcad7aadf42191b88f51a44a609949f Mon Sep 17 00:00:00 2001 From: David Kincaid Date: Sat, 2 Aug 2025 10:19:50 -0400 Subject: [PATCH] Inlay hints fix (#896) * Fix issue displaying enums incorrectly * Make inlay hints retrigger when the LSP connects * Add "doubleclick to insert" to inlay hints --- src/lsp/ClientConnectionManager.ts | 11 ++- src/lsp/index.ts | 2 +- src/providers/inlay_hints.ts | 120 +++++++++++++++++------------ 3 files changed, 80 insertions(+), 53 deletions(-) diff --git a/src/lsp/ClientConnectionManager.ts b/src/lsp/ClientConnectionManager.ts index 1ff0b0b..210019e 100644 --- a/src/lsp/ClientConnectionManager.ts +++ b/src/lsp/ClientConnectionManager.ts @@ -14,10 +14,11 @@ import { import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts"; import { killSubProcesses, subProcess } from "../utils/subspawn"; import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient"; +import { EventEmitter } from "vscode"; const log = createLogger("lsp.manager", { output: "Godot LSP" }); -enum ManagerStatus { +export enum ManagerStatus { INITIALIZING = 0, INITIALIZING_LSP = 1, PENDING = 2, @@ -31,6 +32,9 @@ enum ManagerStatus { export class ClientConnectionManager { public client: GDScriptLanguageClient = null; + private statusChanged = new EventEmitter(); + onStatusChanged = this.statusChanged.event; + private reconnectionAttempts = 0; private target: TargetLSP = TargetLSP.EDITOR; @@ -70,8 +74,10 @@ export class ClientConnectionManager { } private create_new_client() { + const port = this.client?.port ?? -1; this.client?.events?.removeAllListeners(); this.client = new GDScriptLanguageClient(); + this.client.port = port; this.client.events.on("status", this.on_client_status_changed.bind(this)); } @@ -248,7 +254,7 @@ export class ClientConnectionManager { text = "$(check) Connected"; tooltip = `Connected to the GDScript language server.\n${lspTarget}`; if (this.connectedVersion) { - tooltip += `\n${this.connectedVersion}`; + tooltip += `\nGodot version: ${this.connectedVersion}`; } break; case ManagerStatus.DISCONNECTED: @@ -307,6 +313,7 @@ export class ClientConnectionManager { default: break; } + this.statusChanged.fire(this.status); this.update_status_widget(); } diff --git a/src/lsp/index.ts b/src/lsp/index.ts index dfbe3c8..f144d52 100644 --- a/src/lsp/index.ts +++ b/src/lsp/index.ts @@ -1 +1 @@ -export { ClientConnectionManager } from "./ClientConnectionManager"; +export { ClientConnectionManager, ManagerStatus } from "./ClientConnectionManager"; diff --git a/src/providers/inlay_hints.ts b/src/providers/inlay_hints.ts index 6a0b6cc..9d7f8d4 100644 --- a/src/providers/inlay_hints.ts +++ b/src/providers/inlay_hints.ts @@ -2,6 +2,8 @@ import * as vscode from "vscode"; import { CancellationToken, DocumentSymbol, + Event, + EventEmitter, ExtensionContext, InlayHint, InlayHintKind, @@ -9,8 +11,10 @@ import { Position, Range, TextDocument, + TextEdit, } from "vscode"; import { globals } from "../extension"; +import { ManagerStatus } from "../lsp"; import { SceneParser } from "../scene_tools"; import { createLogger, get_configuration } from "../utils"; @@ -21,57 +25,80 @@ const log = createLogger("providers.inlay_hints"); * E.g. `var a: int` gets parsed to ` int `. */ function fromDetail(detail: string): string { - const labelRegex = /: ([\w\d_]+)/; + const labelRegex = /: ([\w\d_.]+)/; const labelMatch = detail.match(labelRegex); - const label = labelMatch ? labelMatch[1] : "unknown"; - return ` ${label} `; + + let label = labelMatch ? labelMatch[1] : "unknown"; + // fix when detail includes a script name + if (label.includes(".gd.")) { + label = label.split(".gd.")[1]; + } + return `${label}`; } -interface HoverResponse { - contents; -} +type HoverResult = { + contents: { + kind: string; + value: string; + }; +}; -async function addByHover( - document: TextDocument, - hoverPosition: Position, - start: Position, -): Promise { - const response = await globals.lsp.client.send_request("textDocument/hover", { +async function addByHover(document: TextDocument, hoverPosition: vscode.Position): Promise { + const response = (await globals.lsp.client.send_request("textDocument/hover", { textDocument: { uri: document.uri.toString() }, position: { line: hoverPosition.line, character: hoverPosition.character, }, - }); + })) as HoverResult; // check if contents is an empty array; if it is, we have no hover information if (Array.isArray(response.contents) && response.contents.length === 0) { return undefined; } - - const label = fromDetail(response.contents.value); - const hint = new InlayHint(start, label, InlayHintKind.Type); - hint.textEdits = [{ range: new Range(start, start), newText: label }]; - return hint; + return response.contents.value; } export class GDInlayHintsProvider implements InlayHintsProvider { public parser = new SceneParser(); + private _onDidChangeInlayHints = new EventEmitter(); + get onDidChangeInlayHints(): Event { + return this._onDidChangeInlayHints.event; + } + constructor(private context: ExtensionContext) { const selector = [ { language: "gdresource", scheme: "file" }, { language: "gdscene", scheme: "file" }, { language: "gdscript", scheme: "file" }, ]; - context.subscriptions.push( - vscode.languages.registerInlayHintsProvider(selector, this), // - ); + context.subscriptions.push(vscode.languages.registerInlayHintsProvider(selector, this)); + + globals.lsp.onStatusChanged((status) => { + this._onDidChangeInlayHints.fire(); + if (status === ManagerStatus.CONNECTED) { + setTimeout(() => { + this._onDidChangeInlayHints.fire(); + }, 250); + } + }); + } + + buildHint(start: Position, detail: string): InlayHint { + const label = fromDetail(detail); + const hint = new InlayHint(start, label, InlayHintKind.Type); + hint.paddingLeft = true; + hint.paddingRight = true; + // hint.tooltip = "tooltip"; + hint.textEdits = [TextEdit.insert(start, ` ${label} `)]; + return hint; } async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise { const hints: InlayHint[] = []; const text = document.getText(range); + log.debug("Inlay Hints: provideInlayHints"); if (document.fileName.endsWith(".gd")) { if (!get_configuration("inlayHints.gdscript", true)) { @@ -79,27 +106,23 @@ export class GDInlayHintsProvider implements InlayHintsProvider { } if (!globals.lsp.client.isRunning()) { - // TODO: inlay hints need to be retriggered once lsp client becomes active return hints; } - const symbolsResponse = await globals.lsp.client.send_request( - "textDocument/documentSymbol", - { - textDocument: { uri: document.uri.toString() }, - }, - ); - log.debug(symbolsResponse); - if (symbolsResponse.length === 0) { + const symbolsRequest = (await globals.lsp.client.send_request("textDocument/documentSymbol", { + textDocument: { uri: document.uri.toString() }, + })) as DocumentSymbol[]; + + if (symbolsRequest.length === 0) { return hints; } const symbols = - typeof symbolsResponse[0] === "object" && "children" in symbolsResponse[0] - ? symbolsResponse[0].children // godot 4.0+ returns an array of children - : symbolsResponse; // godot 3.2 and below returns an array of symbols + typeof symbolsRequest[0] === "object" && "children" in symbolsRequest[0] + ? (symbolsRequest[0].children as DocumentSymbol[]) // godot 4.0+ returns an array of children + : symbolsRequest; // godot 3.2 and below returns an array of symbols - const hasDetail = symbols.some((s: any) => s.detail); + const hasDetail = symbols.some((s) => s.detail); // TODO: make sure godot reports the correct location for variable declaration symbols // (allowing the use of regex only on ranges provided by the LSP (textDocument/documentSymbol)) @@ -109,31 +132,28 @@ export class GDInlayHintsProvider implements InlayHintsProvider { const regex = /((var|const)\s+)([\w\d_]+)\s*:=/g; for (const match of text.matchAll(regex)) { - if (token.isCancellationRequested) break; + if (token.isCancellationRequested) { + break; + } // TODO: until godot supports nested document symbols, we need to send // a hover request for each variable declaration that is nested const start = document.positionAt(match.index + match[0].length - 1); - const hoverPosition = document.positionAt(match.index + match[1].length); if (hasDetail) { - const symbol = symbols.find((s: any) => s.name === match[3]); + const symbol = symbols.find((s) => s.name === match[3]); if (symbol?.detail) { - const label = fromDetail(symbol.detail); - const hint = new InlayHint(start, label); - hint.textEdits = [{ range: new Range(start, start), newText: label }]; - hints.push(hint); - } else { - const hint = await addByHover(document, hoverPosition, start); - if (hint) { - hints.push(hint); - } - } - } else { - const hint = await addByHover(document, hoverPosition, start); - if (hint) { + const hint = this.buildHint(start, symbol.detail); hints.push(hint); + continue; } } + + const hoverPosition = document.positionAt(match.index + match[1].length); + const detail = await addByHover(document, hoverPosition); + if (detail) { + const hint = this.buildHint(start, detail); + hints.push(hint); + } } return hints; }