Selaa lähdekoodia

Scene Preview Improvements (relative path drag/drop) (#815)

* Improve scene preview lock behavior
* Add convert_uri_to_resource_path utility function
* Implement relative nodepath dropping
* Prevent a possible error when trying to refresh a scene that doesn't exist
* Fix wrong command name being called (scenePreview.openMainScene has actually never worked)
David Kincaid 4 kuukautta sitten
vanhempi
commit
a04f58c82d

+ 4 - 10
src/extension.ts

@@ -27,6 +27,7 @@ import {
 	get_project_dir,
 	get_project_dir,
 	get_project_version,
 	get_project_version,
 	verify_godot_version,
 	verify_godot_version,
+	convert_uri_to_resource_path,
 } from "./utils";
 } from "./utils";
 import { prompt_for_godot_executable } from "./utils/prompts";
 import { prompt_for_godot_executable } from "./utils/prompts";
 import { killSubProcesses, subProcess } from "./utils/subspawn";
 import { killSubProcesses, subProcess } from "./utils/subspawn";
@@ -58,7 +59,7 @@ export function activate(context: vscode.ExtensionContext) {
 	globals.debug = new GodotDebugger(context);
 	globals.debug = new GodotDebugger(context);
 	globals.scenePreviewProvider = new ScenePreviewProvider(context);
 	globals.scenePreviewProvider = new ScenePreviewProvider(context);
 	globals.linkProvider = new GDDocumentLinkProvider(context);
 	globals.linkProvider = new GDDocumentLinkProvider(context);
-    globals.dropsProvider = new GDDocumentDropEditProvider(context);
+	globals.dropsProvider = new GDDocumentDropEditProvider(context);
 	globals.hoverProvider = new GDHoverProvider(context);
 	globals.hoverProvider = new GDHoverProvider(context);
 	globals.inlayProvider = new GDInlayHintsProvider(context);
 	globals.inlayProvider = new GDInlayHintsProvider(context);
 	globals.formattingProvider = new FormattingProvider(context);
 	globals.formattingProvider = new FormattingProvider(context);
@@ -122,19 +123,12 @@ export function deactivate(): Thenable<void> {
 	});
 	});
 }
 }
 
 
