From c06ebaa57cdab709ca06efd2528f33d8cdf6a26f Mon Sep 17 00:00:00 2001
From: alemi <me@alemi.dev>
Date: Sat, 15 Feb 2025 18:42:20 +0100
Subject: [PATCH] fix: codemp object cache to store client and ws

vscode shared state must be json serializable, ouch...
---
 src/commands/buffers.ts    | 35 ++++++++++++-----------
 src/commands/client.ts     | 57 +++++++++++++++++++-------------------
 src/commands/workspaces.ts | 20 ++++++-------
 src/extension.ts           | 53 +++++++++++++++++++++++++++++++----
 src/tree.ts                | 24 ++++++++--------
 5 files changed, 112 insertions(+), 77 deletions(-)

diff --git a/src/commands/buffers.ts b/src/commands/buffers.ts
index bf71e70..e073746 100644
--- a/src/commands/buffers.ts
+++ b/src/commands/buffers.ts
@@ -1,8 +1,7 @@
 import * as vscode from 'vscode';
 import * as codemp from 'codemp';
 import * as mapping from "../mapping";
-import { workspaceState } from "./workspaces";
-import { LOGGER, provider } from '../extension';
+import { COC, LOGGER, provider } from '../extension';
 
 let singles: Map<string, boolean> = new Map();
 let locks: Map<string, string> = new Map();
