Преглед на файлове

Add support for uid:// references to hovers and document links (#841)

anthonyme00 преди 4 месеца
родител
ревизия
4d00f9f41a
променени са 4 файла, в които са добавени 166 реда и са изтрити 8 реда
  1. 70 5
      src/lsp/GDScriptLanguageClient.ts
  2. 17 1
      src/providers/document_link.ts
  3. 16 2
      src/providers/hover.ts
  4. 63 0
      src/utils/godot_utils.ts

+ 70 - 5
src/lsp/GDScriptLanguageClient.ts

@@ -65,6 +65,26 @@ type ChangeWorkspaceNotification = {
 	};
 };
 
+type DocumentLinkResult = {
+	range: {
+		end: {
+			character: number;
+			line: number;
+		};
+		start: {
+			character: number;
+			line: number;
+		};
+	};
+	target: string;
+};
+
+type DocumentLinkResponseMessage = {
+	id: number;
+	jsonrpc: string;
+	result: DocumentLinkResult[];
+};
+
 export default class GDScriptLanguageClient extends LanguageClient {
 	public io: MessageIO = new MessageIO();
 
@@ -148,12 +168,28 @@ export default class GDScriptLanguageClient extends LanguageClient {
 		showNotification?: boolean,
 	): T {
 		if (type.method === "textDocument/documentSymbol") {
-			if (error.message.includes("selectionRange must be contained in fullRange")) {
-				log.warn(`Request failed for method "${type.method}", suppressing notification - see issue #820`);
-				return super.handleFailedRequest(type, token, error, defaultValue, false);
+			if (
+				error.message.includes("selectionRange must be contained in fullRange")
+			) {
+				log.warn(
+					`Request failed for method "${type.method}", suppressing notification - see issue #820`
+				);
+				return super.handleFailedRequest(
+					type,
+					token,
+					error,
+					defaultValue,
+					false
+				);
 			}
 		}
-		return super.handleFailedRequest(type, token, error, defaultValue, showNotification);
+		return super.handleFailedRequest(
+			type,
+			token,
+			error,
+			defaultValue,
+			showNotification
+		);
 	}
 
 	private request_filter(message: RequestMessage) {
@@ -209,6 +245,32 @@ export default class GDScriptLanguageClient extends LanguageClient {
 
 				(message as HoverResponseMesssage).result.contents.value = value;
 			}
+		} else if (sentMessage.method === "textDocument/documentLink") {
+			const results: DocumentLinkResult[] = (
+				message as DocumentLinkResponseMessage
+			).result;
+
+			if (!results) {
+				return message;
+			}
+
+			const final_result: DocumentLinkResult[] = [];
+			// at this point, Godot's LSP server does not
+			// return a valid path for resources identified
+			// by "uid://""
+			//
+			// this is a dirty hack to remove any "uid://"
+			// document links.
+			//
+			// to provide links for these, we will be relying on
+			// the internal DocumentLinkProvider instead.
+			for (const result of results) {
+				if (!result.target.startsWith("uid://")) {
+					final_result.push(result);
+				}
+			}
+
+			(message as DocumentLinkResponseMessage).result = final_result;
 		}
 
 		return message;
@@ -248,7 +310,10 @@ export default class GDScriptLanguageClient extends LanguageClient {
 		return message;
 	}
 
-	public async get_symbol_at_position(uri: vscode.Uri, position: vscode.Position) {
+	public async get_symbol_at_position(
+		uri: vscode.Uri,
+		position: vscode.Position
+	) {
 		const params = {
 			textDocument: { uri: uri.toString() },
 			position: { line: position.line, character: position.character },

+ 17 - 1
src/providers/document_link.ts

@@ -9,7 +9,7 @@ import {
 	type ExtensionContext,
 } from "vscode";
 import { SceneParser } from "../scene_tools";
-import { convert_resource_path_to_uri, createLogger } from "../utils";
+import { convert_resource_path_to_uri, convert_uids_to_uris, createLogger } from "../utils";
 
 const log = createLogger("providers.document_links");
 
@@ -70,6 +70,22 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
 			}
 		}
 
+		const uids: Set<string> = new Set();
+		const uid_matches: Array<[string, Range]> = [];
+		for (const match of text.matchAll(/uid:\/\/([0-9a-z]*)/g)) {
+			const r = this.create_range(document, match);
+			uids.add(match[0]);
+			uid_matches.push([match[0], r]);
+		}
+
+		const uid_map = await convert_uids_to_uris(Array.from(uids));
+		for (const uid of uid_matches) {
+			const uri = uid_map.get(uid[0]);
+			if (uri instanceof vscode.Uri) {
+				links.push(new DocumentLink(uid[1], uri));
+			}
+		}
+
 		return links;
 	}
 

+ 16 - 2
src/providers/hover.ts

@@ -10,7 +10,7 @@ import {
 	Hover,
 } from "vscode";
 import { SceneParser } from "../scene_tools";
-import { convert_resource_path_to_uri, createLogger } from "../utils";
+import { convert_resource_path_to_uri, createLogger, convert_uid_to_uri, convert_uri_to_resource_path } from "../utils";
 
 const log = createLogger("providers.hover");
 
@@ -36,6 +36,12 @@ export class GDHoverProvider implements HoverProvider {
 				links += `* [${match[0]}](${uri})\n`;
 			}
 		}
+		for (const match of text.matchAll(/uid:\/\/[0-9a-z]*/g)) {
+			const uri = await convert_uid_to_uri(match[0]);
+			if (uri instanceof Uri) {
+				links += `* [${match[0]}](${uri})\n`;
+			}
+		}
 		return links;
 	}
 
