From 4d00f9f41a173ad27b9516c66e865dfa370801a4 Mon Sep 17 00:00:00 2001 From: anthonyme00 <30407083+anthonyme00@users.noreply.github.com> Date: Sun, 27 Apr 2025 03:39:46 +0700 Subject: [PATCH] Add support for uid:// references to hovers and document links (#841) --- src/lsp/GDScriptLanguageClient.ts | 75 ++++++++++++++++++++++++++++--- src/providers/document_link.ts | 18 +++++++- src/providers/hover.ts | 18 +++++++- src/utils/godot_utils.ts | 63 ++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 8 deletions(-) diff --git a/src/lsp/GDScriptLanguageClient.ts b/src/lsp/GDScriptLanguageClient.ts index ef2b35d..6a0ac11 100644 --- a/src/lsp/GDScriptLanguageClient.ts +++ b/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 }, diff --git a/src/providers/document_link.ts b/src/providers/document_link.ts index 026462f..65739eb 100644 --- a/src/providers/document_link.ts +++ b/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 = 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; } diff --git a/src/providers/hover.ts b/src/providers/hover.ts index f2ae461..fb83e3e 100644 --- a/src/providers/hover.ts +++ b/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")) { diff --git a/src/utils/godot_utils.ts b/src/utils/godot_utils.ts index 2083763..05b2065 100644 --- a/src/utils/godot_utils.ts +++ b/src/utils/godot_utils.ts @@ -127,6 +127,69 @@ export async function convert_uri_to_resource_path(uri: vscode.Uri): Promise = new Map(); + +export async function convert_uids_to_uris(uids: string[]): Promise> { + const not_found_uids: string[] = []; + const uris: Map = 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 { + 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;