mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2025-12-31 13:48:24 +03:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(...)");
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user