@@ -88,7 +94,15 @@ export class GDHoverProvider implements HoverProvider {
 			}
 		}
 
-		const link = document.getText(document.getWordRangeAtPosition(position, /res:\/\/[^"^']*/));
+		let link = document.getText(document.getWordRangeAtPosition(position, /res:\/\/[^"^']*/));
+		if (!link.startsWith("res://")) {
+			link = document.getText(document.getWordRangeAtPosition(position, /uid:\/\/[0-9a-z]*/));
+			if (link.startsWith("uid://")) {
+				const uri = await convert_uid_to_uri(link);
+				link = await convert_uri_to_resource_path(uri);
+			}
+		}
+
 		if (link.startsWith("res://")) {
 			let type = "";
 			if (link.endsWith(".gd")) {

+ 63 - 0
src/utils/godot_utils.ts

@@ -127,6 +127,69 @@ export async function convert_uri_to_resource_path(uri: vscode.Uri): Promise<str
 	return `res://${relative_path}`;
 }
 
+const uidCache: Map<string, vscode.Uri | null> = new Map();
+
+export async function convert_uids_to_uris(uids: string[]): Promise<Map<string, vscode.Uri>> {
+	const not_found_uids: string[] = [];
+	const uris: Map<string, vscode.Uri> = new Map();
+
+	let found_all: boolean = true;
+	for (const uid of uids) {
+		if (!uid.startsWith("uid://")) {
+			continue;
+		}
+
+		if (uidCache.has(uid)) {
+			const uri = uidCache.get(uid);
+			if (fs.existsSync(uri.fsPath)) {
+				uris.set(uid, uri);
+				continue;
+			}
+
+			uidCache.delete(uid);
+		}
+
+		found_all = false;
+		not_found_uids.push(uid);
+	}
+
+	if (found_all) {
+		return uris;
+	}
+
+	const files = await vscode.workspace.findFiles("**/*.uid", null);
+
+	for (const file of files) {
+		const document = await vscode.workspace.openTextDocument(file);
+		const text = document.getText();
+		const match = text.match(/uid:\/\/([0-9a-z]*)/);
+		if (!match) {
+			continue;
+		}
+
+		const found_match = not_found_uids.indexOf(match[0]) >= 0;
+
+		const file_path = file.fsPath.substring(0, file.fsPath.length - ".uid".length);
+		if (!fs.existsSync(file_path)) {
+			continue;
+		}
+
+		const file_uri = vscode.Uri.file(file_path);
+		uidCache.set(match[0], file_uri);
+
+		if (found_match) {
+			uris.set(match[0], file_uri);
+		}
+	}
+
+	return uris;
+}
+
+export async function convert_uid_to_uri(uid: string): Promise<vscode.Uri | undefined> {
+	const uris = await convert_uids_to_uris([uid]);
+	return uris.get(uid);
+}
+
 export type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
 export type VERIFY_RESULT = {
 	status: VERIFY_STATUS;