diff --git a/package-lock.json b/package-lock.json index 523cca13..708ec9e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "vscode" ], "dependencies": { + "immer": "10.1.1", "js-yaml": "^4.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -5000,6 +5001,16 @@ "dev": true, "license": "MIT" }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", diff --git a/package.json b/package.json index b10a09ef..57a53d39 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/vscode/src/KonveyorGUIWebviewViewProvider.ts b/vscode/src/KonveyorGUIWebviewViewProvider.ts index 97384249..7d93dde7 100644 --- a/vscode/src/KonveyorGUIWebviewViewProvider.ts +++ b/vscode/src/KonveyorGUIWebviewViewProvider.ts @@ -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 }); } @@ -201,7 +201,7 @@ export class KonveyorGUIWebviewViewProvider implements WebviewViewProvider { private _loadInitialContent(webview: Webview) { if (this._isWebviewReady && webview) { - const data = this._extensionState.ruleSets; + const data = this._extensionState.data.ruleSets; webview.postMessage({ type: "loadStoredAnalysis", data, diff --git a/vscode/src/data/loadResults.ts b/vscode/src/data/loadResults.ts index abaad885..da24d784 100644 --- a/vscode/src/data/loadResults.ts +++ b/vscode/src/data/loadResults.ts @@ -9,24 +9,19 @@ import { RULE_SET_DATA_FILE_PREFIX, SOLUTION_DATA_FILE_PREFIX, } from "../utilities"; +import { Immutable } from "immer"; export const loadRuleSets = (state: ExtensionState, ruleSets: RuleSet[]): void => { writeDataFile(ruleSets, RULE_SET_DATA_FILE_PREFIX); - state.ruleSets = ruleSets; 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 = []; }); }; @@ -34,15 +29,17 @@ export const loadSolution = async (state: ExtensionState, solution: GetSolutionR writeDataFile(solution, SOLUTION_DATA_FILE_PREFIX); const localChanges = toLocalChanges(solution); doLoadSolution(state, localChanges); - state.localChanges = localChanges; + state.mutateData((draft) => { + draft.localChanges = localChanges; + }); }; export const reloadLastResolutions = async (state: ExtensionState) => { - doLoadSolution(state, state.localChanges); + doLoadSolution(state, state.data.localChanges); window.showInformationMessage(`Loaded last available resolutions`); }; -const doLoadSolution = async (state: ExtensionState, localChanges: LocalChange[]) => { +const doLoadSolution = async (state: ExtensionState, localChanges: Immutable) => { state.memFs.removeAll(KONVEYOR_SCHEME); await writeSolutionsToMemFs(localChanges, state); const locations = localChanges.map( diff --git a/vscode/src/data/virtualStorage.ts b/vscode/src/data/virtualStorage.ts index 7608e690..16035f78 100644 --- a/vscode/src/data/virtualStorage.ts +++ b/vscode/src/data/virtualStorage.ts @@ -5,6 +5,7 @@ import * as Diff from "diff"; import path from "path"; import { KONVEYOR_SCHEME, fromRelativeToKonveyor } from "../utilities"; +import { Immutable } from "immer"; export const toLocalChanges = (solution: GetSolutionResult) => solution.changes.map(({ modified, original, diff }) => ({ @@ -17,7 +18,7 @@ export const toLocalChanges = (solution: GetSolutionResult) => })); export const writeSolutionsToMemFs = async ( - localChanges: LocalChange[], + localChanges: Immutable, { memFs }: ExtensionState, ) => { // TODO: implement logic for deleted/added files diff --git a/vscode/src/diffView/copyCommands.ts b/vscode/src/diffView/copyCommands.ts index 9a1733e5..1065931c 100644 --- a/vscode/src/diffView/copyCommands.ts +++ b/vscode/src/diffView/copyCommands.ts @@ -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); diff --git a/vscode/src/diffView/solutionCommands.ts b/vscode/src/diffView/solutionCommands.ts index c401cbe6..ed30e4e4 100644 --- a/vscode/src/diffView/solutionCommands.ts +++ b/vscode/src/diffView/solutionCommands.ts @@ -4,7 +4,7 @@ import { fromRelativeToKonveyor, KONVEYOR_READ_ONLY_SCHEME } from "../utilities" import { FileItem, toUri } from "./fileModel"; export const applyAll = async (state: ExtensionState) => { - const localChanges = state.localChanges; + const localChanges = state.data.localChanges; await Promise.all( localChanges.map(({ originalUri, modifiedUri }) => vscode.workspace.fs.copy(modifiedUri, originalUri, { overwrite: true }), @@ -21,7 +21,7 @@ export const applyAll = async (state: ExtensionState) => { }; export const discardAll = async (state: ExtensionState) => { - const localChanges = state.localChanges; + const localChanges = state.data.localChanges; await Promise.all( localChanges.map(({ originalUri, modifiedUri }) => vscode.workspace.fs.copy(originalUri, modifiedUri, { overwrite: true }), diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 1f126a41..5d438f8a 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -1,26 +1,43 @@ import * as vscode from "vscode"; import { KonveyorGUIWebviewViewProvider } from "./KonveyorGUIWebviewViewProvider"; import { registerAllCommands as registerAllCommands } from "./commands"; -import { ExtensionState, SharedState } from "./extensionState"; +import { ExtensionData, ExtensionState } from "./extensionState"; import { ViolationCodeActionProvider } from "./ViolationCodeActionProvider"; import { AnalyzerClient } from "./client/analyzerClient"; import { registerDiffView, KonveyorFileModel } from "./diffView"; import { MemFS } from "./data"; +import { Immutable, produce } from "immer"; class VsCodeExtension { private state: ExtensionState; + private data: Immutable; + private _onDidChange = new vscode.EventEmitter>(); + readonly onDidChangeData = this._onDidChange.event; constructor(context: vscode.ExtensionContext) { + this.data = produce( + { localChanges: [], ruleSets: [], resolutionPanelData: undefined }, + () => {}, + ); + const getData = () => this.data; + const setData = (data: Immutable) => { + this.data = data; + this._onDidChange.fire(this.data); + }; + this.state = { analyzerClient: new AnalyzerClient(context), - sharedState: new SharedState(), webviewProviders: new Map(), extensionContext: context, diagnosticCollection: vscode.languages.createDiagnosticCollection("konveyor"), memFs: new MemFS(), fileModel: new KonveyorFileModel(), - localChanges: [], - ruleSets: [], + get data() { + return getData(); + }, + mutateData: (recipe: (draft: ExtensionData) => void) => { + setData(produce(getData(), recipe)); + }, }; this.initializeExtension(context); @@ -55,6 +72,12 @@ class VsCodeExtension { const resolutionViewProvider = new KonveyorGUIWebviewViewProvider(this.state, "resolution"); this.state.webviewProviders.set("resolution", resolutionViewProvider); + [sidebarProvider, resolutionViewProvider].forEach((provider) => + this.onDidChangeData((data) => + provider.sendMessageToWebview({ type: "onDidChangeData", value: data }), + ), + ); + context.subscriptions.push( vscode.window.registerWebviewViewProvider( KonveyorGUIWebviewViewProvider.SIDEBAR_VIEW_TYPE, diff --git a/vscode/src/extensionState.ts b/vscode/src/extensionState.ts index 57183d10..c16e2fbd 100644 --- a/vscode/src/extensionState.ts +++ b/vscode/src/extensionState.ts @@ -4,44 +4,21 @@ import { MemFS } from "./data/fileSystemProvider"; import { KonveyorGUIWebviewViewProvider } from "./KonveyorGUIWebviewViewProvider"; import * as vscode from "vscode"; import { LocalChange, RuleSet } from "@editor-extensions/shared"; -import { EventEmitter } from "vscode"; +import { Immutable } from "immer"; -type SharedStateEventData = any; // Or use a specific type if needed - -export class SharedState { - private state: Map = new Map(); - private emitter: EventEmitter = new EventEmitter(); - - get(key: string) { - return this.state.get(key); - } - - set(key: string, value: any) { - this.state.set(key, value); - this.emitter.fire({ key, value }); // Emit an event for the key when data is set - } - - // Subscribe to an event for a specific key - on(callback: (data: { key: string; value: SharedStateEventData }) => void) { - // Listen for events and pass data to the callback - this.emitter.event(callback); - } - - // Remove a listener for the event - off(callback: (data: { key: string; value: SharedStateEventData }) => void) { - // Remove the specific listener callback - this.emitter.event(callback); - } +export interface ExtensionData { + localChanges: LocalChange[]; + ruleSets: RuleSet[]; + resolutionPanelData: any; } export interface ExtensionState { analyzerClient: AnalyzerClient; - sharedState: SharedState; webviewProviders: Map; extensionContext: vscode.ExtensionContext; diagnosticCollection: vscode.DiagnosticCollection; memFs: MemFS; fileModel: KonveyorFileModel; - localChanges: LocalChange[]; - ruleSets: RuleSet[]; + data: Immutable; + mutateData: (recipe: (draft: ExtensionData) => void) => void; } diff --git a/vscode/src/webviewMessageHandler.ts b/vscode/src/webviewMessageHandler.ts index af3b2816..f6a35431 100644 --- a/vscode/src/webviewMessageHandler.ts +++ b/vscode/src/webviewMessageHandler.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { ExtensionState } from "./extensionState"; import { loadStateFromDataFolder } from "./data"; -import { Change, GetSolutionResult } from "@editor-extensions/shared"; +import { Change } from "@editor-extensions/shared"; import { fromRelativeToKonveyor } from "./utilities"; import path from "path"; @@ -10,18 +10,7 @@ export function setupWebviewMessageListener(webview: vscode.Webview, state: Exte switch (message.command) { case "getSolution": { const { violation, incident } = message; - const [analysisResults, solution] = await loadStateFromDataFolder(); - - const mockSolution: GetSolutionResult = { - errors: [], - changes: [ - { - original: "src/main/java/com/redhat/coolstore/service/CatalogService.java", - modified: "src/main/java/com/redhat/coolstore/service/CatalogService.java", - diff: "diff --git a/src/main/java/com/redhat/coolstore/service/CatalogService.java b/src/main/java/com/redhat/coolstore/service/CatalogService.java\nindex 422a3f4..9a6feff 100644\n--- a/src/main/java/com/redhat/coolstore/service/CatalogService.java\n+++ b/src/main/java/com/redhat/coolstore/service/CatalogService.java\n@@ -9,12 +9,12 @@ import javax.persistence.criteria.CriteriaBuilder;\n import javax.persistence.criteria.CriteriaQuery;\n import javax.persistence.criteria.Root;\n \n-import javax.ejb.Stateless;\n+import jakarta.enterprise.context.ApplicationScoped;\n import javax.persistence.EntityManager;\n \n import com.redhat.coolstore.model.*;\n \n-@Stateless\n+@ApplicationScoped\n public class CatalogService {\n \n @Inject\n", - }, - ], - }; + const [, solution] = await loadStateFromDataFolder(); vscode.commands.executeCommand("konveyor.diffView.focus"); vscode.commands.executeCommand("konveyor.showResolutionPanel");