diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ea022727..b5af1351 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -2,6 +2,8 @@ // Import the module and reference it with the alias vscode in your code below import * as vscode from "vscode"; +import { deserializeFlow, serializeFlow } from "@flyde/loader/dist/server"; +import { updateMovedFileReferences } from "./updateMovedFileReferences"; import { FlydeEditorEditorProvider } from "./flydeEditor"; import * as execa from "execa"; @@ -55,6 +57,78 @@ export function activate(context: vscode.ExtensionContext) { const mainOutputChannel = vscode.window.createOutputChannel("Flyde"); const debugOutputChannel = vscode.window.createOutputChannel("Flyde (Debug)"); + // Update Flyde references before VS Code moves or renames files. + context.subscriptions.push( + vscode.workspace.onWillRenameFiles((event) => { + event.waitUntil( + (async () => { + const edit = new vscode.WorkspaceEdit(); + + const moves = event.files.map(({ oldUri, newUri }) => ({ + oldPath: oldUri.fsPath, + newPath: newUri.fsPath, + })); + + // Search for Flyde flows in the workspace. + const flowFiles = await vscode.workspace.findFiles( + "**/*.flyde", + "**/node_modules/**" + ); + + for (const flowUri of flowFiles) { + try { + // Read the current document, including unsaved changes. + const document = + await vscode.workspace.openTextDocument(flowUri); + + const originalText = document.getText(); + + if (!originalText.trim()) { + continue; + } + + const flow = deserializeFlow( + originalText, + flowUri.fsPath + ); + + // Recalculate file-backed references. + const changed = updateMovedFileReferences( + flow, + flowUri.fsPath, + moves + ); + + if (!changed) { + continue; + } + + const updatedText = serializeFlow(flow); + + const entireDocument = new vscode.Range( + document.positionAt(0), + document.positionAt(originalText.length) + ); + + edit.replace( + flowUri, + entireDocument, + updatedText + ); + + } catch (error) { + mainOutputChannel.appendLine( + `Could not update references in ${flowUri.fsPath}: ${String(error)}` + ); + } + } + + return edit; + })() + ); + }) + ); + let currentTheme = vscode.window.activeColorTheme; let useDarkMode = diff --git a/vscode/src/updateMovedFileReferences.ts b/vscode/src/updateMovedFileReferences.ts new file mode 100644 index 00000000..34fe7ffc --- /dev/null +++ b/vscode/src/updateMovedFileReferences.ts @@ -0,0 +1,122 @@ + +import * as path from "path"; +import { FlydeFlow, VisualNode } from "@flyde/core"; + +export interface FileMove { + oldPath: string; + newPath: string; +} + +/** + * Resolve a file's new location after one or more file/folder moves. + */ +function getMovedPath( + filePath: string, + moves: FileMove[] +): string { + const absolutePath = path.resolve(filePath); + + for (const move of moves) { + const oldPath = path.resolve(move.oldPath); + const newPath = path.resolve(move.newPath); + + const relative = path.relative(oldPath, absolutePath); + + // Match the exact file or a file inside a moved folder. + if ( + relative === "" || + (relative !== ".." && + !relative.startsWith(".." + path.sep) && + !path.isAbsolute(relative)) + ) { + return path.resolve(newPath, relative); + } + } + + return absolutePath; +} + +/** + * Update file-backed node references inside a Flyde flow. + * + * Returns true when at least one reference changed. + */ +export function updateMovedFileReferences( + flow: FlydeFlow, + flowFilePath: string, + moves: FileMove[] +): boolean { + let changed = false; + + const oldFlowPath = path.resolve(flowFilePath); + const newFlowPath = getMovedPath(oldFlowPath, moves); + + function updateNode(node: VisualNode): void { + for (const instance of node.instances) { + const source = instance.source; + + if (!source) { + continue; + } + + if ( + source.type === "file" && + typeof source.data === "string" + ) { + const wasAbsolute = path.isAbsolute(source.data); + + const oldTarget = path.resolve( + path.dirname(oldFlowPath), + source.data + ); + + const newTarget = getMovedPath(oldTarget, moves); + + if ( + oldTarget !== newTarget || + oldFlowPath !== newFlowPath + ) { + let updatedReference: string; + + if (wasAbsolute) { + updatedReference = newTarget; + } else { + updatedReference = path.relative( + path.dirname(newFlowPath), + newTarget + ); + + // Keep relative imports explicitly relative. + if ( + !updatedReference.startsWith(".") + ) { + updatedReference = "./" + updatedReference; + } + + // Flyde references use forward slashes. + updatedReference = + updatedReference.replace(/\\/g, "/"); + } + + if (source.data !== updatedReference) { + source.data = updatedReference; + changed = true; + } + } + } + + // Also handle nested inline visual nodes. + if ( + source.type === "inline" && + source.data && + typeof source.data === "object" + ) { + updateNode(source.data as VisualNode); + } + } + } + + updateNode(flow.node); + + return changed; +} \ No newline at end of file