Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ 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 {
RenamedFileEntry,
resolveMovedTargetPath,
updateFlowObjectImportsOnMove,
} from "./fileMoveImportUpdater";

// the application insights key (also known as instrumentation key)

Expand Down Expand Up @@ -229,6 +235,78 @@ export function activate(context: vscode.ExtensionContext) {
await showPrivacySettings();
})
);

context.subscriptions.push(
vscode.workspace.onDidRenameFiles(async (event) => {
try {
const renamedEntries: RenamedFileEntry[] = event.files.map((f) => ({
oldPath: f.oldUri.fsPath,
newPath: f.newUri.fsPath,
}));
if (renamedEntries.length === 0) {
return;
}

const flydeUris = await vscode.workspace.findFiles(
"**/*.flyde",
"**/node_modules/**"
);

const edit = new vscode.WorkspaceEdit();
let anyUpdated = false;

for (const flowUri of flydeUris) {
const newFlowPath = flowUri.fsPath;
let oldFlowPath = newFlowPath;
for (const entry of renamedEntries) {
if (entry.newPath === newFlowPath) {
oldFlowPath = entry.oldPath;
break;
} else if (
newFlowPath.startsWith(entry.newPath + "/") ||
newFlowPath.startsWith(entry.newPath + "\\")
) {
const suffix = newFlowPath.slice(entry.newPath.length);
oldFlowPath = entry.oldPath + suffix;
break;
}
}

try {
const doc = await vscode.workspace.openTextDocument(flowUri);
const raw = doc.getText();
if (!raw.trim()) {
continue;
}
const flow = deserializeFlow(raw, newFlowPath);
const changed = updateFlowObjectImportsOnMove(
flow as any,
oldFlowPath,
newFlowPath,
renamedEntries
);
if (changed) {
const serialized = serializeFlow(flow);
const fullRange = new vscode.Range(0, 0, doc.lineCount, 0);
edit.replace(flowUri, fullRange, serialized);
anyUpdated = true;
}
} catch (flowErr) {
console.error(
`Failed to update imports for ${newFlowPath} after file move:`,
flowErr
);
}
}

if (anyUpdated) {
await vscode.workspace.applyEdit(edit);
}
} catch (err) {
console.error("Error handling onDidRenameFiles in Flyde:", err);
}
})
);
}

// this method is called when your extension is deactivated
Expand Down
141 changes: 141 additions & 0 deletions vscode/src/fileMoveImportUpdater.node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import assert from "node:assert/strict";
import * as path from "node:path";
import test from "node:test";
import {
formatRelativeFlydeImport,
resolveMovedTargetPath,
updateFlowObjectImportsOnMove,
} from "./fileMoveImportUpdater.ts";

test("updates file source path when an imported code node is moved to a subfolder", () => {
const workspace = path.resolve("/workspace/project");
const flowPath = path.join(workspace, "Main.flyde");
const oldNodePath = path.join(workspace, "MyCodeNode.flyde.ts");
const newNodePath = path.join(workspace, "nodes", "MyCodeNode.flyde.ts");

const flow = {
node: {
id: "Main",
instances: [
{
id: "ins-1",
nodeId: "MyCodeNode",
type: "code",
source: {
type: "file",
data: "MyCodeNode.flyde.ts",
},
},
],
},
};

const changed = updateFlowObjectImportsOnMove(flow, flowPath, flowPath, [
{ oldPath: oldNodePath, newPath: newNodePath },
]);

assert.equal(changed, true);
assert.equal(flow.node.instances[0].source.data, "nodes/MyCodeNode.flyde.ts");
});

test("updates file source path when the importing .flyde flow itself is moved to another folder", () => {
const workspace = path.resolve("/workspace/project");
const oldFlowPath = path.join(workspace, "Main.flyde");
const newFlowPath = path.join(workspace, "flows", "Main.flyde");

const flow = {
node: {
id: "Main",
instances: [
{
id: "ins-1",
nodeId: "Helper",
type: "code",
source: {
type: "file",
data: "./Helper.flyde.ts",
},
},
],
},
};

const changed = updateFlowObjectImportsOnMove(flow, oldFlowPath, newFlowPath, [
{ oldPath: oldFlowPath, newPath: newFlowPath },
]);

assert.equal(changed, true);
assert.equal(flow.node.instances[0].source.data, "../Helper.flyde.ts");
});

test("updates nested inline visual node imports and legacy imports map when a directory is renamed", () => {
const workspace = path.resolve("/workspace/project");
const flowPath = path.join(workspace, "Main.flyde");
const oldDir = path.join(workspace, "old-folder");
const newDir = path.join(workspace, "new-folder");

const flow = {
imports: {
"./old-folder/Legacy.flyde.ts": ["Legacy"],
"@flyde/nodes": ["InlineValue"],
},
node: {
id: "Main",
instances: [
{
id: "inline-1",
type: "visual",
source: {
type: "inline",
data: {
id: "InlineSub",
instances: [
{
id: "sub-1",
nodeId: "SubFlow",
type: "visual",
source: {
type: "file",
data: "./old-folder/SubFlow.flyde",
},
},
],
},
},
},
],
},
};

const changed = updateFlowObjectImportsOnMove(flow, flowPath, flowPath, [
{ oldPath: oldDir, newPath: newDir },
]);

assert.equal(changed, true);
assert.equal(
flow.node.instances[0].source.data.instances[0].source.data,
"./new-folder/SubFlow.flyde"
);
assert.deepEqual(flow.imports, {
"./new-folder/Legacy.flyde.ts": ["Legacy"],
"@flyde/nodes": ["InlineValue"],
});
});

test("resolveMovedTargetPath and formatRelativeFlydeImport preserve unchanged paths", () => {
const workspace = path.resolve("/workspace/project");
const target = path.join(workspace, "Unchanged.flyde.ts");
assert.equal(
resolveMovedTargetPath(target, [
{
oldPath: path.join(workspace, "Other.flyde.ts"),
newPath: path.join(workspace, "sub", "Other.flyde.ts"),
},
]),
target
);
assert.equal(
formatRelativeFlydeImport(workspace, target, "./Unchanged.flyde.ts"),
"./Unchanged.flyde.ts"
);
});
Loading