Inlay hints fix (#896)

* Fix issue displaying enums incorrectly

* Make inlay hints retrigger when the LSP connects

* Add "doubleclick to insert" to inlay hints
This commit is contained in:
David Kincaid
2025-08-02 10:19:50 -04:00
committed by GitHub
parent 37bb1116fb
commit fed2a2edab
3 changed files with 80 additions and 53 deletions

View File

@@ -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<ManagerStatus>();
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();
}

View File

@@ -1 +1 @@
export { ClientConnectionManager } from "./ClientConnectionManager";
export { ClientConnectionManager, ManagerStatus } from "./ClientConnectionManager";

View File

@@ -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<InlayHint | undefined> {
const response = await globals.lsp.client.send_request<HoverResponse>("textDocument/hover", {
async function addByHover(document: TextDocument, hoverPosition: vscode.Position): Promise<string | undefined> {
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<void>();
get onDidChangeInlayHints(): Event<void> {
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<InlayHint[]> {
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<DocumentSymbol[]>(
"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;
}