@@ -16,7 +15,7 @@ export async function apply_changes_to_buffer(path: string, controller: codemp.B
 
 	singles.set(path, true);
 	while (true) {
-		if (workspaceState.workspace === null) {
+		if (!COC.has_workspace()) {
 			LOGGER.info(`left workspace, unregistering buffer controller '${path}' callback`);
 			controller.clearCallback();
 			return;
@@ -34,12 +33,12 @@ export async function apply_changes_to_buffer(path: string, controller: codemp.B
 			if (codemp.hash(editor.document.getText()) !== event.hash) {
 				if (autoResync) {
 					vscode.window.showWarningMessage("Out of Sync, resynching...");
-					await resync(path, workspaceState.workspace, editor, 20);
+					await resync(path, COC.workspace(), editor, 20);
 				} else {
 					controller.clearCallback();
 					const selection = await vscode.window.showWarningMessage('Out of Sync', 'Resync');
-					if (selection !== undefined && workspaceState.workspace) {
-						await resync(path, workspaceState.workspace, editor, 20);
+					if (selection !== undefined && COC.workspace()) {
+						await resync(path, COC.workspace(), editor, 20);
 						controller.callback(async (controller: codemp.BufferController) =>
 							await apply_changes_to_buffer(controller.path(), controller)
 						);
@@ -63,7 +62,7 @@ export async function apply_changes_to_buffer(path: string, controller: codemp.B
 }
 
 export async function attach_to_remote_buffer(buffer_name: string, set_content?: boolean): Promise<codemp.BufferController | undefined> {
-	if (workspaceState.workspace === null) {
+	if (!COC.has_workspace()) {
 		vscode.window.showErrorMessage("join a Workspace first");
 		return;
 	}
@@ -87,7 +86,7 @@ export async function attach_to_remote_buffer(buffer_name: string, set_content?:
 	let doc = await vscode.workspace.openTextDocument(path);
 	let editor = await vscode.window.showTextDocument(doc, { preserveFocus: false })
 	await editor.edit((editor) => editor.setEndOfLine(vscode.EndOfLine.LF)); // set LF for EOL sequence
-	let buffer: codemp.BufferController = await workspaceState.workspace.attachBuffer(buffer_name);
+	let buffer: codemp.BufferController = await COC.workspace().attachBuffer(buffer_name);
 
 	// wait for server changes
 	// TODO poll never unblocks, so this dirty fix is necessary
@@ -149,7 +148,7 @@ export async function attach_to_remote_buffer(buffer_name: string, set_content?:
 }
 
 export async function attach(selected: vscode.TreeItem | undefined) {
-	if (workspaceState.workspace === null) return vscode.window.showWarningMessage("Join a workspace first");
+	if (!COC.has_workspace()) return vscode.window.showWarningMessage("Join a workspace first");
 	let buffer_name: string | undefined;
 	if (selected !== undefined && selected.label !== undefined) {
 		if (typeof (selected.label) === 'string') {
@@ -158,14 +157,14 @@ export async function attach(selected: vscode.TreeItem | undefined) {
 			buffer_name = selected.label.label; // TODO ughh what is this api?
 		}
 	} else {
-		buffer_name = await vscode.window.showQuickPick(workspaceState.workspace.searchBuffers(), { placeHolder: "buffer to attach to:" }, undefined);
+		buffer_name = await vscode.window.showQuickPick(COC.workspace().searchBuffers(), { placeHolder: "buffer to attach to:" }, undefined);
 	}
 	if (!buffer_name) return;
 	await attach_to_remote_buffer(buffer_name);
 }
 
 export async function detach(selected: vscode.TreeItem | undefined) {
-	if (workspaceState.workspace === null) return vscode.window.showWarningMessage("Not in a workspace");
+	if (!COC.has_workspace()) return vscode.window.showWarningMessage("Not in a workspace");
 	let buffer_name: string | undefined;
 	if (selected !== undefined && selected.label !== undefined) {
 		if (typeof (selected.label) === 'string') {
@@ -174,12 +173,12 @@ export async function detach(selected: vscode.TreeItem | undefined) {
 			buffer_name = selected.label.label; // TODO ughh what is this api?
 		}
 	} else {
-		buffer_name = await vscode.window.showQuickPick(workspaceState.workspace.activeBuffers(), { placeHolder: "buffer to detach from:" }, undefined);
+		buffer_name = await vscode.window.showQuickPick(COC.workspace().activeBuffers(), { placeHolder: "buffer to detach from:" }, undefined);
 	}
 	if (!buffer_name) return;
-	let controller = workspaceState.workspace.getBuffer(buffer_name);
+	let controller = COC.workspace().getBuffer(buffer_name);
 	if (controller) controller.clearCallback();
-	workspaceState.workspace.detachBuffer(buffer_name);
+	COC.workspace().detachBuffer(buffer_name);
 	mapping.bufferMapper.remove(buffer_name);
 	vscode.window.showInformationMessage(`Detached from buffer ${buffer_name}`)
 	provider.refresh();
@@ -187,7 +186,7 @@ export async function detach(selected: vscode.TreeItem | undefined) {
 
 
 export async function share() {
-	if (workspaceState.workspace === null) return vscode.window.showWarningMessage("Join a workspace first");
+	if (!COC.workspace()) return vscode.window.showWarningMessage("Join a workspace first");
 	let buffer_name: string | undefined;
 	if (vscode.window.activeTextEditor !== null) {
 		buffer_name = vscode.window.activeTextEditor?.document.uri.toString();
@@ -199,12 +198,12 @@ export async function share() {
 	let workspacePath: string = vscode.workspace.workspaceFolders[0].uri.toString();
 	buffer_name = buffer_name.replace(workspacePath, "").substring(1); //vscode.workspace.asRelativePath doesn't work properly with other extensions like ssh, substring(1) to remove "/"
 	console.log("After: " + buffer_name);
-	await workspaceState.workspace.createBuffer(buffer_name);
+	await COC.workspace().createBuffer(buffer_name);
 	await attach_to_remote_buffer(buffer_name, true);
 }
 
 export async function sync(selected: vscode.TreeItem | undefined) {
-	if (workspaceState.workspace === null) return vscode.window.showWarningMessage("Join a workspace first");
+	if (!COC.has_workspace()) return vscode.window.showWarningMessage("Join a workspace first");
 	let editor;
 	let buffer_name;
 	if (selected !== undefined && selected.label !== undefined) {
@@ -222,7 +221,7 @@ export async function sync(selected: vscode.TreeItem | undefined) {
 		if (buffer_name === undefined) return vscode.window.showWarningMessage("Buffer not synched with codemp");
 	}
 
-	resync(buffer_name, workspaceState.workspace, editor);
+	resync(buffer_name, COC.workspace(), editor);
 }
 
 export async function resync(buffer_name: string, workspace: codemp.Workspace, editor: vscode.TextEditor, tries?: number) {
diff --git a/src/commands/client.ts b/src/commands/client.ts
index 85ca7d2..af61824 100644
--- a/src/commands/client.ts
+++ b/src/commands/client.ts
@@ -2,11 +2,10 @@ import * as vscode from 'vscode';
 import * as codemp from 'codemp';
 import * as mapping from "../mapping";
 import { executeJump, workspaceState } from "./workspaces";
-import { LOGGER, provider } from '../extension';
+import { COC, LOGGER, provider } from '../extension';
 
 
 // TODO this "global state" should probably live elsewher but we need lo update it from these commands
-export let client: codemp.Client | null = null;
 export let workspace_list: string[] = [];
 export let cursor_disposable: vscode.Disposable | null;
 
@@ -24,13 +23,13 @@ export async function connect() {
 	}
 
 	try {
-		client = await codemp.connect({
+		COC.set_client(await codemp.connect({
 			username: username,
 			password: password,
 			host: config.get<string>("server"),
 			port: config.get<number>("port"),
 			tls: config.get<boolean>("tls"),
-		});
+		}));
 		vscode.window.showInformationMessage("Connected to codemp");
 		provider.refresh();
 		listWorkspaces(); // dont await, run in background
@@ -40,7 +39,7 @@ export async function connect() {
 }
 
 export async function join(selected: vscode.TreeItem | undefined) {
-	if (client === null) return vscode.window.showWarningMessage("Connect first");
+	if (COC.has_client()) return vscode.window.showWarningMessage("Connect first");
 	let workspace_id: string | undefined;
 	if (selected !== undefined && selected.label !== undefined) {
 		if (typeof (selected.label) === 'string') {
@@ -56,8 +55,8 @@ export async function join(selected: vscode.TreeItem | undefined) {
 		let ws = await vscode.window.showWorkspaceFolderPick({ placeHolder: "directory to open workspace into:" });
 		if (ws === undefined) return vscode.window.showErrorMessage("Open a Workspace folder first");
 	}
-	workspaceState.workspace = await client.attachWorkspace(workspace_id);
-	let controller = workspaceState.workspace.cursor();
+	COC.set_workspace(await COC.client().attachWorkspace(workspace_id));
+	let controller = COC.workspace().cursor();
 	controller.callback(cursor_callback);
 
 	let once = true;
@@ -92,9 +91,9 @@ export async function join(selected: vscode.TreeItem | undefined) {
 		}
 	});
 
-	workspaceState.workspace.callback(workspace_callback);
+	COC.workspace().callback(workspace_callback);
 
-	for (let user of workspaceState.workspace.userList()) {
+	for (let user of COC.workspace().userList()) {
 		mapping.colors_cache.set(user.name, new mapping.UserDecoration(user.name));
 	}
 
@@ -104,12 +103,12 @@ export async function join(selected: vscode.TreeItem | undefined) {
 
 async function workspace_callback(controller: codemp.Workspace) {
 	while (true) {
-		if (workspaceState.workspace === null) {
+		if (!COC.has_workspace()) {
 			controller.clearCallback();
 			LOGGER.info("left workspace, stopping receiving events");
 			return;
 		}
-		let event = await workspaceState.workspace.tryRecv();
+		let event = await COC.workspace().tryRecv();
 		if (event === null) break;
 		if (event.type == "leave") {
 			mapping.colors_cache.get(event.value)?.clear()
@@ -125,7 +124,7 @@ async function workspace_callback(controller: codemp.Workspace) {
 async function cursor_callback(controller: codemp.CursorController) {
 	while (true) {
 		let event = await controller.tryRecv();
-		if (workspaceState.workspace === null) {
+		if (!COC.has_workspace()) {
 			controller.clearCallback();
 			LOGGER.info("left workspace, stopping cursor controller");
 			return;
@@ -152,9 +151,9 @@ async function cursor_callback(controller: codemp.CursorController) {
 
 
 export async function listWorkspaces() {
-	if (client === null) return vscode.window.showWarningMessage("Connect first");
-	let workspace_joined = await client.fetchJoinedWorkspaces();
-	let workspace_owned = await client.fetchOwnedWorkspaces();
+	if (!COC.has_client()) return vscode.window.showWarningMessage("Connect first");
+	let workspace_joined = await COC.client().fetchJoinedWorkspaces();
+	let workspace_owned = await COC.client().fetchOwnedWorkspaces();
 	workspace_list = workspace_owned.concat(workspace_joined);
 	provider.refresh();
 }
@@ -162,49 +161,49 @@ export async function listWorkspaces() {
 
 
 export async function createWorkspace() {
-	if (client === null) return vscode.window.showWarningMessage("Connect first");
+	if (!COC.has_client()) return vscode.window.showWarningMessage("Connect first");
 	let workspace_id = await vscode.window.showInputBox({ prompt: "Enter name for workspace" });
 	if (workspace_id === undefined) return;
-	await client.createWorkspace(workspace_id);
+	await COC.client().createWorkspace(workspace_id);
 	vscode.window.showInformationMessage("Created new workspace " + workspace_id);
 	listWorkspaces();
 }
 
 export async function inviteToWorkspace() {
-	if (client === null) return vscode.window.showWarningMessage("Connect first");
+	if (!COC.has_client()) return vscode.window.showWarningMessage("Connect first");
 	let workspace_id = await vscode.window.showQuickPick(workspace_list, { placeHolder: "workspace to invite to:" });
 	if (workspace_id === undefined) return;
 	let user_id = await vscode.window.showInputBox({ prompt: "Name of user to invite" });
 	if (user_id === undefined) return;
-	await client.inviteToWorkspace(workspace_id, user_id);
+	await COC.client().inviteToWorkspace(workspace_id, user_id);
 	vscode.window.showInformationMessage("Invited " + user_id + " into workspace " + workspace_id);
 }
 
 export async function leave() {
-	if (!client) throw "can't leave while disconnected";
-	if (!workspaceState.workspace) throw "can't leave while not in a workspace";
-	workspaceState.workspace.cursor().clearCallback()
-	client.leaveWorkspace(workspaceState.workspace.id());
+	if (!COC.has_client()) throw "can't leave while disconnected";
+	if (!COC.has_workspace()) throw "can't leave while not in a workspace";
+	COC.workspace().cursor().clearCallback()
+	COC.client().leaveWorkspace(COC.workspace().id());
 	if (cursor_disposable !== null) cursor_disposable.dispose();
-	let workspace_id = workspaceState.workspace.id();
-	workspaceState.workspace = null;
+	let workspace_id = COC.workspace().id();
+	COC.clear_workspace();
 	provider.refresh();
 	vscode.window.showInformationMessage("Left workspace " + workspace_id);
 }
 
 export async function deleteWorkspace() {
-	if (client === null) return vscode.window.showWarningMessage("Connect first");
+	if (!COC.has_client()) return vscode.window.showWarningMessage("Connect first");
 	let workspace_id = await vscode.window.showInputBox({ prompt: "Enter workspace's name to delete" });
 	if (workspace_id === undefined) return;
-	await client.deleteWorkspace(workspace_id);
+	await COC.client().deleteWorkspace(workspace_id);
 	vscode.window.showInformationMessage("Deleted workspace " + workspace_id);
 	listWorkspaces();
 }
 
 
 export async function refresh() {
-	if (client === null) return vscode.window.showWarningMessage("Connect first");
-	await client.refresh();
+	if (!COC.has_client()) return vscode.window.showWarningMessage("Connect first");
+	await COC.client().refresh();
 	vscode.window.showInformationMessage("Refreshed Session token");
 }
 
diff --git a/src/commands/workspaces.ts b/src/commands/workspaces.ts
index fd52411..e5f2d4b 100644
--- a/src/commands/workspaces.ts
+++ b/src/commands/workspaces.ts
@@ -1,23 +1,19 @@
 import * as vscode from 'vscode';
-import * as codemp from 'codemp';
 import * as mapping from "../mapping";
-import { client } from "./client"
-import { LOGGER, provider } from '../extension';
+import { COC, provider } from '../extension';
 
 
 export let workspaceState: {
-	workspace: codemp.Workspace | null,
 	follow: string | null,
 	justJumped: boolean,
 } = {
-	workspace: null,
 	follow: null,
 	justJumped: false,
 };
 
 
 export async function jump(selected: vscode.TreeItem | undefined) {
-	if (client === null) return vscode.window.showWarningMessage("Connect first");
+	if (!COC.has_client()) return vscode.window.showWarningMessage("Connect first");
 	let user;
 	if (selected !== undefined && selected.label !== undefined) {
 		if (typeof (selected.label) === 'string') {
@@ -52,23 +48,23 @@ export async function executeJump(user: string) {
 
 export async function createBuffer() {
 	let bufferName: any = (await vscode.window.showInputBox({ prompt: "path of the buffer to create" }));
-	if (workspaceState.workspace === null) return vscode.window.showWarningMessage("Join a workspace first");
-	await workspaceState.workspace.createBuffer(bufferName);
+	if (!COC.has_workspace()) return vscode.window.showWarningMessage("Join a workspace first");
+	await COC.workspace().createBuffer(bufferName);
 	vscode.window.showInformationMessage(`new buffer created :${bufferName}`);
 	provider.refresh();
 }
 
 export async function listBuffers() {
-	if (workspaceState.workspace === null) return vscode.window.showWarningMessage("Join a workspace first");
-	let buffers = workspaceState.workspace.searchBuffers();
+	if (!COC.has_workspace()) return vscode.window.showWarningMessage("Join a workspace first");
+	let buffers = COC.workspace().searchBuffers();
 	vscode.window.showInformationMessage(buffers.join("\n"));
 	provider.refresh();
 }
 
 export async function deleteBuffer() {
 	let bufferName: any = (await vscode.window.showInputBox({ prompt: "path of the buffer to delete" }));
-	if (workspaceState.workspace === null) return vscode.window.showWarningMessage("Join a workspace first");
-	await workspaceState.workspace.deleteBuffer(bufferName);
+	if (!COC.has_workspace()) return vscode.window.showWarningMessage("Join a workspace first");
+	await COC.workspace().deleteBuffer(bufferName);
 	vscode.window.showInformationMessage(`Deleted buffer :${bufferName}`);
 	provider.refresh();
 }
diff --git a/src/extension.ts b/src/extension.ts
index 4d4cdee..13efdd0 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,15 +1,56 @@
 import * as vscode from 'vscode';
 import * as codemp from 'codemp';
-import { client, connect, join, refresh, createWorkspace, inviteToWorkspace, listWorkspaces, leave, deleteWorkspace, version } from './commands/client';
+import { connect, join, refresh, createWorkspace, inviteToWorkspace, listWorkspaces, leave, deleteWorkspace, version } from './commands/client';
 import { CodempTreeProvider } from './tree';
 import * as mapping from './mapping';
-import { workspaceState, jump, listBuffers, createBuffer, deleteBuffer } from './commands/workspaces'
+import { jump, listBuffers, createBuffer, deleteBuffer } from './commands/workspaces'
 import { attach, share, sync, apply_changes_to_buffer, detach } from './commands/buffers'
 
 export let provider = new CodempTreeProvider();
 
 export let LOGGER = vscode.window.createOutputChannel("codemp", { log: true });
 
+export class CodempObjectCache {
+	private _client: codemp.Client | null = null;
+	private _workspace: codemp.Workspace | null = null;
+
+	public client(): codemp.Client {
+		if (this._client === null) throw "Must connect first!";
+		return this._client;
+	}
+
+	public has_client(): boolean {
+		return this._client !== null;
+	}
+
+	public set_client(client: codemp.Client) {
+		this._client = client;
+	}
+
+	public clear_client() {
+		this._client = null;
+	}
+
+	public workspace(): codemp.Workspace {
+		if (this._workspace === null) throw "Must join a workspace first";
+		return this._workspace;
+	}
+
+	public has_workspace(): boolean {
+		return this._workspace !== null;
+	}
+
+	public set_workspace(workspace: codemp.Workspace) {
+		this._workspace = workspace;
+	}
+
+	public clear_workspace() {
+		this._workspace = null;
+	}
+}
+
+export let COC = new CodempObjectCache();
+
 // extension is activated the very first time the command is executed
 export function activate(context: vscode.ExtensionContext) {
 	let config = vscode.workspace.getConfiguration('codemp');
@@ -22,11 +63,11 @@ export function activate(context: vscode.ExtensionContext) {
 	context.subscriptions.push(sub);
 
 	vscode.window.onDidChangeVisibleTextEditors(async (editors: readonly vscode.TextEditor[]) => {
-		if (workspaceState.workspace === null) return;
+		if (!COC.has_workspace()) return;
 		for (let editor of editors) {
 			let path = mapping.bufferMapper.by_editor(editor.document.uri);
 			if (!path) continue;
-			let controller = workspaceState.workspace.getBuffer(path);
+			let controller = COC.workspace().getBuffer(path);
 			if (!controller) continue;
 			await apply_changes_to_buffer(path, controller, true);
 		}
@@ -57,8 +98,8 @@ export function activate(context: vscode.ExtensionContext) {
 }
 
 export async function deactivate() {
-	if (client && workspaceState.workspace) {
-		await client.leaveWorkspace(workspaceState.workspace.id());
+	if (COC.has_client() && COC.has_workspace()) {
+		await COC.client().leaveWorkspace(COC.workspace().id());
 	}
 }
 
diff --git a/src/tree.ts b/src/tree.ts
index 704b736..adf1119 100644
--- a/src/tree.ts
+++ b/src/tree.ts
@@ -1,7 +1,7 @@
 import * as vscode from 'vscode';
-import { client, workspace_list } from './commands/client';
-import { workspaceState } from './commands/workspaces';
+import { workspace_list } from './commands/client';
 import { bufferMapper, colors_cache } from './mapping';
+import { COC } from "./extension";
 
 export class CodempTreeProvider implements vscode.TreeDataProvider<CodempTreeItem> {
 
@@ -22,17 +22,17 @@ export class CodempTreeProvider implements vscode.TreeDataProvider<CodempTreeIte
 		if (element) {
 			switch (element.type) {
 				case Type.CurrentWorkspace:
-					if (workspaceState.workspace === null) return []; // TODO ???? error maybe ???
-					let items = workspaceState.workspace.searchBuffers().map((x) =>
+					if (!COC.has_workspace()) return [];
+					let items = COC.workspace().searchBuffers().map((x) =>
 						new CodempTreeItem(x, Type.Buffer, { active: bufferMapper.bufferToEditorMapping.has(x) })
 					);
 					items.push(new CodempTreeItem("", Type.Placeholder, { expandable: false }));
 					items.push(new CodempTreeItem("Users", Type.UserContainer, { expandable: true }));
 					return items;
 				case Type.WorkspaceContainer:
-					let active = workspaceState.workspace === null;
+					let active = !COC.has_workspace();
 					return workspace_list
-						.filter((x) => workspaceState.workspace == null || x != workspaceState.workspace.id())
+						.filter((x) => x != COC.workspace().id())
 						.map((x) => new CodempTreeItem(x, Type.Workspace, { expandable: false, active: active }));
 
 				case Type.UserContainer:
@@ -44,9 +44,9 @@ export class CodempTreeProvider implements vscode.TreeDataProvider<CodempTreeIte
 
 				case Type.ClientContainer:
 					let info = [];
-					if (client === null) return [];
-					info.push(new CodempTreeItem("username", Type.ClientInfo, { description: client.currentUser().name }));
-					info.push(new CodempTreeItem("uuid", Type.ClientInfo, { description: client.currentUser().uuid }));
+					if (!COC.has_client()) return [];
+					info.push(new CodempTreeItem("username", Type.ClientInfo, { description: COC.client().currentUser().name }));
+					info.push(new CodempTreeItem("uuid", Type.ClientInfo, { description: COC.client().currentUser().uuid }));
 					return info;
 
 				case Type.Placeholder:
@@ -58,14 +58,14 @@ export class CodempTreeProvider implements vscode.TreeDataProvider<CodempTreeIte
 					return [];
 			}
 		} else {
-			if (client === null) {
+			if (!COC.has_client()) {
 				return []; // empty screen with [connect] button
 			}
 
 			let items = [];
 
-			if (workspaceState.workspace !== null) {
-				items.push(new CodempTreeItem(workspaceState.workspace.id(), Type.CurrentWorkspace, { expandable: true }));
+			if (COC.has_workspace()) {
+				items.push(new CodempTreeItem(COC.workspace().id(), Type.CurrentWorkspace, { expandable: true }));
 				items.push(new CodempTreeItem("", Type.Placeholder, {}));
 			}