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
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ Not sure where to start? Join our [discord](https://www.flyde.dev/discord) and w

Check out the official website at [https://flyde.dev](https://flyde.dev) for more information.

## Moving imported flows

Renaming or moving files and folders through VS Code updates local file imports
in workspace `.flyde` documents, including imports inside a moved flow. The edit
preserves YAML comments and unsaved document changes. Package imports are not
rewritten. This applies to VS Code rename operations; changes made externally on
disk do not emit the event used by the extension. Files with invalid YAML are
skipped with a message in the Flyde output channel.

## Telemetry

Flyde VS Code extension collects anonymous usage data to help improve the product. This includes events like extension activation, flow creation, and error reporting. No personal information or code content is collected. You can disable telemetry in VS Code settings (`flyde.telemetry.enabled`) or by setting the environment variable `FLYDE_TELEMETRY_DISABLED=true`.
Expand Down
5 changes: 3 additions & 2 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@
"socket.io": "^4.5.1",
"ts-node": "^10.9.2",
"use-debounce": "^8.0.4",
"walker": "^1.0.8"
"walker": "^1.0.8",
"yaml": "^2.3.1"
},
"packageManager": "yarn@3.3.1"
}
}
2 changes: 2 additions & 0 deletions vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as vscode from "vscode";

import { FlydeEditorEditorProvider } from "./flydeEditor";
import { registerImportRenames } from "./renameImports";
import * as execa from "execa";

var fp = require("find-free-port");
Expand Down Expand Up @@ -53,6 +54,7 @@ export function activate(context: vscode.ExtensionContext) {
});

const mainOutputChannel = vscode.window.createOutputChannel("Flyde");
context.subscriptions.push(registerImportRenames(mainOutputChannel));
const debugOutputChannel = vscode.window.createOutputChannel("Flyde (Debug)");

let currentTheme = vscode.window.activeColorTheme;
Expand Down
122 changes: 122 additions & 0 deletions vscode/src/importEdits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as path from "path";
import { isMap, isScalar, isSeq, parseDocument } from "yaml";

export interface FileMove {
oldPath: string;
newPath: string;
}

export interface ImportEdit {
start: number;
end: number;
text: string;
}

// Resolve every move against the original paths, not against earlier moves in
// the same batch (which could otherwise redirect an import twice).
function movedPath(
file: string,
moves: readonly FileMove[],
paths: typeof path,
): string {
for (const move of [...moves].sort(
(a, b) => b.oldPath.length - a.oldPath.length,
)) {
const relative = paths.relative(move.oldPath, file);
if (
relative === "" ||
(!paths.isAbsolute(relative) &&
relative !== ".." &&
!relative.startsWith(`..${paths.sep}`))
) {
return paths.join(move.newPath, relative);
}
}
return file;
}

/** Rewrite only import scalars, preserving comments, formatting, and unsaved edits. */
export function getImportEdits(
text: string,
file: string,
moves: readonly FileMove[],
paths: typeof path = path,
): ImportEdit[] {
const document = parseDocument(text);
if (document.errors.length) {
throw new Error(document.errors[0].message);
}
const edits: ImportEdit[] = [];
const destination = movedPath(file, moves, paths);

function rewrite(value: unknown): void {
if (!isScalar(value) || typeof value.value !== "string" || !value.range) {
return;
}
const original = value.value;
if (
!original.startsWith("./") &&
!original.startsWith("../") &&
!original.startsWith(".\\") &&
!original.startsWith("..\\") &&
!paths.isAbsolute(original)
) {
return;
}
const target = paths.resolve(paths.dirname(file), original);
const newTarget = movedPath(target, moves, paths);
if (destination === file && target === newTarget) {
return;
}
let updated = paths.isAbsolute(original)
? newTarget
: paths.relative(paths.dirname(destination), newTarget);
updated = updated.split(paths.sep).join("/");
if (!paths.isAbsolute(updated) && !updated.startsWith("../")) {
updated = `./${updated}`;
}
if (updated !== original) {
edits.push({
start: value.range[0],
end: value.range[1],
text: JSON.stringify(updated),
});
}
}

function visitNode(node: unknown): void {
if (!isMap(node)) {
return;
}
const instances = node.get("instances", true);
if (!isSeq(instances)) {
return;
}
for (const instance of instances.items) {
if (!isMap(instance)) {
continue;
}
const source = instance.get("source", true);
if (isMap(source)) {
if (source.get("type") === "file") {
rewrite(source.get("data", true));
} else if (source.get("type") === "inline") {
visitNode(source.get("data", true));
}
}
// Older flows keep inline nodes directly on the instance.
visitNode(instance.get("node", true));
}
}

if (isMap(document.contents)) {
visitNode(document.contents.get("node", true));
const imports = document.contents.get("imports", true);
if (isMap(imports)) {
for (const entry of imports.items) {
rewrite(entry.key);
}
}
}
return edits;
}
72 changes: 72 additions & 0 deletions vscode/src/renameImports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as vscode from "vscode";
import { getImportEdits } from "./importEdits";

export function registerImportRenames(
output: vscode.OutputChannel,
): vscode.Disposable {
return vscode.workspace.onWillRenameFiles((event) => {
// waitUntil must be called synchronously during event dispatch. VS Code
// applies the returned edits before moving files, including dirty documents.
event.waitUntil(prepareImportRenames(event, output));
});
}

async function prepareImportRenames(
event: vscode.FileWillRenameEvent,
output: vscode.OutputChannel,
): Promise<vscode.WorkspaceEdit> {
const edit = new vscode.WorkspaceEdit();
const moves = event.files
.filter(
(file) => file.oldUri.scheme === "file" && file.newUri.scheme === "file",
)
.map((file) => ({
oldPath: file.oldUri.fsPath,
newPath: file.newUri.fsPath,
}));
if (!moves.length) {
return edit;
}
const uris = await vscode.workspace.findFiles(
"**/*.flyde",
"**/{node_modules,.git}/**",
undefined,
event.token,
);
const documents = new Map(uris.map((uri) => [uri.toString(), uri]));
for (const document of vscode.workspace.textDocuments) {
if (
document.uri.scheme === "file" &&
document.uri.path.endsWith(".flyde")
) {
documents.set(document.uri.toString(), document.uri);
}
}
for (const uri of documents.values()) {
if (event.token.isCancellationRequested) {
return new vscode.WorkspaceEdit();
}
try {
const document = await vscode.workspace.openTextDocument(uri);
for (const change of getImportEdits(
document.getText(),
uri.fsPath,
moves,
)) {
edit.replace(
uri,
new vscode.Range(
document.positionAt(change.start),
document.positionAt(change.end),
),
change.text,
);
}
} catch (error) {
output.appendLine(
`Could not update Flyde imports in ${uri.fsPath}: ${error}`,
);
}
}
return edit;
}
Loading