diff --git a/src/extension.ts b/src/extension.ts index b6676d4..1dcd1d7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,6 +27,7 @@ import { get_project_dir, get_project_version, verify_godot_version, + convert_uri_to_resource_path, } from "./utils"; import { prompt_for_godot_executable } from "./utils/prompts"; import { killSubProcesses, subProcess } from "./utils/subspawn"; @@ -58,7 +59,7 @@ export function activate(context: vscode.ExtensionContext) { globals.debug = new GodotDebugger(context); globals.scenePreviewProvider = new ScenePreviewProvider(context); globals.linkProvider = new GDDocumentLinkProvider(context); - globals.dropsProvider = new GDDocumentDropEditProvider(context); + globals.dropsProvider = new GDDocumentDropEditProvider(context); globals.hoverProvider = new GDHoverProvider(context); globals.inlayProvider = new GDInlayHintsProvider(context); globals.formattingProvider = new FormattingProvider(context); @@ -122,19 +123,12 @@ export function deactivate(): Thenable { }); } -function copy_resource_path(uri: vscode.Uri) { +async function copy_resource_path(uri: vscode.Uri) { if (!uri) { uri = vscode.window.activeTextEditor.document.uri; } - const project_dir = path.dirname(find_project_file(uri.fsPath)); - if (project_dir === null) { - return; - } - - let relative_path = path.normalize(path.relative(project_dir, uri.fsPath)); - relative_path = relative_path.split(path.sep).join(path.posix.sep); - relative_path = "res://" + relative_path; + const relative_path = await convert_uri_to_resource_path(uri); vscode.env.clipboard.writeText(relative_path); } diff --git a/src/providers/document_drops.ts b/src/providers/document_drops.ts index d6dc1c3..6adf4da 100644 --- a/src/providers/document_drops.ts +++ b/src/providers/document_drops.ts @@ -1,3 +1,4 @@ +import * as path from "node:path"; import * as vscode from "vscode"; import { CancellationToken, @@ -12,11 +13,15 @@ import { TextDocument, Uri, } from "vscode"; -import { createLogger, node_name_to_snake, get_project_version } from "../utils"; +import { SceneParser } from "../scene_tools/parser"; +import { createLogger, node_name_to_snake, get_project_version, convert_uri_to_resource_path } from "../utils"; +import { SceneNode } from "../scene_tools/types"; const log = createLogger("providers.drops"); export class GDDocumentDropEditProvider implements DocumentDropEditProvider { + public parser = new SceneParser(); + constructor(private context: ExtensionContext) { const dropEditSelector = [ { language: "csharp", scheme: "file" }, @@ -33,24 +38,56 @@ export class GDDocumentDropEditProvider implements DocumentDropEditProvider { ): Promise { // log.debug("provideDocumentDropEdits", document, dataTransfer); - // const origin = dataTransfer.get("text/plain").value; - // log.debug(origin); + const targetResPath = await convert_uri_to_resource_path(document.uri); - // TODO: compare the source scene to the target file - // What should happen when you drag a node into a script that isn't the - // "main" script for that scene? - // Attempt to calculate a relative path that resolves correctly? + const originFsPath = dataTransfer.get("godot/scene").value; + const originUri = vscode.Uri.file(originFsPath); + + const originDocument = await vscode.workspace.openTextDocument(originUri); + const scene = await this.parser.parse_scene(originDocument); + + let scriptId = ""; + for (const res of scene.externalResources.values()) { + if (res.path === targetResPath) { + scriptId = res.id; + break; + } + } + + let nodePathOfTarget: SceneNode; + if (scriptId) { + const find_node = () => { + if (scene.root.scriptId === scriptId) { + return scene.root; + } + for (const node of scene.nodes.values()) { + if (node.scriptId === scriptId) { + return node; + } + } + }; + nodePathOfTarget = find_node(); + } const className: string = dataTransfer.get("godot/class")?.value; if (className) { - const path: string = dataTransfer.get("godot/path")?.value; + const nodePath: string = dataTransfer.get("godot/path")?.value; + let relativePath: string = dataTransfer.get("godot/relativePath")?.value; const unique = dataTransfer.get("godot/unique")?.value === "true"; const label: string = dataTransfer.get("godot/label")?.value; + if (nodePathOfTarget) { + const targetPath = path.normalize(path.relative(nodePathOfTarget?.path, nodePath)); + relativePath = targetPath.split(path.sep).join(path.posix.sep); + } + // For the root node, the path is empty and needs to be replaced with the node name - const savePath = path || label; + let savePath = relativePath || label; if (document.languageId === "gdscript") { + if (savePath.startsWith(".")) { + savePath = `'${savePath}'`; + } let qualifiedPath = `$${savePath}`; if (unique) { diff --git a/src/providers/document_link.ts b/src/providers/document_link.ts index cf183fe..026462f 100644 --- a/src/providers/document_link.ts +++ b/src/providers/document_link.ts @@ -40,7 +40,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider { const uri = Uri.from({ scheme: "file", path: path, - fragment: `${scene.externalResources[id].line},0`, + fragment: `${scene.externalResources.get(id).line},0`, }); const r = this.create_range(document, match); @@ -54,7 +54,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider { const uri = Uri.from({ scheme: "file", path: path, - fragment: `${scene.subResources[id].line},0`, + fragment: `${scene.subResources.get(id).line},0`, }); const r = this.create_range(document, match); diff --git a/src/providers/hover.ts b/src/providers/hover.ts index d15421a..f2ae461 100644 --- a/src/providers/hover.ts +++ b/src/providers/hover.ts @@ -49,8 +49,8 @@ export class GDHoverProvider implements HoverProvider { if (word.startsWith("ExtResource")) { const match = word.match(wordPattern); const id = match[1]; - const resource = scene.externalResources[id]; - const definition = scene.externalResources[id].body; + const resource = scene.externalResources.get(id); + const definition = resource.body; const links = await this.get_links(definition); const contents = new MarkdownString(); @@ -77,7 +77,7 @@ export class GDHoverProvider implements HoverProvider { const match = word.match(wordPattern); const id = match[1]; - let definition = scene.subResources[id].body; + let definition = scene.subResources.get(id).body; // don't display contents of giant arrays definition = definition?.replace(/Array\([0-9,\.\- ]*\)/, "Array(...)"); diff --git a/src/providers/inlay_hints.ts b/src/providers/inlay_hints.ts index f9afdb6..802458b 100644 --- a/src/providers/inlay_hints.ts +++ b/src/providers/inlay_hints.ts @@ -128,7 +128,7 @@ export class GDInlayHintsProvider implements InlayHintsProvider { for (const match of text.matchAll(/ExtResource\(\s?"?(\w+)\s?"?\)/g)) { const id = match[1]; const end = document.positionAt(match.index + match[0].length); - const resource = scene.externalResources[id]; + const resource = scene.externalResources.get(id); const label = `${resource.type}: "${resource.path}"`; @@ -140,7 +140,7 @@ export class GDInlayHintsProvider implements InlayHintsProvider { for (const match of text.matchAll(/SubResource\(\s?"?(\w+)\s?"?\)/g)) { const id = match[1]; const end = document.positionAt(match.index + match[0].length); - const resource = scene.subResources[id]; + const resource = scene.subResources.get(id); const label = `${resource.type}`; diff --git a/src/scene_tools/parser.ts b/src/scene_tools/parser.ts index 520d94a..2c04ca5 100644 --- a/src/scene_tools/parser.ts +++ b/src/scene_tools/parser.ts @@ -1,6 +1,6 @@ +import * as fs from "node:fs"; +import { basename, extname } from "node:path"; import { TextDocument, Uri } from "vscode"; -import { basename, extname } from "path"; -import * as fs from "fs"; import { SceneNode, Scene } from "./types"; import { createLogger } from "../utils"; @@ -46,7 +46,7 @@ export class SceneParser { const uid = line.match(/uid="([\w:/]+)"/)?.[1]; const id = line.match(/ id="?([\w]+)"?/)?.[1]; - scene.externalResources[id] = { + scene.externalResources.set(id, { body: line, path: path, type: type, @@ -54,7 +54,7 @@ export class SceneParser { id: id, index: match.index, line: document.lineAt(document.positionAt(match.index)).lineNumber + 1, - }; + }); } let lastResource = null; @@ -76,7 +76,7 @@ export class SceneParser { lastResource.body = text.slice(lastResource.index, match.index).trimEnd(); } - scene.subResources[id] = resource; + scene.subResources.set(id, resource); lastResource = resource; } @@ -134,9 +134,10 @@ export class SceneParser { scene.nodes.set(_path, node); if (instance) { - if (instance in scene.externalResources) { - node.tooltip = scene.externalResources[instance].path; - node.resourcePath = scene.externalResources[instance].path; + const res = scene.externalResources.get(instance); + if (res) { + node.tooltip = res.path; + node.resourcePath = res.path; if ([".tscn"].includes(extname(node.resourcePath))) { node.contextValue += "openable"; } diff --git a/src/scene_tools/preview.ts b/src/scene_tools/preview.ts index c044093..5cd1e65 100644 --- a/src/scene_tools/preview.ts +++ b/src/scene_tools/preview.ts @@ -61,11 +61,11 @@ export class ScenePreviewProvider implements TreeDataProvider, TreeDr register_command("scenePreview.openScene", this.open_scene.bind(this)), register_command("scenePreview.openScript", this.open_script.bind(this)), register_command("scenePreview.openCurrentScene", this.open_current_scene.bind(this)), - register_command("scenePreview.openCurrentScript", this.open_main_script.bind(this)), + register_command("scenePreview.openMainScript", this.open_main_script.bind(this)), register_command("scenePreview.goToDefinition", this.go_to_definition.bind(this)), register_command("scenePreview.openDocumentation", this.open_documentation.bind(this)), register_command("scenePreview.refresh", this.refresh.bind(this)), - window.onDidChangeActiveTextEditor(this.refresh.bind(this)), + window.onDidChangeActiveTextEditor(this.text_editor_changed.bind(this)), window.registerFileDecorationProvider(this.uniqueDecorator), window.registerFileDecorationProvider(this.scriptDecorator), this.watcher.onDidChange(this.on_file_changed.bind(this)), @@ -73,6 +73,14 @@ export class ScenePreviewProvider implements TreeDataProvider, TreeDr this.tree.onDidChangeSelection(this.tree_selection_changed), this.tree, ); + const result: string | undefined = this.context.workspaceState.get("godotTools.scenePreview.lockedScene"); + if (result) { + if (fs.existsSync(result)) { + set_context("scenePreview.locked", true); + this.scenePreviewLocked = true; + this.currentScene = result; + } + } this.refresh(); } @@ -83,7 +91,9 @@ export class ScenePreviewProvider implements TreeDataProvider, TreeDr token: vscode.CancellationToken, ): void | Thenable { data.set("godot/scene", new vscode.DataTransferItem(this.currentScene)); - data.set("godot/path", new vscode.DataTransferItem(source[0].relativePath)); + data.set("godot/node", new vscode.DataTransferItem(source[0])); + data.set("godot/path", new vscode.DataTransferItem(source[0].path)); + data.set("godot/relativePath", new vscode.DataTransferItem(source[0].relativePath)); data.set("godot/class", new vscode.DataTransferItem(source[0].className)); data.set("godot/unique", new vscode.DataTransferItem(source[0].unique)); data.set("godot/label", new vscode.DataTransferItem(source[0].label)); @@ -103,11 +113,10 @@ export class ScenePreviewProvider implements TreeDataProvider, TreeDr }, 20); } - public async refresh() { + public async text_editor_changed() { if (this.scenePreviewLocked) { return; } - const editor = vscode.window.activeTextEditor; if (editor) { let fileName = editor.document.uri.fsPath; @@ -140,24 +149,34 @@ export class ScenePreviewProvider implements TreeDataProvider, TreeDr return; } - const document = await vscode.workspace.openTextDocument(fileName); - this.scene = this.parser.parse_scene(document); - - this.tree.message = this.scene.title; this.currentScene = fileName; - - this.changeTreeEvent.fire(); + this.refresh(); } } + public async refresh() { + if (!fs.existsSync(this.currentScene)) { + return; + } + + const document = await vscode.workspace.openTextDocument(this.currentScene); + this.scene = this.parser.parse_scene(document); + + this.tree.message = this.scene.title; + + this.changeTreeEvent.fire(); + } + private lock_preview() { this.scenePreviewLocked = true; set_context("scenePreview.locked", true); + this.context.workspaceState.update("godotTools.scenePreview.lockedScene", this.currentScene); } private unlock_preview() { this.scenePreviewLocked = false; set_context("scenePreview.locked", false); + this.context.workspaceState.update("godotTools.scenePreview.lockedScene", ""); this.refresh(); } @@ -181,7 +200,7 @@ export class ScenePreviewProvider implements TreeDataProvider, TreeDr } private async open_script(item: SceneNode) { - const path = this.scene.externalResources[item.scriptId].path; + const path = this.scene.externalResources.get(item.scriptId).path; const uri = await convert_resource_path_to_uri(path); if (uri) { @@ -200,7 +219,7 @@ export class ScenePreviewProvider implements TreeDataProvider, TreeDr if (this.currentScene) { const root = this.scene.root; if (root?.hasScript) { - const path = this.scene.externalResources[root.scriptId].path; + const path = this.scene.externalResources.get(root.scriptId).path; const uri = await convert_resource_path_to_uri(path); if (uri) { vscode.window.showTextDocument(uri, { preview: true }); diff --git a/src/scene_tools/types.ts b/src/scene_tools/types.ts index 39d568f..264bf5f 100644 --- a/src/scene_tools/types.ts +++ b/src/scene_tools/types.ts @@ -53,7 +53,7 @@ export class SceneNode extends TreeItem { this.scriptId = line.match(/script = ExtResource\(\s*"?([\w]+)"?\s*\)/)[1]; this.contextValue += "hasScript"; } - if (line != "") { + if (line !== "") { newLines.push(line); } } @@ -79,7 +79,7 @@ export class Scene { public title: string; public mtime: number; public root: SceneNode | undefined; - public externalResources: {[key: string]: GDResource} = {}; - public subResources: {[key: string]: GDResource} = {}; + public externalResources: Map = new Map(); + public subResources: Map = new Map(); public nodes: Map = new Map(); } diff --git a/src/utils/godot_utils.ts b/src/utils/godot_utils.ts index 30ee9d1..2083763 100644 --- a/src/utils/godot_utils.ts +++ b/src/utils/godot_utils.ts @@ -116,6 +116,17 @@ export async function convert_resource_path_to_uri(resPath: string): Promise { + const project_dir = path.dirname(find_project_file(uri.fsPath)); + if (project_dir === null) { + return; + } + + let relative_path = path.normalize(path.relative(project_dir, uri.fsPath)); + relative_path = relative_path.split(path.sep).join(path.posix.sep); + return `res://${relative_path}`; +} + export type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE"; export type VERIFY_RESULT = { status: VERIFY_STATUS;