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)
This commit is contained in:
David Kincaid
2025-03-10 04:50:00 -04:00
committed by GitHub
parent 03606fdb3a
commit a04f58c82d
9 changed files with 112 additions and 50 deletions

View File

@@ -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<void> {
});
}
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);
}

View File

@@ -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<DocumentDropEdit> {
// 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) {

View File

@@ -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);

View File

@@ -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(...)");

View File

@@ -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}`;

View File

@@ -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";
}

View File

@@ -61,11 +61,11 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, 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<SceneNode>, 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<SceneNode>, TreeDr
token: vscode.CancellationToken,
): void | Thenable<void> {
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<SceneNode>, 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<SceneNode>, 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<SceneNode>, 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<SceneNode>, 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 });

View File

@@ -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<string, GDResource> = new Map();
public subResources: Map<string, GDResource> = new Map();
public nodes: Map<string, SceneNode> = new Map();
}

View File

@@ -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));
}
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_RESULT = {
status: VERIFY_STATUS;