From 3e90578517fc9e082de297628be5ed899136339d Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Mon, 17 Feb 2025 19:21:41 +0000 Subject: [PATCH] fix: improve code base and clean-up logic - break-out large functions - move redundant logic into helper functions - improve error handling - improve type checking --- gnome-extension/src/extension.ts | 371 ++++++++++++------------------- 1 file changed, 137 insertions(+), 234 deletions(-) diff --git a/gnome-extension/src/extension.ts b/gnome-extension/src/extension.ts index 53ea98e..c960550 100644 --- a/gnome-extension/src/extension.ts +++ b/gnome-extension/src/extension.ts @@ -35,6 +35,8 @@ interface EditorPath { isDefault?: boolean; } +const FILE_URI_PREFIX = 'file://'; + export default class VSCodeWorkspacesExtension extends Extension { gsettings?: Gio.Settings; @@ -69,6 +71,7 @@ export default class VSCodeWorkspacesExtension extends Extension { }, ]; private readonly _iconNames = ['vscode', 'code', 'vscodium', 'codium', 'code-insiders']; + private readonly FILE_URI_PREFIX: string = FILE_URI_PREFIX; enable() { this.gsettings = this.getSettings(); @@ -177,11 +180,17 @@ export default class VSCodeWorkspacesExtension extends Extension { } _setSettings() { - this._newWindow = this.gsettings!.get_value('new-window').deepUnpack() ?? false; - this._editorLocation = this.gsettings!.get_value('editor-location').deepUnpack() ?? 'auto'; - this._refreshInterval = this.gsettings!.get_value('refresh-interval').deepUnpack() ?? 300; - this._preferCodeWorkspaceFile = this.gsettings!.get_value('prefer-workspace-file').deepUnpack() ?? false; - this._debug = this.gsettings!.get_value('debug').deepUnpack() ?? false; + + if (!this.gsettings) { + this._log('Settings not found'); + return; + } + + this._newWindow = this.gsettings.get_value('new-window').deepUnpack() ?? false; + this._editorLocation = this.gsettings.get_value('editor-location').deepUnpack() ?? 'auto'; + this._refreshInterval = this.gsettings.get_value('refresh-interval').deepUnpack() ?? 300; + this._preferCodeWorkspaceFile = this.gsettings.get_value('prefer-workspace-file').deepUnpack() ?? false; + this._debug = this.gsettings.get_value('debug').deepUnpack() ?? false; this._log(`Workspaces Extension enabled`); this._log(`New Window: ${this._newWindow}`); @@ -389,277 +398,171 @@ export default class VSCodeWorkspacesExtension extends Extension { (this._indicator?.menu as PopupMenu.PopupMenu).addMenuItem(comboBoxSubMenu); } - _iterateWorkspaceDir(dir: Gio.File, callback: (workspace: Workspace) => void) { + // Parses a workspace.json file in a given directory and returns a Workspace object or null + _parseWorkspaceJson(workspaceStoreDir: Gio.File): Workspace | null { try { - const enumerator = dir.enumerate_children( - 'standard::*,unix::uid', - Gio.FileQueryInfoFlags.NONE, - null + const workspaceFile = Gio.File.new_for_path( + GLib.build_filenamev([workspaceStoreDir.get_path()!, 'workspace.json']) ); - try { - let info: Gio.FileInfo | null; - while ((info = enumerator.next_file(null)) !== null) { - try { - const workspaceStoreDir = enumerator.get_child(info); - - this._log(`Checking ${workspaceStoreDir.get_path()}`); - - const workspaceFile = Gio.File.new_for_path( - GLib.build_filenamev([workspaceStoreDir.get_path()!, 'workspace.json']) - ); - - if (!workspaceFile.query_exists(null)) { - this._log(`No workspace.json found in ${workspaceStoreDir.get_path()}`); - continue; - } - - const [, contents] = workspaceFile.load_contents(null); - const decoder = new TextDecoder(); - const json = JSON.parse(decoder.decode(contents)); - - const workspaceURI = (json.folder || json.workspace) as string | undefined; - if (!workspaceURI) { - this._log('No folder or workspace property found in workspace.json'); - continue; - } - - this._log(`Found workspace.json in ${workspaceStoreDir.get_path()} with ${workspaceURI}`); - - const newWorkspace = { - uri: workspaceURI, - storeDir: workspaceStoreDir, - }; - - const pathToWorkspace = Gio.File.new_for_uri(newWorkspace.uri); - - if (!pathToWorkspace.query_exists(null)) { - this._log( - `Workspace does not exist and will be removed from the list: ${pathToWorkspace.get_path()}` - ); - this._workspaces.delete(newWorkspace); - - const trashRes = newWorkspace.storeDir.trash(null); - const workspaceName = GLib.path_get_basename(newWorkspace.uri); - - if (!trashRes) { - this._log(`Failed to move ${workspaceName} to trash`); - return; - } - - this._log(`Workspace Trashed: ${workspaceName}`); - continue; - } - - callback(newWorkspace); - - const workspaceExists = Array.from(this._workspaces).some(workspace => { - return workspace.uri === workspaceURI; - }); - - if (workspaceExists) { - this._log(`Workspace already exists in recent workspaces: ${workspaceURI}`); - continue; - } + if (!workspaceFile.query_exists(null)) { + this._log(`No workspace.json found in ${workspaceStoreDir.get_path()}`); + return null; + } + const [, contents] = workspaceFile.load_contents(null); + const decoder = new TextDecoder(); + const json = JSON.parse(decoder.decode(contents)); + const workspaceURI = (json.folder || json.workspace) as string | undefined; + if (!workspaceURI) { + this._log('No folder or workspace property found in workspace.json'); + return null; + } + this._log(`Parsed workspace.json in ${workspaceStoreDir.get_path()} with ${workspaceURI}`); + return { uri: workspaceURI, storeDir: workspaceStoreDir }; + } catch (error) { + logError(error as object, 'Failed to parse workspace.json'); + return null; + } + } - if (this._workspaces.has(newWorkspace)) { - this._log(`Workspace already exists: ${newWorkspace}`); - continue; - } + _iterateWorkspaceDir(dir: Gio.File, callback: (workspace: Workspace) => void) { + let enumerator: Gio.FileEnumerator | null = null; + try { + enumerator = dir.enumerate_children('standard::*,unix::uid', Gio.FileQueryInfoFlags.NONE, null); + let info: Gio.FileInfo | null; + while ((info = enumerator.next_file(null)) !== null) { + const workspaceStoreDir = enumerator.get_child(info); + this._log(`Checking ${workspaceStoreDir.get_path()}`); + const workspace = this._parseWorkspaceJson(workspaceStoreDir); + if (!workspace) continue; - this._workspaces.add(newWorkspace); - } catch (error) { - logError(error as object, 'Failed to parse workspace.json'); + const pathToWorkspace = Gio.File.new_for_uri(workspace.uri); + if (!pathToWorkspace.query_exists(null)) { + this._log(`Workspace does not exist and will be removed: ${pathToWorkspace.get_path()}`); + this._workspaces.delete(workspace); + const trashRes = workspace.storeDir?.trash(null); + if (!trashRes) { + this._log(`Failed to move workspace to trash: ${workspace.uri}`); continue; } + this._log(`Workspace trashed: ${workspace.uri}`); + continue; } - } finally { - const enumCloseRes = enumerator.close(null); - if (!enumCloseRes) { - throw new Error('Failed to close enumerator'); + callback(workspace); + // Avoid adding duplicates + if ([...this._workspaces].some(ws => ws.uri === workspace.uri)) { + this._log(`Workspace already exists: ${workspace.uri}`); + continue; } + this._workspaces.add(workspace); } } catch (error) { - logError(error as object, 'Failed to iterate workspace directory'); + logError(error as object, 'Error iterating workspace directory'); + } finally { + if (enumerator) { + if (!enumerator.close(null)) { + this._log('Failed to close enumerator'); + } + } + } + } + + // Create a RecentWorkspace entry from a Workspace + _createRecentWorkspaceEntry(workspace: Workspace): RecentWorkspace { + let workspaceName = GLib.path_get_basename(workspace.uri); + if (workspaceName.endsWith('.code-workspace')) { + workspaceName = workspaceName.replace('.code-workspace', ''); } + return { + name: workspaceName, + path: workspace.uri, + softRemove: () => { + this._log(`Moving Workspace to Trash: ${workspaceName}`); + this._workspaces.delete(workspace); + this._recentWorkspaces = new Set( + Array.from(this._recentWorkspaces).filter( + recentWorkspace => recentWorkspace.path !== workspace.uri + ) + ); + const trashRes = workspace.storeDir?.trash(null); + if (!trashRes) { + this._log(`Failed to move ${workspaceName} to trash`); + return; + } + this._log(`Workspace Trashed: ${workspaceName}`); + this._createMenu(); + }, + removeWorkspaceItem: () => { + this._log(`Removing workspace: ${workspaceName}`); + this._workspaces.delete(workspace); + this._recentWorkspaces = new Set( + Array.from(this._recentWorkspaces).filter( + recentWorkspace => recentWorkspace.path !== workspace.uri + ) + ); + workspace.storeDir?.delete(null); + this._createMenu(); + }, + }; } _getRecentWorkspaces() { try { - const dir = Gio.File.new_for_path(this._activeEditor?.workspacePath!); - - this._iterateWorkspaceDir(dir, workspace => { - //! This callback checks if the workspace exists and if it is a directory, it checks if there is a `.code-workspace` file in the directory - - const pathToWorkspace = Gio.File.new_for_uri(workspace.uri); - - // we will only execute the below if the setting is active - + const activeEditorPath = this._activeEditor?.workspacePath; + if (!activeEditorPath) return; + const dir = Gio.File.new_for_path(activeEditorPath); + this._iterateWorkspaceDir(dir, (workspace: Workspace) => { + // If not preferring .code-workspace file, skip extra check if (!this._preferCodeWorkspaceFile) { - this._log(`Not preferring code-workspace file - continuing: ${workspace.uri}`); + this._log(`Not preferring code-workspace file for ${workspace.uri}`); return; } - - if ( - pathToWorkspace.query_file_type(Gio.FileQueryInfoFlags.NONE, null) !== - Gio.FileType.DIRECTORY - ) { - this._log(`Not a directory - continuing: ${pathToWorkspace.get_path()}`); + const pathToWorkspace = Gio.File.new_for_uri(workspace.uri); + if (pathToWorkspace.query_file_type(Gio.FileQueryInfoFlags.NONE, null) !== Gio.FileType.DIRECTORY) { + this._log(`Not a directory: ${pathToWorkspace.get_path()}`); return; } - - // Check if there is a file with a `.code-workspace` extension in the directory - - // look for children, and find one with the `.code-workspace` extension - const enumerator = pathToWorkspace.enumerate_children( - 'standard::*,unix::uid', - Gio.FileQueryInfoFlags.NONE, - null - ); - + const enumerator = pathToWorkspace.enumerate_children('standard::*,unix::uid', Gio.FileQueryInfoFlags.NONE, null); let info: Gio.FileInfo | null; - let workspaceFilePath: string | null = null; - while ((info = enumerator.next_file(null)) !== null) { const file = enumerator.get_child(info); - if (file.get_basename()?.endsWith('.code-workspace')) { workspaceFilePath = file.get_path(); break; } } - - const enumCloseRes = enumerator.close(null); - - if (!enumCloseRes) { + if (!enumerator.close(null)) { throw new Error('Failed to close enumerator'); } - - this._log(`Checking for ${workspaceFilePath}`); - - if (!workspaceFilePath) { - this._log(`Failed to get workspace file path`); - return; - } - + this._log(`Checked for .code-workspace: ${workspaceFilePath}`); + if (!workspaceFilePath) return; const workspaceFile = Gio.File.new_for_path(workspaceFilePath); if (!workspaceFile.query_exists(null)) { - this._log(`No .code-workspace file found in ${workspace.uri} - opening directory`); + this._log(`.code-workspace file does not exist in ${workspace.uri}`); return; } - + // If workspace file exists, remove the directory entry in favor this._log(`Found .code-workspace file in ${workspace.uri}`); - - // Check if a workspace with the same uri as the workspaceFile exists - const workspaceFileExists = Array.from(this._workspaces).some(_workspace => { - return _workspace.uri === workspaceFile.get_uri(); - }); - - if (!workspaceFileExists) { - return; - } - - this._log(`Workspace already exists in recent workspaces - Removing directory workspace in favour of code-workspace file: ${workspaceFile.get_uri()}`); - this._workspaces.delete(workspace); - - // remove the directory workspace from the cache - const recentWorkspaceObject = Array.from(this._recentWorkspaces).find( - recentWorkspace => recentWorkspace.path === workspace.uri - ); - - recentWorkspaceObject?.softRemove(); - + const recentItem = Array.from(this._recentWorkspaces).find(rw => rw.path === workspace.uri); + recentItem?.softRemove(); this._recentWorkspaces = new Set( - Array.from(this._recentWorkspaces).filter( - recentWorkspace => recentWorkspace.path !== workspace.uri - ) + Array.from(this._recentWorkspaces).filter(rw => rw.path !== workspace.uri) ); }); - // sort the workspace files by access time - this._workspaces = new Set(Array.from(this._workspaces).sort((a, b) => { - - const aInfo = Gio.File.new_for_uri(a.uri).query_info('standard::*,unix::atime', Gio.FileQueryInfoFlags.NONE, null); - const bInfo = Gio.File.new_for_uri(b.uri).query_info('standard::*,unix::atime', Gio.FileQueryInfoFlags.NONE, null); - - if (!aInfo || !bInfo) { - this._log(`No file info found for ${a} or ${b}`); - return 0; - } - - const aAccessTime = aInfo.get_attribute_uint64('unix::atime'); - const bAccessTime = bInfo.get_attribute_uint64('unix::atime'); - - if (aAccessTime > bAccessTime) { - return -1; - } - - if (aAccessTime < bAccessTime) { - return 1; - } - - return 0; - })); - // this._log the Set of workspaces, given that .values() returns an iterator - this._log(`[Workspace Cache]: ${Array.from(this._workspaces).map(workspace => workspace.uri)}`); - - this._recentWorkspaces = new Set( - Array.from(this._workspaces).map(workspace => { - let workspaceName = GLib.path_get_basename(workspace.uri); - - // if there is`.code-workspace` included in the workspaceName, remove it - if (workspaceName.endsWith('.code-workspace')) { - workspaceName = workspaceName.replace('.code-workspace', ''); - } - - return { - name: workspaceName, - path: workspace.uri, - softRemove: () => { - this._log(`Moving Workspace to Trash: ${workspaceName}`); - // Purge from the recent workspaces - this._workspaces.delete(workspace); - // Purge from the cache - this._recentWorkspaces = new Set( - Array.from(this._recentWorkspaces).filter( - recentWorkspace => recentWorkspace.path !== workspace.uri - ) - ); - - // now remove the workspaceStore directory - const trashRes = workspace.storeDir?.trash(null); - - if (!trashRes) { - this._log(`Failed to move ${workspaceName} to trash`); - return; - } - - this._log(`Workspace Trashed: ${workspaceName}`); - - // Refresh the menu to reflect the changes - this._createMenu(); - }, - removeWorkspaceItem: () => { - this._log(`Removing workspace: ${workspaceName}`); - // Purge from the recent workspaces - this._workspaces.delete(workspace); - // Purge from the cache - this._recentWorkspaces = new Set( - Array.from(this._recentWorkspaces).filter( - recentWorkspace => recentWorkspace.path !== workspace.uri - ) - ); - // now remove the workspace directory - workspace.storeDir?.delete(null); - this._createMenu(); - }, - }; - }) - ); + // Sort workspaces by access time, most recently active first + const sortedWorkspaces = Array.from(this._workspaces).sort((a, b) => { + const aInfo = Gio.File.new_for_uri(a.uri).query_info('unix::atime', Gio.FileQueryInfoFlags.NONE, null); + const bInfo = Gio.File.new_for_uri(b.uri).query_info('unix::atime', Gio.FileQueryInfoFlags.NONE, null); + const aAtime = aInfo ? aInfo.get_attribute_uint64('unix::atime') : 0; + const bAtime = bInfo ? bInfo.get_attribute_uint64('unix::atime') : 0; + return bAtime - aAtime; + }); - // this._log the Set of recent workspaces, given that .values() returns an iterator - this._log(`[Recent Workspaces]: ${Array.from(this._recentWorkspaces).map(workspace => workspace.path)}`); + this._log(`[Workspace Cache]: ${sortedWorkspaces.map(ws => ws.uri)}`); + this._recentWorkspaces = new Set(sortedWorkspaces.map(ws => this._createRecentWorkspaceEntry(ws))); + this._log(`[Recent Workspaces]: ${Array.from(this._recentWorkspaces).map(rw => rw.path)}`); } catch (e) { logError(e as object, 'Failed to load recent workspaces'); }