diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ea022727..0232dde9 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -10,11 +10,15 @@ var fp = require("find-free-port"); import { createEmbeddedServer } from "./embedded-server"; import { join } from "path"; +import * as path from "path"; +import * as nodeFs from "fs"; import { activateReporter, reportEvent, analytics } from "./analytics"; import { showFirstRunPrivacyNotice, showPrivacySettings } from "./privacyNotice"; import { Template, getTemplates, scaffoldTemplate } from "./templateUtils"; +import { deserializeFlow, serializeFlow } from "@flyde/loader/dist/server"; +import { updateFlowImportForRename } from "./flow-imports"; // the application insights key (also known as instrumentation key) @@ -106,6 +110,39 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("flyde.openAsText", openAsTextHandler) ); + const renameImportListener = vscode.workspace.onDidRenameFiles(async (event) => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) return; + + for (const moved of event.files) { + if (!moved.oldUri.fsPath.endsWith(".flyde") || !moved.newUri.fsPath.endsWith(".flyde")) { + continue; + } + + const flowUris = await vscode.workspace.findFiles( + new vscode.RelativePattern(workspaceFolder, "**/*.flyde"), + "**/node_modules/**", + ); + + for (const uri of flowUris) { + if (path.normalize(uri.fsPath) === path.normalize(moved.newUri.fsPath)) continue; + + try { + const contents = await nodeFs.promises.readFile(uri.fsPath, "utf8"); + const flow = deserializeFlow(contents, uri.fsPath); + if (!updateFlowImportForRename(flow, uri.fsPath, moved.oldUri.fsPath, moved.newUri.fsPath)) { + continue; + } + + await nodeFs.promises.writeFile(uri.fsPath, serializeFlow(flow), "utf8"); + } catch (error) { + console.warn(`Could not update Flyde imports in ${uri.fsPath}:`, error); + } + } + } + }); + context.subscriptions.push(renameImportListener); + function getWorkspaceRootPath(): vscode.Uri | undefined { // Check if there is an open workspace if ( diff --git a/vscode/src/flow-imports.spec.ts b/vscode/src/flow-imports.spec.ts new file mode 100644 index 00000000..92e355e9 --- /dev/null +++ b/vscode/src/flow-imports.spec.ts @@ -0,0 +1,50 @@ +import assert = require("assert"); +import { updateFlowImportForRename } from "./flow-imports"; + +const flow = (data: string) => ({ + node: { + id: "Parent", + inputs: {}, + outputs: {}, + instances: [ + { + id: "child", + nodeId: "Child", + type: "visual", + source: { type: "file", data }, + inputConfig: {}, + pos: { x: 0, y: 0 }, + }, + ], + connections: [], + }, + imports: {}, +}); + +describe("flow import rename handling", () => { + it("updates a relative import when its target moves", () => { + const value = flow("./ChildFlow.flyde"); + const changed = updateFlowImportForRename( + value as any, + "/workspace/flows/Parent.flyde", + "/workspace/flows/ChildFlow.flyde", + "/workspace/components/ChildFlow.flyde", + ); + + assert.equal(changed, true); + assert.equal(value.node.instances[0].source.data, "../components/ChildFlow.flyde"); + }); + + it("does not rewrite imports that point elsewhere", () => { + const value = flow("./OtherFlow.flyde"); + const changed = updateFlowImportForRename( + value as any, + "/workspace/flows/Parent.flyde", + "/workspace/flows/ChildFlow.flyde", + "/workspace/components/ChildFlow.flyde", + ); + + assert.equal(changed, false); + assert.equal(value.node.instances[0].source.data, "./OtherFlow.flyde"); + }); +}); diff --git a/vscode/src/flow-imports.ts b/vscode/src/flow-imports.ts new file mode 100644 index 00000000..af0a3e28 --- /dev/null +++ b/vscode/src/flow-imports.ts @@ -0,0 +1,41 @@ +import { dirname, isAbsolute, normalize, relative, resolve } from "path"; +import type { FlydeFlow } from "@flyde/core"; + +const toComparablePath = (value: string) => normalize(resolve(value)); + +const toImportPath = (value: string) => { + const normalized = value.replace(/\\/g, "/"); + return normalized.startsWith(".") ? normalized : `./${normalized}`; +}; + +/** Updates file-based node imports when their target file is moved. */ +export function updateFlowImportForRename( + flow: FlydeFlow, + importingFilePath: string, + oldTargetPath: string, + newTargetPath: string, +): boolean { + const oldTarget = toComparablePath(oldTargetPath); + let changed = false; + + for (const instance of flow.node.instances) { + if (instance.source?.type !== "file" || typeof instance.source.data !== "string") { + continue; + } + + const data = instance.source.data; + const resolvedTarget = isAbsolute(data) + ? toComparablePath(data) + : toComparablePath(resolve(dirname(importingFilePath), data)); + + if (resolvedTarget !== oldTarget) continue; + + const nextData = toImportPath(relative(dirname(importingFilePath), newTargetPath)); + if (nextData !== data) { + instance.source.data = nextData; + changed = true; + } + } + + return changed; +}