ソースを参照

Inlay hints fix (#896)

* Fix issue displaying enums incorrectly

* Make inlay hints retrigger when the LSP connects

* Add "doubleclick to insert" to inlay hints
David Kincaid 4 日 前
コミット
fed2a2edab
3 ファイル変更80 行追加53 行削除
  1. 9 2
      src/lsp/ClientConnectionManager.ts
  2. 1 1
      src/lsp/index.ts
  3. 70 50
      src/providers/inlay_hints.ts

+ 9 - 2
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<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();
 	}
 

+ 1 - 1
src/lsp/index.ts

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

+ 70 - 50
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} `;
-}
 
-interface HoverResponse {
-	contents;
+	let label = labelMatch ? labelMatch[1] : "unknown";
+	// fix when detail includes a script name
+	if (label.includes(".gd.")) {
+		label = label.split(".gd.")[1];
+	}
+	return `${label}`;
 }
 
-async function addByHover(
-	document: TextDocument,
-	hoverPosition: Position,
-	start: Position,
-): Promise<InlayHint | undefined> {
-	const response = await globals.lsp.client.send_request<HoverResponse>("textDocument/hover", {
+type HoverResult = {
+	contents: {
+		kind: string;
+		value: string;
+	};
+};
+
+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;
 		}