-function copy_resource_path(uri: vscode.Uri) {
+async function copy_resource_path(uri: vscode.Uri) {
 	if (!uri) {
 	if (!uri) {
 		uri = vscode.window.activeTextEditor.document.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);
 	vscode.env.clipboard.writeText(relative_path);
 }
 }

+ 46 - 9
src/providers/document_drops.ts

@@ -1,3 +1,4 @@
+import * as path from "node:path";
 import * as vscode from "vscode";
 import * as vscode from "vscode";
 import {
 import {
 	CancellationToken,
 	CancellationToken,
@@ -12,11 +13,15 @@ import {
 	TextDocument,
 	TextDocument,
 	Uri,
 	Uri,
 } from "vscode";
 } 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");
 const log = createLogger("providers.drops");
 
 
 export class GDDocumentDropEditProvider implements DocumentDropEditProvider {
 export class GDDocumentDropEditProvider implements DocumentDropEditProvider {
+	public parser = new SceneParser();
+
 	constructor(private context: ExtensionContext) {
 	constructor(private context: ExtensionContext) {
 		const dropEditSelector = [
 		const dropEditSelector = [
 			{ language: "csharp", scheme: "file" },
 			{ language: "csharp", scheme: "file" },
@@ -33,24 +38,56 @@ export class GDDocumentDropEditProvider implements DocumentDropEditProvider {
 	): Promise<DocumentDropEdit> {
 	): Promise<DocumentDropEdit> {
 		// log.debug("provideDocumentDropEdits", document, dataTransfer);
 		// 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);
+
+		const originFsPath = dataTransfer.get("godot/scene").value;
+		const originUri = vscode.Uri.file(originFsPath);
 
 
-		// 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 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;
 		const className: string = dataTransfer.get("godot/class")?.value;
 		if (className) {
 		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 unique = dataTransfer.get("godot/unique")?.value === "true";
 			const label: string = dataTransfer.get("godot/label")?.value;
 			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
 			// 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 (document.languageId === "gdscript") {
+				if (savePath.startsWith(".")) {
+					savePath = `'${savePath}'`;
+				}
 				let qualifiedPath = `$${savePath}`;
 				let qualifiedPath = `$${savePath}`;
 
 
 				if (unique) {
 				if (unique) {

+ 2 - 2
src/providers/document_link.ts

@@ -40,7 +40,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
 				const uri = Uri.from({
 				const uri = Uri.from({
 					scheme: "file",
 					scheme: "file",
 					path: path,
 					path: path,
-					fragment: `${scene.externalResources[id].line},0`,
+					fragment: `${scene.externalResources.get(id).line},0`,
 				});
 				});
 
 
 				const r = this.create_range(document, match);
 				const r = this.create_range(document, match);
@@ -54,7 +54,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
 				const uri = Uri.from({
 				const uri = Uri.from({
 					scheme: "file",
 					scheme: "file",
 					path: path,
 					path: path,
-					fragment: `${scene.subResources[id].line},0`,
+					fragment: `${scene.subResources.get(id).line},0`,
 				});
 				});
 
 
 				const r = this.create_range(document, match);
 				const r = this.create_range(document, match);

+ 3 - 3
src/providers/hover.ts

@@ -49,8 +49,8 @@ export class GDHoverProvider implements HoverProvider {
 			if (word.startsWith("ExtResource")) {
 			if (word.startsWith("ExtResource")) {
 				const match = word.match(wordPattern);
 				const match = word.match(wordPattern);
 				const id = match[1];
 				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 links = await this.get_links(definition);
 
 
 				const contents = new MarkdownString();
 				const contents = new MarkdownString();
@@ -77,7 +77,7 @@ export class GDHoverProvider implements HoverProvider {
 				const match = word.match(wordPattern);
 				const match = word.match(wordPattern);
 				const id = match[1];
 				const id = match[1];
 
 
-				let definition = scene.subResources[id].body;
+				let definition = scene.subResources.get(id).body;
 				// don't display contents of giant arrays
 				// don't display contents of giant arrays
 				definition = definition?.replace(/Array\([0-9,\.\- ]*\)/, "Array(...)");
 				definition = definition?.replace(/Array\([0-9,\.\- ]*\)/, "Array(...)");
 
 

+ 2 - 2
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)) {
 		for (const match of text.matchAll(/ExtResource\(\s?"?(\w+)\s?"?\)/g)) {
 			const id = match[1];
 			const id = match[1];
 			const end = document.positionAt(match.index + match[0].length);
 			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}"`;
 			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)) {
 		for (const match of text.matchAll(/SubResource\(\s?"?(\w+)\s?"?\)/g)) {
 			const id = match[1];
 			const id = match[1];
 			const end = document.positionAt(match.index + match[0].length);
 			const end = document.positionAt(match.index + match[0].length);
-			const resource = scene.subResources[id];
+			const resource = scene.subResources.get(id);
 
 
 			const label = `${resource.type}`;
 			const label = `${resource.type}`;
 
 

+ 9 - 8
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 { TextDocument, Uri } from "vscode";
-import { basename, extname } from "path";
-import * as fs from "fs";
 import { SceneNode, Scene } from "./types";
 import { SceneNode, Scene } from "./types";
 import { createLogger } from "../utils";
 import { createLogger } from "../utils";
 
 
@@ -46,7 +46,7 @@ export class SceneParser {
 			const uid = line.match(/uid="([\w:/]+)"/)?.[1];
 			const uid = line.match(/uid="([\w:/]+)"/)?.[1];
 			const id = line.match(/ id="?([\w]+)"?/)?.[1];
 			const id = line.match(/ id="?([\w]+)"?/)?.[1];
 
 
-			scene.externalResources[id] = {
+			scene.externalResources.set(id, {
 				body: line,
 				body: line,
 				path: path,
 				path: path,
 				type: type,
 				type: type,
@@ -54,7 +54,7 @@ export class SceneParser {
 				id: id,
 				id: id,
 				index: match.index,
 				index: match.index,
 				line: document.lineAt(document.positionAt(match.index)).lineNumber + 1,
 				line: document.lineAt(document.positionAt(match.index)).lineNumber + 1,
-			};
+			});
 		}
 		}
 
 
 		let lastResource = null;
 		let lastResource = null;
@@ -76,7 +76,7 @@ export class SceneParser {
 				lastResource.body = text.slice(lastResource.index, match.index).trimEnd();
 				lastResource.body = text.slice(lastResource.index, match.index).trimEnd();
 			}
 			}
 
 
-			scene.subResources[id] = resource;
+			scene.subResources.set(id, resource);
 			lastResource = resource;
 			lastResource = resource;
 		}
 		}
 
 
@@ -134,9 +134,10 @@ export class SceneParser {
 			scene.nodes.set(_path, node);
 			scene.nodes.set(_path, node);
 
 
 			if (instance) {
 			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))) {
 					if ([".tscn"].includes(extname(node.resourcePath))) {
 						node.contextValue += "openable";
 						node.contextValue += "openable";
 					}
 					}

+ 31 - 12
src/scene_tools/preview.ts

@@ -61,11 +61,11 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
 			register_command("scenePreview.openScene", this.open_scene.bind(this)),
 			register_command("scenePreview.openScene", this.open_scene.bind(this)),
 			register_command("scenePreview.openScript", this.open_script.bind(this)),
 			register_command("scenePreview.openScript", this.open_script.bind(this)),
 			register_command("scenePreview.openCurrentScene", this.open_current_scene.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.goToDefinition", this.go_to_definition.bind(this)),
 			register_command("scenePreview.openDocumentation", this.open_documentation.bind(this)),
 			register_command("scenePreview.openDocumentation", this.open_documentation.bind(this)),
 			register_command("scenePreview.refresh", this.refresh.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.uniqueDecorator),
 			window.registerFileDecorationProvider(this.scriptDecorator),
 			window.registerFileDecorationProvider(this.scriptDecorator),
 			this.watcher.onDidChange(this.on_file_changed.bind(this)),
 			this.watcher.onDidChange(this.on_file_changed.bind(this)),
@@ -73,6 +73,14 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
 			this.tree.onDidChangeSelection(this.tree_selection_changed),
 			this.tree.onDidChangeSelection(this.tree_selection_changed),
 			this.tree,
 			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();
 		this.refresh();
 	}
 	}
@@ -83,7 +91,9 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
 		token: vscode.CancellationToken,
 		token: vscode.CancellationToken,
 	): void | Thenable<void> {
 	): void | Thenable<void> {
 		data.set("godot/scene", new vscode.DataTransferItem(this.currentScene));
 		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/class", new vscode.DataTransferItem(source[0].className));
 		data.set("godot/unique", new vscode.DataTransferItem(source[0].unique));
 		data.set("godot/unique", new vscode.DataTransferItem(source[0].unique));
 		data.set("godot/label", new vscode.DataTransferItem(source[0].label));
 		data.set("godot/label", new vscode.DataTransferItem(source[0].label));
@@ -103,11 +113,10 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
 		}, 20);
 		}, 20);
 	}
 	}
 
 
-	public async refresh() {
+	public async text_editor_changed() {
 		if (this.scenePreviewLocked) {
 		if (this.scenePreviewLocked) {
 			return;
 			return;
 		}
 		}
-
 		const editor = vscode.window.activeTextEditor;
 		const editor = vscode.window.activeTextEditor;
 		if (editor) {
 		if (editor) {
 			let fileName = editor.document.uri.fsPath;
 			let fileName = editor.document.uri.fsPath;
@@ -140,24 +149,34 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
 				return;
 				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.currentScene = fileName;
+			this.refresh();
+		}
+	}
 
 
-			this.changeTreeEvent.fire();
+	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() {
 	private lock_preview() {
 		this.scenePreviewLocked = true;
 		this.scenePreviewLocked = true;
 		set_context("scenePreview.locked", true);
 		set_context("scenePreview.locked", true);
+		this.context.workspaceState.update("godotTools.scenePreview.lockedScene", this.currentScene);
 	}
 	}
 
 
 	private unlock_preview() {
 	private unlock_preview() {
 		this.scenePreviewLocked = false;
 		this.scenePreviewLocked = false;
 		set_context("scenePreview.locked", false);
 		set_context("scenePreview.locked", false);
+		this.context.workspaceState.update("godotTools.scenePreview.lockedScene", "");
 		this.refresh();
 		this.refresh();
 	}
 	}
 
 
@@ -181,7 +200,7 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
 	}
 	}
 
 
 	private async open_script(item: SceneNode) {
 	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);
 		const uri = await convert_resource_path_to_uri(path);
 		if (uri) {
 		if (uri) {
@@ -200,7 +219,7 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
 		if (this.currentScene) {
 		if (this.currentScene) {
 			const root = this.scene.root;
 			const root = this.scene.root;
 			if (root?.hasScript) {
 			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);
 				const uri = await convert_resource_path_to_uri(path);
 				if (uri) {
 				if (uri) {
 					vscode.window.showTextDocument(uri, { preview: true });
 					vscode.window.showTextDocument(uri, { preview: true });

+ 3 - 3
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.scriptId = line.match(/script = ExtResource\(\s*"?([\w]+)"?\s*\)/)[1];
 				this.contextValue += "hasScript";
 				this.contextValue += "hasScript";
 			}
 			}
-			if (line != "") {
+			if (line !== "") {
 				newLines.push(line);
 				newLines.push(line);
 			}
 			}
 		}
 		}
@@ -79,7 +79,7 @@ export class Scene {
 	public title: string;
 	public title: string;
 	public mtime: number;
 	public mtime: number;
 	public root: SceneNode | undefined;
 	public root: SceneNode | undefined;
-	public externalResources: {[key: string]: GDResource} = {};
-	public subResources: {[key: string]: GDResource} = {};
+	public externalResources: Map<string, GDResource> = new Map();
+	public subResources: Map<string, GDResource> = new Map();
 	public nodes: Map<string, SceneNode> = new Map();
 	public nodes: Map<string, SceneNode> = new Map();
 }
 }

+ 11 - 0
src/utils/godot_utils.ts

@@ -116,6 +116,17 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
 	return vscode.Uri.joinPath(vscode.Uri.file(dir), resPath.substring("res://".length));
 	return vscode.Uri.joinPath(vscode.Uri.file(dir), resPath.substring("res://".length));
 }
 }
 
 
+export async function convert_uri_to_resource_path(uri: vscode.Uri): Promise<string | null> {
+	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_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
 export type VERIFY_RESULT = {
 export type VERIFY_RESULT = {
 	status: VERIFY_STATUS;
 	status: VERIFY_STATUS;