Skip to content

Commit

Permalink
✨ Use immer for extension state (#107)
Browse files Browse the repository at this point in the history
Concentrate data state in the extension object as state.data and enforce
top-down propagation of changes.

Key points:
1. state is made immutable using Immer - reference comparison can be
    used to detect changes
2. components interested in receiving notifications about state change
    should add callbacks via onDidChangeData
3. by convention after initialization the state is modified only via
    commands

Related refactorings:
1. track per-file solution state - LocalChange has now "state" prop with
    3 values: pending,applied,discarded
2. use onDidChangeData to trigger file model actions (content of the
    native Konveyor Resolution view) and in-memory FS actions (suggestion
    diffs applied to current code)

---------

Signed-off-by: Radoslaw Szwajkowski <[email protected]>
  • Loading branch information
rszwajko authored Nov 21, 2024
1 parent 126cbe9 commit 69e3864
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 117 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"js-yaml": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"uuid": "^10.0.0"
"uuid": "^10.0.0",
"immer": "10.1.1"
}
}
1 change: 1 addition & 0 deletions shared/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export interface LocalChange {
modifiedUri: Uri;
originalUri: Uri;
diff: string;
state: "pending" | "applied" | "discarded";
}

export interface ResolutionMessage {
Expand Down
2 changes: 1 addition & 1 deletion vscode/src/KonveyorGUIWebviewViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class KonveyorGUIWebviewViewProvider implements WebviewViewProvider {
this.initializeWebview(this._panel.webview);

if (this._viewType === KonveyorGUIWebviewViewProvider.RESOLUTION_VIEW_TYPE) {
const savedData = this._extensionState.sharedState.get("resolutionPanelData");
const savedData = this._extensionState.data.resolutionPanelData;
if (savedData) {
this._panel.webview.postMessage({ type: "loadResolutionState", data: savedData });
}
Expand Down
14 changes: 7 additions & 7 deletions vscode/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,23 +336,23 @@ const commandsMap: (state: ExtensionState) => {
// Update the user settings
await config.update("labelSelector", modifiedLabelSelector, ConfigurationTarget.Workspace);
},
"konveyor.loadRuleSets": (ruleSets: RuleSet[]): void => loadRuleSets(state, ruleSets),
"konveyor.loadRuleSets": async (ruleSets: RuleSet[]) => loadRuleSets(state, ruleSets),
"konveyor.cleanRuleSets": () => cleanRuleSets(state),
"konveyor.loadStaticResults": loadStaticResults,
"konveyor.loadResultsFromDataFolder": loadResultsFromDataFolder,
"konveyor.loadSolution": async (solution: GetSolutionResult) => loadSolution(state, solution),
"konveyor.applyAll": () => applyAll(state),
"konveyor.applyFile": (item: FileItem | Uri) => applyFile(item, state),
"konveyor.copyDiff": (item: FileItem | Uri) => copyDiff(item, state),
"konveyor.applyAll": async () => applyAll(state),
"konveyor.applyFile": async (item: FileItem | Uri) => applyFile(item, state),
"konveyor.copyDiff": async (item: FileItem | Uri) => copyDiff(item, state),
"konveyor.copyPath": copyPath,
"konveyor.diffView.viewFix": viewFix,
"konveyor.discardAll": () => discardAll(state),
"konveyor.discardFile": (item: FileItem | Uri) => discardFile(item, state),
"konveyor.discardAll": async () => discardAll(state),
"konveyor.discardFile": async (item: FileItem | Uri) => discardFile(item, state),
"konveyor.showResolutionPanel": () => {
const resolutionProvider = state.webviewProviders?.get("resolution");
resolutionProvider?.showWebviewPanel();
},
"konveyor.reloadLastResolutions": () => reloadLastResolutions(state),
"konveyor.reloadLastResolutions": async () => reloadLastResolutions(state),
"konveyor.diffView.applyBlock": applyBlock,
"konveyor.diffView.applyBlockInline": applyBlock,
"konveyor.diffView.applySelection": applyBlock,
Expand Down
39 changes: 17 additions & 22 deletions vscode/src/data/loadResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,45 @@ import { processIncidents } from "./analyzerResults";
import { ExtensionState } from "src/extensionState";
import { writeDataFile } from "./storage";
import { toLocalChanges, writeSolutionsToMemFs } from "./virtualStorage";
import { Location, Position, window } from "vscode";
import { window } from "vscode";
import {
KONVEYOR_SCHEME,
RULE_SET_DATA_FILE_PREFIX,
SOLUTION_DATA_FILE_PREFIX,
} from "../utilities";

export const loadRuleSets = (state: ExtensionState, ruleSets: RuleSet[]): void => {
writeDataFile(ruleSets, RULE_SET_DATA_FILE_PREFIX);
state.ruleSets = ruleSets;
export const loadRuleSets = async (state: ExtensionState, ruleSets: RuleSet[]) => {
await writeDataFile(ruleSets, RULE_SET_DATA_FILE_PREFIX);
state.diagnosticCollection.set(processIncidents(ruleSets));
const sidebarProvider = state.webviewProviders?.get("sidebar");
sidebarProvider?.webview?.postMessage({
type: "loadStoredAnalysis",
data: ruleSets,
state.mutateData((draft) => {
draft.ruleSets = ruleSets;
});
};
export const cleanRuleSets = (state: ExtensionState) => {
state.ruleSets = [];
state.diagnosticCollection.clear();
const sidebarProvider = state.webviewProviders?.get("sidebar");
sidebarProvider?.webview?.postMessage({
type: "loadStoredAnalysis",
data: undefined,
state.mutateData((draft) => {
draft.ruleSets = [];
});
};

export const loadSolution = async (state: ExtensionState, solution: GetSolutionResult) => {
writeDataFile(solution, SOLUTION_DATA_FILE_PREFIX);
const localChanges = toLocalChanges(solution);
doLoadSolution(state, localChanges);
state.localChanges = localChanges;
await writeDataFile(solution, SOLUTION_DATA_FILE_PREFIX);
await doLoadSolution(state, toLocalChanges(solution));
};

export const reloadLastResolutions = async (state: ExtensionState) => {
doLoadSolution(state, state.localChanges);
await doLoadSolution(
state,
state.data.localChanges.map((it) => ({ ...it, state: "pending" })),
);

window.showInformationMessage(`Loaded last available resolutions`);
};

const doLoadSolution = async (state: ExtensionState, localChanges: LocalChange[]) => {
state.memFs.removeAll(KONVEYOR_SCHEME);
await writeSolutionsToMemFs(localChanges, state);
const locations = localChanges.map(
({ originalUri: uri }) => new Location(uri, new Position(0, 0)),
);
state.fileModel.updateLocations(locations);
state.mutateData((draft) => {
draft.localChanges = localChanges;
});
};
6 changes: 4 additions & 2 deletions vscode/src/data/virtualStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import * as Diff from "diff";
import path from "path";

import { KONVEYOR_SCHEME, fromRelativeToKonveyor } from "../utilities";
import { Immutable } from "immer";

export const toLocalChanges = (solution: GetSolutionResult) =>
export const toLocalChanges = (solution: GetSolutionResult): LocalChange[] =>
solution.changes.map(({ modified, original, diff }) => ({
modifiedUri: fromRelativeToKonveyor(modified),
originalUri: Uri.from({
scheme: "file",
path: path.join(workspace.workspaceFolders?.[0].uri.fsPath ?? "", original),
}),
diff,
state: "pending",
}));

export const writeSolutionsToMemFs = async (
localChanges: LocalChange[],
localChanges: Immutable<LocalChange[]>,
{ memFs }: ExtensionState,
) => {
// TODO: implement logic for deleted/added files
Expand Down
2 changes: 1 addition & 1 deletion vscode/src/diffView/copyCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FileItem, toUri } from "./fileModel";
import { ExtensionState } from "src/extensionState";

export async function copyDiff(item: FileItem | vscode.Uri | unknown, state: ExtensionState) {
const localChanges = state.localChanges;
const localChanges = state.data.localChanges;
const uri = toUri(item);
if (!uri) {
console.error("Failed to copy diff. Unknown URI.", item, uri);
Expand Down
37 changes: 35 additions & 2 deletions vscode/src/diffView/register.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import * as vscode from "vscode";
import { KonveyorTreeDataProvider } from "./fileModel";
import { Navigation } from "./navigation";
import { ExtensionState } from "src/extensionState";
import { ExtensionData, ExtensionState } from "src/extensionState";
import { KONVEYOR_READ_ONLY_SCHEME, KONVEYOR_SCHEME } from "../utilities";
import KonveyorReadOnlyProvider from "../data/readOnlyStorage";
import { Immutable } from "immer";
import { LocalChange } from "@editor-extensions/shared";

export function registerDiffView({
extensionContext: context,
memFs,
fileModel: model,
}: ExtensionState): void {
}: ExtensionState): (data: Immutable<ExtensionData>) => void {
context.subscriptions.push(
vscode.workspace.registerFileSystemProvider(KONVEYOR_SCHEME, memFs, {
isCaseSensitive: true,
Expand Down Expand Up @@ -39,4 +41,35 @@ export function registerDiffView({
readOnlyProvider,
),
);

const lastLocalChanges: LocalChange[] = [];
return async (data: Immutable<ExtensionData>) => {
const locations = data.localChanges
.filter((change) => change.state === "pending")
.map(({ originalUri: uri }) => new vscode.Location(uri, new vscode.Position(0, 0)));
model.updateLocations(locations);

const hasChanged = (it: unknown, index: number) => lastLocalChanges[index] !== it;
const copyFromTo = (change: LocalChange) =>
change.state === "discarded"
? [change.originalUri, change.modifiedUri]
: [change.modifiedUri, change.originalUri];

await Promise.all(
data.localChanges
.map((change, index): [LocalChange, number] => [change, index])
.filter(([change, index]) => hasChanged(change, index))
.filter(([{ state }]) => state === "applied" || state === "discarded")
.map(([change, index]): [LocalChange, number, vscode.Uri[]] => [
change,
index,
copyFromTo(change),
])
.map(([change, index, [fromUri, toUri]]) =>
vscode.workspace.fs.copy(fromUri, toUri, { overwrite: true }).then(() => {
lastLocalChanges[index] = change;
}),
),
);
};
}
77 changes: 44 additions & 33 deletions vscode/src/diffView/solutionCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,26 @@ import * as vscode from "vscode";
import { ExtensionState } from "src/extensionState";
import { fromRelativeToKonveyor, KONVEYOR_READ_ONLY_SCHEME } from "../utilities";
import { FileItem, toUri } from "./fileModel";
import { LocalChange } from "@editor-extensions/shared";
import { Immutable } from "immer";

export const applyAll = async (state: ExtensionState) => {
const localChanges = state.localChanges;
await Promise.all(
localChanges.map(({ originalUri, modifiedUri }) =>
vscode.workspace.fs.copy(modifiedUri, originalUri, { overwrite: true }),
),
);
const sidebarProvider = state.webviewProviders?.get("sidebar");
sidebarProvider?.webview?.postMessage({
type: "solutionConfirmation",
data: { confirmed: true, solution: null },
const localChanges = state.data.localChanges;

state.mutateData((draft) => {
draft.localChanges = localChanges.map((it) => ({ ...it, state: "applied" }));
});
//TODO: need to keep solutions view and analysis view in sync based on these actions

vscode.window.showInformationMessage(`All resolutions applied successfully`);
state.fileModel.updateLocations([]);
};

export const discardAll = async (state: ExtensionState) => {
const localChanges = state.localChanges;
await Promise.all(
localChanges.map(({ originalUri, modifiedUri }) =>
vscode.workspace.fs.copy(originalUri, modifiedUri, { overwrite: true }),
),
);
state.fileModel.updateLocations([]);
const localChanges = state.data.localChanges;

state.mutateData((draft) => {
draft.localChanges = localChanges.map((it) => ({ ...it, state: "discarded" }));
});

vscode.window.showInformationMessage(`Discarded all resolutions`);
};

Expand Down Expand Up @@ -64,15 +58,12 @@ export const viewFixInDiffEditor = async (uri: vscode.Uri, preserveFocus: boolea
);

export const applyFile = async (item: FileItem | vscode.Uri | unknown, state: ExtensionState) => {
const originalUri = toUri(item);
if (!originalUri) {
vscode.window.showErrorMessage("Failed to apply changes");
console.error("Failed to apply changes", item, originalUri);
return;
const index = getChangeIndex(item, "Failed to apply changes", state.data.localChanges);
if (index > -1) {
state.mutateData((draft) => {
draft.localChanges[index].state = "applied";
});
}
const modifiedUri = fromRelativeToKonveyor(vscode.workspace.asRelativePath(originalUri));
await vscode.workspace.fs.copy(modifiedUri, originalUri, { overwrite: true });
state.fileModel.markedAsApplied(originalUri);
};

interface ApplyBlockArgs {
Expand Down Expand Up @@ -102,13 +93,33 @@ export const applyBlock = async ({ originalUri, originalWithModifiedChanges }: A
};

export const discardFile = async (item: FileItem | vscode.Uri | unknown, state: ExtensionState) => {
const index = getChangeIndex(item, "Failed to discard changes", state.data.localChanges);
if (index > -1) {
state.mutateData((draft) => {
draft.localChanges[index].state = "discarded";
});
}
};

const getChangeIndex = (
item: FileItem | vscode.Uri | unknown,
errorMsg: string,
localChanges: Immutable<LocalChange[]>,
) => {
const originalUri = toUri(item);
if (!originalUri) {
vscode.window.showErrorMessage("Failed to discard changes");
console.error("Failed to discard changes", item, originalUri);
return;
vscode.window.showErrorMessage(`${errorMsg}(unknown URI)`);
console.error(`${errorMsg}(unknown URI)`, item, originalUri);
return -1;
}
const index = localChanges.findIndex(
(it) => it.originalUri.toString() === originalUri.toString(),
);

if (index < 0) {
vscode.window.showErrorMessage(`${errorMsg}(unknown index)`);
console.error(`${errorMsg}(unknown index)`, item, originalUri);
return -1;
}
const modifiedUri = fromRelativeToKonveyor(vscode.workspace.asRelativePath(originalUri));
await vscode.workspace.fs.copy(originalUri, modifiedUri, { overwrite: true });
state.fileModel.markedAsApplied(originalUri);
return index;
};
Loading

0 comments on commit 69e3864

Please sign in to comment.