From 0b03f75ddc9435f48e4d8f084782810aac21d194 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Fri, 21 Mar 2025 00:17:05 -0400 Subject: [PATCH 001/121] initial integration with new changes --- .env | 3 + .prettierrc | 1 + assets/eco-icon.png | Bin 0 -> 518 bytes data/smells.json | 93 +++++ package.json | 536 ++++++++++++------------- src/api/backend.ts | 106 ++--- src/commands/configureWorkspace.ts | 179 +++++++++ src/commands/detectSmells.ts | 259 ++++++++---- src/commands/filterSmells.ts | 72 ++++ src/commands/jumpToSmell.ts | 25 ++ src/commands/openFile.ts | 21 + src/commands/refactorSmell.ts | 105 ++--- src/commands/resetConfiguration.ts | 39 ++ src/commands/showLogs.ts | 312 +++++++------- src/commands/toggleSmellLinting.ts | 89 ++-- src/commands/wipeWorkCache.ts | 72 ++-- src/context/SmellsCacheManager.ts | 136 +++++++ src/context/contextManager.ts | 40 +- src/extension.ts | 323 +++++++-------- src/listeners/fileSaveListener.ts | 48 +++ src/managers/SmellsViewStateManager.ts | 135 +++++++ src/managers/SmellsViewUIManager.ts | 185 +++++++++ src/providers/FilterSmellsProvider.ts | 181 +++++++++ src/providers/SmellsViewProvider.ts | 130 ++++++ src/ui/fileHighlighter.ts | 23 +- src/ui/hoverManager.ts | 29 +- src/ui/lineSelectionManager.ts | 23 +- src/ui/refactorView.ts | 198 --------- src/utils/envConfig.ts | 7 +- src/utils/handleEditorChange.ts | 126 ------ src/utils/handleSmellSettings.ts | 53 --- src/utils/hashDocs.ts | 31 -- src/utils/smellDetails.ts | 74 ---- src/utils/smellsData.ts | 97 +++++ 34 files changed, 2327 insertions(+), 1424 deletions(-) create mode 100644 assets/eco-icon.png create mode 100644 data/smells.json create mode 100644 src/commands/configureWorkspace.ts create mode 100644 src/commands/filterSmells.ts create mode 100644 src/commands/jumpToSmell.ts create mode 100644 src/commands/openFile.ts create mode 100644 src/commands/resetConfiguration.ts create mode 100644 src/context/SmellsCacheManager.ts create mode 100644 src/listeners/fileSaveListener.ts create mode 100644 src/managers/SmellsViewStateManager.ts create mode 100644 src/managers/SmellsViewUIManager.ts create mode 100644 src/providers/FilterSmellsProvider.ts create mode 100644 src/providers/SmellsViewProvider.ts delete mode 100644 src/ui/refactorView.ts delete mode 100644 src/utils/handleEditorChange.ts delete mode 100644 src/utils/handleSmellSettings.ts delete mode 100644 src/utils/hashDocs.ts delete mode 100644 src/utils/smellDetails.ts create mode 100644 src/utils/smellsData.ts diff --git a/.env b/.env index 86798f4..dfd4ddf 100644 --- a/.env +++ b/.env @@ -4,3 +4,6 @@ FILE_CHANGES_KEY='lastSavedHashes' LAST_USED_SMELLS_KEY='lastUsedSmells' CURRENT_REFACTOR_DATA_KEY='refactorData' ACTIVE_DIFF_KEY='activeDiff' +FILE_HASH_CACHE_KEY='fileHashCache' +SMELL_CACHE_KEY='smellCache' +SMELL_LINTING_ENABLED_KEY='smellLintingEnabled' diff --git a/.prettierrc b/.prettierrc index ed1cfa5..21a17ba 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,6 +3,7 @@ "printWidth": 85, "semi": true, "singleQuote": true, + "endOfLine": "auto", "tabWidth": 2, "trailingComma": "all", "plugins": ["prettier-plugin-tailwindcss"] diff --git a/assets/eco-icon.png b/assets/eco-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f5314b77ab490c287f3d739c6a4d540bf74020a5 GIT binary patch literal 518 zcmV+h0{Q)kP)kB1_Y{X&%F<|@yd_>XRN(7T4Z5kmF z(h6Hx#6}TOv=GEj5v)|u(nf=%45SJnn5_&+5%C3Dgh&!y9}Jv@kIQ|nY@-KmGv~~i z@6P>ZF5CfBmD(@?q=7jg2mAqAN@4sMm;!zR*PiDGPVS78O-NOTa=lC(4*J@HY+7sCpCkZ3SOGb@eFfs2gdpTi35 zOY|s!uLVw|>GW_Z#Sg;@{Ff+b{NIVUetS_V&gLZ$dlEhM;Zn}&U;6M#ai_$%&yzmf zhB%uKKXi!S2`kX0sr(1@I`JVT{|}|^8J}c/test/mocks/vscode-mock.ts" + }, + "moduleDirectories": [ + "node_modules", + "tests/__mocks__" + ], + "roots": [ + "/src", + "/test" + ], + "collectCoverage": true, + "coverageReporters": [ + "text", + "html", + "lcov" + ], + "coverageDirectory": "/coverage/", + "coverageThreshold": { + "global": { + "statements": 80 + } + }, + "collectCoverageFrom": [ + "src/**/*.ts", + "!src/**/*.d.ts", + "!src/**/index.ts", + "!test/mocks/*", + "!src/extension.ts", + "!src/context/*", + "!src/utils/configManager.ts", + "!src/commands/showLogs.ts", + "!src/ui/refactorView.ts", + "!src/utils/handleEditorChange.ts" + ] + }, + "lint-staged": { + "src/**/*.ts": [ + "eslint --fix", + "prettier --write" + ] + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "20.x", + "@types/vscode": "^1.92.0", + "@types/ws": "^8.5.14", + "@typescript-eslint/eslint-plugin": "^8.24.1", + "@typescript-eslint/parser": "^8.24.1", + "@vscode/test-cli": "^0.0.10", + "@vscode/test-electron": "^2.4.1", + "css-loader": "^7.1.2", + "eslint": "^9.21.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-unused-imports": "^4.1.4", + "husky": "^9.1.7", + "jest": "^29.7.0", + "jest-silent-reporter": "^0.6.0", + "lint-staged": "^15.4.3", + "prettier": "^3.5.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "style-loader": "^4.0.0", + "ts-jest": "^29.2.6", + "ts-loader": "^9.5.1", + "typescript": "^5.7.2", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4", + "webpack-node-externals": "^3.0.0" + }, + "dependencies": { + "@types/dotenv": "^6.1.1", + "bufferutil": "^4.0.9", + "dotenv": "^16.4.7", + "dotenv-webpack": "^8.1.0", + "utf-8-validate": "^6.0.5", + "ws": "^8.18.0" + }, "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "ecooptimizer", + "title": "Eco Optimizer", + "icon": "assets/eco-icon.png" + } + ] + }, + "views": { + "ecooptimizer": [ + { + "id": "ecooptimizer.view", + "name": "Code Smells", + "icon": "assets/eco-icon.png" + }, + { + "id": "ecooptimizer.filterView", + "name": "Filter Smells", + "icon": "assets/eco-icon.png" + } + ] + }, + "viewsWelcome": [ + { + "view": "ecooptimizer.view", + "contents": "No code smells detected yet. Configure your workspace to start analysis.\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", + "when": "!workspaceState.workspaceConfigured" + } + ], "commands": [ { - "command": "ecooptimizer.detectSmells", + "command": "ecooptimizer.configureWorkspace", + "title": "Configure Workspace", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.resetConfiguration", + "title": "Reset Configuration", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.toggleSmellFilter", + "title": "Toggle Smell", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.editSmellFilterOption", + "title": "Edit Option", + "icon": "$(edit)", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.selectAllFilterSmells", + "title": "Select All Smells", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.deselectAllFilterSmells", + "title": "Deselect All Smells", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.openFile", + "title": "Open File", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.detectSmellsFolder", + "title": "Detect Smells for All Files", + "icon": "$(search)", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.detectSmellsFile", "title": "Detect Smells", - "category": "Eco" + "icon": "$(search)", + "category": "Eco Optimizer" }, { "command": "ecooptimizer.refactorSmell", @@ -33,26 +206,20 @@ "category": "Eco" }, { - "command": "ecooptimizer.wipeWorkCache", - "title": "Wipe Workspace Cache", - "category": "Eco" - }, - { - "command": "ecooptimizer.showRefactorSidebar", - "title": "Show Refactor Sidebar", - "category": "Eco" + "command": "ecooptimizer.refactorAllSmellsOfType", + "title": "Refactor Smells By Type", + "icon": "$(tools)", + "category": "Eco Optimizer" }, { - "command": "ecooptimizer.pauseRefactorSidebar", - "title": "Pause Refactor Sidebar", - "category": "Eco", - "enablement": "false" + "command": "ecooptimizer.jumpToSmell", + "title": "Jump to Smell in File", + "category": "Eco Optimizer" }, { - "command": "ecooptimizer.clearRefactorSidebar", - "title": "Clear Refactor Sidebar", - "category": "Eco", - "enablement": "false" + "command": "ecooptimizer.wipeWorkCache", + "title": "Clear Smells Cache", + "category": "Eco Optimizer" }, { "command": "ecooptimizer.startLogging", @@ -67,6 +234,50 @@ } ], "menus": { + "view/title": [ + { + "command": "ecooptimizer.resetConfiguration", + "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured", + "group": "resource" + }, + { + "command": "ecooptimizer.wipeWorkCache", + "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured", + "group": "resource" + }, + { + "command": "ecooptimizer.selectAllFilterSmells", + "when": "view == ecooptimizer.filterView", + "group": "resource" + }, + { + "command": "ecooptimizer.deselectAllFilterSmells", + "when": "view == ecooptimizer.filterView", + "group": "resource" + } + ], + "view/item/context": [ + { + "command": "ecooptimizer.editSmellFilterOption", + "when": "viewItem == smellOption", + "group": "inline" + }, + { + "command": "ecooptimizer.detectSmellsFolder", + "when": "viewItem == ecoOptimizerFolder", + "group": "inline" + }, + { + "command": "ecooptimizer.detectSmellsFile", + "when": "viewItem == ecoOptimizerFile", + "group": "inline" + }, + { + "command": "ecooptimizer.refactorAllSmellsOfType", + "when": "viewItem == ecoOptimizerFile", + "group": "inline" + } + ], "editor/title": [ { "command": "ecooptimizer.toggleSmellLinting", @@ -101,165 +312,53 @@ "default": "", "description": "Path to store log files and output reports. Defaults to a 'logs' folder inside the workspace." }, - "detection.smells": { + "detection.smellColours": { "order": 1, "type": "object", "additionalProperties": false, - "description": "Configure which smells to detect and their highlight colours.", + "description": "Configure the highlight colours of each smell (css syntax).", "default": { - "long-element-chain": { - "enabled": true, - "colour": "lightblue" - }, - "too-many-arguments": { - "enabled": true, - "colour": "lightcoral" - }, - "long-lambda-expression": { - "enabled": true, - "colour": "mediumpurple" - }, - "long-message-chain": { - "enabled": true, - "colour": "lightpink" - }, - "cached-repeated-calls": { - "enabled": true, - "colour": "lightgreen" - }, - "string-concat-loop": { - "enabled": true, - "colour": "lightsalmon" - }, - "no-self-use": { - "enabled": true, - "colour": "lightcyan" - }, - "use-a-generator": { - "enabled": true, - "colour": "yellow" - } + "long-element-chain": "lightblue", + "too-many-arguments": "lightcoral", + "long-lambda-expression": "mediumpurple", + "long-message-chain": "lightpink", + "cached-repeated-calls": "lightgreen", + "string-concat-loop": "lightsalmon", + "no-self-use": "lightcyan", + "use-a-generator": "yellow" }, "properties": { "long-element-chain": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of long element chains." - }, - "colour": { - "type": "string", - "default": "lightblue", - "description": "Colour (css syntax) for highlighting long element chains." - } - } + "type": "string", + "default": "lightblue" }, "too-many-arguments": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of functions with too many arguments." - }, - "colour": { - "type": "string", - "default": "lightcoral", - "description": "Colour (css syntax) for highlighting functions with too many arguments." - } - } + "type": "string", + "default": "lightcoral" }, "long-lambda-expression": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of long lambda expressions." - }, - "colour": { - "type": "string", - "default": "mediumpurple", - "description": "Colour (css syntax) for highlighting long lambda expressions." - } - } + "type": "string", + "default": "mediumpurple" }, "long-message-chain": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of long message chains." - }, - "colour": { - "type": "string", - "default": "lightpink", - "description": "Colour (css syntax) for highlighting long message chains." - } - } + "type": "string", + "default": "lightpink" }, "cached-repeated-calls": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of cached repeated calls." - }, - "colour": { - "type": "string", - "default": "lightgreen", - "description": "Colour (css syntax) for highlighting cached repeated calls." - } - } + "type": "string", + "default": "lightgreen" }, "string-concat-loop": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of string concatenation in loops." - }, - "colour": { - "type": "string", - "default": "lightsalmon", - "description": "Colour (css syntax) for highlighting string concatenation in loops." - } - } + "type": "string", + "default": "lightsalmon" }, "no-self-use": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of methods with no self-use." - }, - "colour": { - "type": "string", - "default": "lightcyan", - "description": "Colour (css syntax) for highlighting methods with no self-use." - } - } + "type": "string", + "default": "lightcyan" }, "use-a-generator": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable detection of places where a generator could be used." - }, - "colour": { - "type": "string", - "default": "yellow", - "description": "Colour (css syntax) for highlighting places where a generator could be used." - } - } + "type": "string", + "default": "yellow" } } }, @@ -292,131 +391,6 @@ "description": "Choose a highlight style for all smells." } } - }, - "keybindings": [ - { - "command": "ecooptimizer.refactorSmell", - "key": "ctrl+shift+r", - "when": "editorTextFocus && resourceExtname == '.py'" - } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "refactorSidebarContainer", - "title": "Refactoring", - "icon": "resources/refactor-icon.svg" - } - ] - }, - "views": { - "refactorSidebarContainer": [ - { - "id": "extension.refactorSidebar", - "name": "Refactoring Summary", - "type": "webview" - } - ] } - }, - "directories": { - "src": "./src", - "test": "./test" - }, - "scripts": { - "vscode:prepublish": "npm run package", - "compile": "webpack", - "test": "jest --verbose", - "test:watch": "jest --watch --silent --verbose", - "watch": "webpack --watch", - "package": "webpack --mode production --devtool hidden-source-map", - "compile-tests": "tsc -p . --outDir out", - "watch-tests": "tsc -p . -w --outDir out", - "lint": "eslint src", - "prepare": "husky" - }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "setupFilesAfterEnv": [ - "./test/setup.ts" - ], - "moduleNameMapper": { - "^vscode$": "/test/mocks/vscode-mock.ts" - }, - "moduleDirectories": [ - "node_modules", - "tests/__mocks__" - ], - "roots": [ - "/src", - "/test" - ], - "collectCoverage": true, - "coverageReporters": [ - "text", - "html", - "lcov" - ], - "coverageDirectory": "/coverage/", - "coverageThreshold": { - "global": { - "statements": 80 - } - }, - "collectCoverageFrom": [ - "src/**/*.ts", - "!src/**/*.d.ts", - "!src/**/index.ts", - "!test/mocks/*", - "!src/extension.ts", - "!src/context/*", - "!src/utils/configManager.ts", - "!src/commands/showLogs.ts", - "!src/ui/refactorView.ts", - "!src/utils/handleEditorChange.ts" - ] - }, - "lint-staged": { - "src/**/*.ts": [ - "eslint --fix", - "prettier --write" - ] - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "20.x", - "@types/vscode": "^1.92.0", - "@types/ws": "^8.5.14", - "@typescript-eslint/eslint-plugin": "^8.24.1", - "@typescript-eslint/parser": "^8.24.1", - "@vscode/test-cli": "^0.0.10", - "@vscode/test-electron": "^2.4.1", - "css-loader": "^7.1.2", - "eslint": "^9.21.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-prettier": "^5.2.3", - "eslint-plugin-unused-imports": "^4.1.4", - "husky": "^9.1.7", - "jest": "^29.7.0", - "jest-silent-reporter": "^0.6.0", - "lint-staged": "^15.4.3", - "prettier": "^3.5.2", - "prettier-plugin-tailwindcss": "^0.6.11", - "style-loader": "^4.0.0", - "ts-jest": "^29.2.6", - "ts-loader": "^9.5.1", - "typescript": "^5.7.2", - "webpack": "^5.95.0", - "webpack-cli": "^5.1.4", - "webpack-node-externals": "^3.0.0" - }, - "dependencies": { - "@types/dotenv": "^6.1.1", - "bufferutil": "^4.0.9", - "dotenv": "^16.4.7", - "dotenv-webpack": "^8.1.0", - "utf-8-validate": "^6.0.5", - "ws": "^8.18.0" } } diff --git a/src/api/backend.ts b/src/api/backend.ts index d5979ea..49aefc9 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -19,12 +19,17 @@ export async function checkServerStatus(): Promise { } } +/** + * Initializes and synchronizes logs with the backend. + * + * @param {string} log_dir - The directory where logs are stored. + * @returns {Promise} - Returns `true` if the logs are successfully initialized and synchronized, otherwise throws an error. + * @throws {Error} - Throws an error if the initialization fails due to network issues or backend errors. + */ export async function initLogs(log_dir: string): Promise { const url = `${BASE_URL}/logs/init`; try { - console.log('Initializing and synching logs with backend'); - const response = await fetch(url, { method: 'POST', headers: { @@ -34,71 +39,73 @@ export async function initLogs(log_dir: string): Promise { }); if (!response.ok) { - console.error(`Unable to initialize logging: ${JSON.stringify(response)}`); - - return false; + throw new Error(`Unable to initialize logging: ${response.statusText}`); } return true; } catch (error: any) { - console.error(`Eco: Unable to initialize logging: ${error.message}`); - vscode.window.showErrorMessage( - 'Eco: Unable to reach the backend. Please check your connection.', - ); - return false; + if (error instanceof Error) { + throw new Error(`Eco: Unable to initialize logging: ${error.message}`); + } else { + throw new Error('Eco: An unexpected error occurred while initializing logs.'); + } } } -// ✅ Fetch detected smells for a given file (only enabled smells) +/** + * Sends a request to the backend to detect code smells in the specified file. + * + * @param filePath - The absolute path to the file being analyzed. + * @param enabledSmells - A dictionary containing enabled smells and their configured options. + * @returns A promise resolving to the backend response or throwing an error if unsuccessful. + */ export async function fetchSmells( filePath: string, - enabledSmells: string[], -): Promise { + enabledSmells: Record> +): Promise<{ smells: Smell[]; status: number }> { const url = `${BASE_URL}/smells`; try { - console.log( - `Eco: Requesting smells for file: ${filePath} with filters: ${enabledSmells}`, - ); - const response = await fetch(url, { - method: 'POST', // ✅ Send enabled smells in the request body + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify({ file_path: filePath, enabled_smells: enabledSmells }), // ✅ Include enabled smells + body: JSON.stringify({ + file_path: filePath, + enabled_smells: enabledSmells, + }), }); if (!response.ok) { - console.error( - `Eco: API request failed (${response.status} - ${response.statusText})`, + throw new Error( + `Backend request failed with status ${response.status}: ${response.statusText}` ); - vscode.window.showErrorMessage( - `Eco: Failed to fetch smells`, - ); - return []; } - const smellsList = (await response.json()) as Smell[]; + const smellsList = await response.json(); if (!Array.isArray(smellsList)) { - console.error('Eco: Invalid response format from backend.'); - vscode.window.showErrorMessage('Eco: Failed to fetch smells'); - return []; + throw new Error("Unexpected response format from backend."); } - console.log(`Eco: Successfully retrieved ${smellsList.length} smells.`); - return smellsList; + return { smells: smellsList, status: response.status }; } catch (error: any) { - console.error(`Eco: Network error while fetching smells: ${error.message}`); - vscode.window.showErrorMessage( - 'Eco: Failed to fetch smells', + throw new Error( + `Failed to connect to the backend: ${error.message}. Please check your network and try again.` ); - return []; } } -// Request refactoring for a specific smell + +/** + * Refactors a specific code smell in a given file. + * + * @param {string} filePath - The path to the file containing the code smell. + * @param {Smell} smell - The code smell to refactor. + * @returns {Promise} - The result of the refactoring operation. + * @throws {Error} - Throws an error if the workspace folder cannot be determined, the API request fails, or an unexpected error occurs. + */ export async function refactorSmell( filePath: string, smell: Smell, @@ -107,21 +114,14 @@ export async function refactorSmell( const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => filePath.includes(folder.uri.fsPath), - ) + ); if (!workspaceFolder) { - console.error('Eco: Error - Unable to determine workspace folder for', filePath); - throw new Error( - `Eco: Unable to find a matching workspace folder for file: ${filePath}`, - ); + throw new Error(`Unable to determine workspace folder for file: ${filePath}`); } const workspaceFolderPath = workspaceFolder.uri.fsPath; - console.log( - `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspaceFolderPath}"`, - ); - const payload = { source_dir: workspaceFolderPath, smell, @@ -138,16 +138,16 @@ export async function refactorSmell( if (!response.ok) { const errorText = await response.text(); - console.error( - `Eco: Error - Refactoring smell "${smell.symbol}": ${errorText}`, - ); - throw new Error(`Eco: Error refactoring smell: ${errorText}`); + throw new Error(`Refactoring failed for smell "${smell.symbol}": ${errorText}`); } const refactorResult = (await response.json()) as RefactorOutput; return refactorResult; } catch (error) { - console.error('Eco: Unexpected error in refactorSmell:', error); - throw error; + if (error instanceof Error) { + throw new Error(`Unexpected error during refactoring: ${error.message}`); + } else { + throw new Error('An unexpected error occurred during refactoring.'); + } } -} +} \ No newline at end of file diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts new file mode 100644 index 0000000..7be0c7d --- /dev/null +++ b/src/commands/configureWorkspace.ts @@ -0,0 +1,179 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; + +/** + * Prompts the user to configure a workspace by selecting either a Python file or folder. + * Updates the workspace state accordingly and refreshes the tree view to reflect the changes. + * + * @param context - The extension context used to persist workspace state. + * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. + */ +export async function configureWorkspace( + context: vscode.ExtensionContext, + treeDataProvider: vscode.TreeDataProvider, +) { + // Prompt the user to choose between configuring a Python file or folder + const choice = await vscode.window.showQuickPick( + ['Configure a Python File', 'Configure a Python Folder'], + { placeHolder: 'Choose whether to configure a Python file or folder.' }, + ); + + // Exit if the user cancels the selection + if (!choice) return; + + // Call the appropriate function based on the user's choice + if (choice === 'Configure a Python File') { + await configurePythonFile(context, treeDataProvider); + } else { + await configurePythonFolder(context, treeDataProvider); + } +} + +/** + * Prompts the user to select a Python file from open editors or the workspace. + * Updates the workspace state with the selected file and refreshes the tree view. + * + * @param context - The extension context used to persist workspace state. + * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. + */ +async function configurePythonFile( + context: vscode.ExtensionContext, + treeDataProvider: vscode.TreeDataProvider, +) { + // Retrieve Python files from open editors + const openEditorFiles = vscode.window.tabGroups.activeTabGroup.tabs + .map((tab) => (tab.input as any)?.uri?.fsPath) + .filter((filePath) => filePath && filePath.endsWith('.py')); + + // Retrieve Python files from the workspace using a glob pattern + const workspaceFiles = await vscode.workspace.findFiles( + '**/*.py', + '**/node_modules/**', + ); + const workspaceFilePaths = workspaceFiles.map((uri) => uri.fsPath); + + // Combine and deduplicate the list of Python files + const allPythonFiles = Array.from( + new Set([...openEditorFiles, ...workspaceFilePaths]), + ); + + // Notify the user if no Python files are found + if (allPythonFiles.length === 0) { + vscode.window.showErrorMessage( + 'No Python files found in open editors or workspace.', + ); + return; + } + + // Prompt the user to select a Python file from the combined list + const selectedFile = await vscode.window.showQuickPick(allPythonFiles, { + placeHolder: 'Select a Python file to use as your workspace.', + }); + + // Update the workspace state and notify the user if a file is selected + if (selectedFile) { + await updateWorkspace(context, treeDataProvider, selectedFile); + vscode.window.showInformationMessage( + `Workspace configured for file: ${path.basename(selectedFile)}`, + ); + } +} + +/** + * Prompts the user to select a Python folder from the workspace. + * Updates the workspace state with the selected folder and refreshes the tree view. + * + * @param context - The extension context used to persist workspace state. + * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. + */ +async function configurePythonFolder( + context: vscode.ExtensionContext, + treeDataProvider: vscode.TreeDataProvider, +) { + // Retrieve the workspace folders from the current workspace + const workspaceFolders = vscode.workspace.workspaceFolders; + + // Notify the user if no workspace folders are found + if (!workspaceFolders || workspaceFolders.length === 0) { + vscode.window.showErrorMessage( + 'No workspace folders found. Open a project in Explorer first.', + ); + return; + } + + // Filter workspace folders to include only those containing Python files + const validPythonFolders = workspaceFolders + .map((folder) => folder.uri.fsPath) + .filter((folderPath) => containsPythonFiles(folderPath)); + + // Notify the user if no valid Python folders are found + if (validPythonFolders.length === 0) { + vscode.window.showErrorMessage( + 'No valid Python folders found in your workspace.', + ); + return; + } + + // Prompt the user to select a Python folder from the filtered list + const selectedFolder = await vscode.window.showQuickPick(validPythonFolders, { + placeHolder: 'Select a Python folder to use as your workspace.', + }); + + // Update the workspace state and notify the user if a folder is selected + if (selectedFolder) { + await updateWorkspace(context, treeDataProvider, selectedFolder); + vscode.window.showInformationMessage( + `Workspace configured for folder: ${path.basename(selectedFolder)}`, + ); + } +} + +/** + * Checks if a given folder contains Python files. + * This function scans the folder for `.py` files or an `__init__.py` file. + * + * @param folderPath - The absolute path of the folder to check. + * @returns True if the folder contains Python files, otherwise false. + */ +function containsPythonFiles(folderPath: string): boolean { + try { + // Read the contents of the folder + const files = fs.readdirSync(folderPath); + + // Check if any file ends with `.py` or if the folder contains `__init__.py` + return ( + files.some((file) => file.endsWith('.py')) || files.includes('__init__.py') + ); + } catch (error) { + // Return false if an error occurs (e.g., folder is inaccessible) + return false; + } +} + +/** + * Updates the workspace state to reflect the configured Python file or folder. + * Refreshes the tree view to reflect the changes. + * + * @param context - The extension context used to persist workspace state. + * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. + * @param workspacePath - The selected workspace path (file or folder). + */ +async function updateWorkspace( + context: vscode.ExtensionContext, + treeDataProvider: vscode.TreeDataProvider, + workspacePath: string, +) { + // Update the workspace state with the selected path + await context.workspaceState.update('workspaceConfiguredPath', workspacePath); + + // Set a context variable to indicate that the workspace is configured + vscode.commands.executeCommand( + 'setContext', + 'workspaceState.workspaceConfigured', + true, + ); + + // Refresh the tree view to reflect the updated workspace configuration + (treeDataProvider as any).refresh(); +} diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index f597f96..5681670 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -1,121 +1,208 @@ import * as vscode from 'vscode'; - -import { FileHighlighter } from '../ui/fileHighlighter'; -import { getEditorAndFilePath } from '../utils/editorUtils'; +import * as fs from 'fs'; +import * as path from 'path'; import { fetchSmells } from '../api/backend'; -import { ContextManager } from '../context/contextManager'; -import { envConfig } from '../utils/envConfig'; -import { hashContent, updateHash } from '../utils/hashDocs'; -import { wipeWorkCache } from './wipeWorkCache'; -import { getEnabledSmells } from '../utils/handleSmellSettings'; +import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { getEnabledSmells } from '../utils/smellsData'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { serverStatus, ServerStatusType } from '../utils/serverStatus'; -serverStatus.on('change', (newStatus: ServerStatusType) => { - console.log('Server status changed:', newStatus); - if (newStatus === ServerStatusType.DOWN) { - vscode.window.showWarningMessage( - 'Smell detection limited. Only cached smells will be shown.', - ); +/** + * Detects code smells for a given file. + * Uses cached smells if available; otherwise, fetches from the backend. + * + * @param smellsCacheManager - Manages caching of smells and file hashes. + * @param treeDataProvider - UI provider for updating tree view. + * @param fileUri - The VS Code file URI or string path of the file to analyze. + */ +export async function detectSmellsFile( + smellsCacheManager: SmellsCacheManager, + treeDataProvider: SmellsDisplayProvider, + fileUri: vscode.Uri | string, +) { + // Validate the file URI or path + if (!fileUri) { + vscode.window.showErrorMessage('No file selected for analysis.'); + return; } -}); -export interface SmellDetectRecord { - hash: string; - smells: Smell[]; -} + // Convert file URI to a path if necessary + const filePath = typeof fileUri === 'string' ? fileUri : fileUri.fsPath; -let fileHighlighter: FileHighlighter; + // Handle outdated files before proceeding + console.log('Handling outdated file:', filePath); + await handleOutdatedFile(filePath, smellsCacheManager, treeDataProvider); -export async function detectSmells(contextManager: ContextManager): Promise { - const { editor, filePath } = getEditorAndFilePath(); + // Open the file and compute its hash + const document = await vscode.workspace.openTextDocument(filePath); + const fileContent = document.getText(); - // ✅ Ensure an active editor exists - if (!editor) { - vscode.window.showErrorMessage('Eco: No active editor found.'); - console.error('Eco: No active editor found to detect smells.'); + // Store the file hash after analyzing + console.log('Storing file hash for:', filePath); + await smellsCacheManager.storeFileHash(filePath, fileContent); + + // Retrieve enabled smells from configuration + console.log('Retrieving enabled smells...'); + const enabledSmells = getEnabledSmells(); + + // Ensure that at least one smell type is enabled + if (Object.keys(enabledSmells).length === 0) { + vscode.window.showWarningMessage( + 'No enabled smells found. Please configure enabled smells in the settings.', + ); return; } - // ✅ Ensure filePath is valid - if (!filePath) { - vscode.window.showErrorMessage('Eco: Active editor has no valid file path.'); - console.error('Eco: No valid file path found for smell detection.'); + // Check if smells are already cached + console.log('Checking for cached smells...'); + const cachedSmells = smellsCacheManager.getCachedSmells(filePath); + if (cachedSmells !== undefined) { + // Use cached smells if available + vscode.window.showInformationMessage( + `Using cached smells for ${path.basename(filePath)}.`, + ); + + if (cachedSmells.length > 0) { + console.log('Updating UI with cached smells...'); + treeDataProvider.updateSmells(filePath, cachedSmells, enabledSmells); + } else { + treeDataProvider.updateStatus(filePath, 'no_issues'); + } + + console.log('Analysis complete: Using cached smells.'); return; } - console.log(`Eco: Detecting smells in file: ${filePath}`); - - const enabledSmells = getEnabledSmells(); - if (!Object.values(enabledSmells).includes(true)) { + if (serverStatus.getStatus() === ServerStatusType.DOWN) { vscode.window.showWarningMessage( - 'Eco: No smells are enabled! Detection skipped.', + 'Action blocked: Server is down and no cached smells exist for this file version.', ); + treeDataProvider.updateStatus(filePath, 'server_down'); return; } - // ✅ Check if the enabled smells have changed - const lastUsedSmells = contextManager.getWorkspaceData( - envConfig.LAST_USED_SMELLS_KEY!, - {}, - ); - if (JSON.stringify(lastUsedSmells) !== JSON.stringify(enabledSmells)) { - console.log('Eco: Smell settings have changed! Wiping cache.'); - await wipeWorkCache(contextManager, 'settings'); - contextManager.setWorkspaceData(envConfig.LAST_USED_SMELLS_KEY!, enabledSmells); - } + // Update UI to indicate the file is queued for analysis + treeDataProvider.updateStatus(filePath, 'queued'); - // Handle cache and previous smells - const allSmells: Record = - contextManager.getWorkspaceData(envConfig.SMELL_MAP_KEY!) || {}; - const fileSmells = allSmells[filePath]; - const currentFileHash = hashContent(editor.document.getText()); - - let smellsData: Smell[] | undefined; - - if (fileSmells && currentFileHash === fileSmells.hash) { - vscode.window.showInformationMessage(`Eco: Using cached smells for ${filePath}`); - - smellsData = fileSmells.smells; - } else if (serverStatus.getStatus() === ServerStatusType.UP) { - updateHash(contextManager, editor.document); - - try { - smellsData = await fetchSmells( - filePath, - Object.keys(enabledSmells).filter((s) => enabledSmells[s]), - ); - } catch (err) { - console.error(err); - return; - } + try { + // Prepare enabled smells for backend request + console.log('Preparing enabled smells for backend...'); + const enabledSmellsForBackend = Object.fromEntries( + Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), + ); - if (smellsData) { - allSmells[filePath] = { hash: currentFileHash, smells: smellsData }; - contextManager.setWorkspaceData(envConfig.SMELL_MAP_KEY!, allSmells); + // Request smell analysis from the backend + console.log('Requesting smell analysis from the backend...'); + const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); + + // Handle response and update UI + if (status === 200) { + // Cache detected smells, even if no smells are found + console.log('Caching detected smells...'); + await smellsCacheManager.setCachedSmells(filePath, smells); + + // Remove the file from modifiedFiles after re-analysis + treeDataProvider.clearOutdatedStatus(filePath); + + console.log('Updating UI with detected smells...'); + if (smells.length > 0) { + treeDataProvider.updateSmells(filePath, smells, enabledSmells); + vscode.window.showInformationMessage( + `Analysis complete: Detected ${ + smells.length + } code smell(s) in ${path.basename(filePath)}.`, + ); + } else { + treeDataProvider.updateStatus(filePath, 'no_issues'); // Update status based on backend result + vscode.window.showInformationMessage( + `Analysis complete: No code smells found in ${path.basename(filePath)}.`, + ); + } + + console.log('Analysis complete: Detected smells.'); + } else { + throw new Error(`Unexpected status code: ${status}`); } - } else { + } catch (error: any) { + // Handle errors during analysis + treeDataProvider.updateStatus(filePath, 'failed'); + vscode.window.showErrorMessage(`Analysis failed: ${error.message}`); + } +} + +/** + * Detects code smells for all Python files within a folder. + * Uses cached smells where available. + * + * @param smellsCacheManager - Manages caching of smells and file hashes. + * @param treeDataProvider - UI provider for updating tree view. + * @param folderPath - The absolute path of the folder to analyze. + */ +export async function detectSmellsFolder( + smellsCacheManager: SmellsCacheManager, + treeDataProvider: SmellsDisplayProvider, + folderPath: string, +) { + console.log('Detecting smells for all Python files in:', folderPath); + // Notify the user that folder analysis has started + vscode.window.showInformationMessage( + `Detecting code smells for all Python files in: ${path.basename(folderPath)}`, + ); + + // Retrieve all Python files in the specified folder + const pythonFiles = fs + .readdirSync(folderPath) + .filter((file) => file.endsWith('.py')) + .map((file) => path.join(folderPath, file)); + + // Ensure that Python files exist in the folder before analysis + if (pythonFiles.length === 0) { vscode.window.showWarningMessage( - 'Action blocked: Server is down and no cached smells exist for this file version.', + `No Python files found in ${path.basename(folderPath)}.`, ); return; } - if (!smellsData || smellsData.length === 0) { - vscode.window.showInformationMessage('Eco: No code smells detected.'); + // Retrieve enabled smells from configuration + const enabledSmells = getEnabledSmells(); + + // Ensure that at least one smell type is enabled + if (Object.keys(enabledSmells).length === 0) { + vscode.window.showWarningMessage( + 'No enabled smells found. Please configure enabled smells in the settings.', + ); return; } - console.log(`Eco: Highlighting detected smells in ${filePath}.`); - if (!fileHighlighter) { - fileHighlighter = FileHighlighter.getInstance(contextManager); + // Analyze each Python file in the folder + for (const file of pythonFiles) { + console.log('Analyzing:', file); + await detectSmellsFile(smellsCacheManager, treeDataProvider, file); } - fileHighlighter.highlightSmells(editor, smellsData); - vscode.window.showInformationMessage( - `Eco: Highlighted ${smellsData.length} smells.`, - ); + // Refresh UI to reflect folder analysis results + treeDataProvider.refresh(); +} - // Set the linting state to enabled - contextManager.setWorkspaceData(envConfig.SMELL_LINTING_ENABLED_KEY, true); - vscode.commands.executeCommand('setContext', 'eco.smellLintingEnabled', true); +/** + * Handles outdated files before detecting smells. + * Deletes cached smells and updates the UI for outdated files. + * + * @param filePath - The path of the file to analyze. + * @param smellsCacheManager - Manages caching of smells and file hashes. + * @param smellsDisplayProvider - The UI provider for updating the tree view. + */ +async function handleOutdatedFile( + filePath: string, + smellsCacheManager: SmellsCacheManager, + smellsDisplayProvider: SmellsDisplayProvider, +) { + // Check if the file is marked as outdated + if (smellsDisplayProvider.isFileOutdated(filePath)) { + // Delete cached smells for the outdated file + await smellsCacheManager.clearCachedSmellsForFile(filePath); + + // Remove the outdated status from the UI + smellsDisplayProvider.updateStatus(filePath, 'queued'); // Reset to "queued" or another default status + } } diff --git a/src/commands/filterSmells.ts b/src/commands/filterSmells.ts new file mode 100644 index 0000000..590ca6e --- /dev/null +++ b/src/commands/filterSmells.ts @@ -0,0 +1,72 @@ +import * as vscode from 'vscode'; +import { FilterSmellsProvider } from '../providers/FilterSmellsProvider'; + +/** + * Registers VS Code commands for managing smell filters. + * @param context - The VS Code extension context. + * @param filterSmellsProvider - The provider responsible for handling smell filtering. + */ +export function registerFilterSmellCommands( + context: vscode.ExtensionContext, + filterSmellsProvider: FilterSmellsProvider, +) { + /** + * Toggles the state of a specific smell filter. + */ + context.subscriptions.push( + vscode.commands.registerCommand( + 'ecooptimizer.toggleSmellFilter', + (smellKey: string) => { + filterSmellsProvider.toggleSmell(smellKey); + }, + ), + ); + + /** + * Edits a specific smell filter option. + * Prompts the user for input, validates the value, and updates the setting. + */ + context.subscriptions.push( + vscode.commands.registerCommand( + 'ecooptimizer.editSmellFilterOption', + async (item: any) => { + if (!item || !item.smellKey || !item.optionKey) { + vscode.window.showErrorMessage('Error: Missing smell or option key.'); + return; + } + + const { smellKey, optionKey, value: oldValue } = item; + + const newValue = await vscode.window.showInputBox({ + prompt: `Enter a new value for ${optionKey}`, + value: oldValue?.toString() || '', + validateInput: (input) => + isNaN(Number(input)) ? 'Must be a number' : undefined, + }); + + if (newValue !== undefined && !isNaN(Number(newValue))) { + filterSmellsProvider.updateOption(smellKey, optionKey, Number(newValue)); + filterSmellsProvider.refresh(); + } + }, + ), + ); + + /** + * Enables all smell filters. + */ + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.selectAllFilterSmells', () => { + filterSmellsProvider.setAllSmellsEnabled(true); + }), + ); + + /** + * Disables all smell filters. + */ + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.deselectAllFilterSmells', () => { + filterSmellsProvider.setAllSmellsEnabled(false); + }), + ); +} diff --git a/src/commands/jumpToSmell.ts b/src/commands/jumpToSmell.ts new file mode 100644 index 0000000..94fb8ff --- /dev/null +++ b/src/commands/jumpToSmell.ts @@ -0,0 +1,25 @@ +import * as vscode from 'vscode'; + +/** + * Jumps to a specific line in the given file within the VS Code editor. + * @param filePath - The absolute path of the file. + * @param line - The line number to navigate to. + */ +export async function jumpToSmell(filePath: string, line: number): Promise { + try { + const document = await vscode.workspace.openTextDocument(filePath); + const editor = await vscode.window.showTextDocument(document); + + // Move cursor to the specified line + const position = new vscode.Position(line, 0); + editor.selection = new vscode.Selection(position, position); + editor.revealRange( + new vscode.Range(position, position), + vscode.TextEditorRevealType.InCenter, + ); + } catch (error: any) { + vscode.window.showErrorMessage( + `Failed to jump to smell in ${filePath}: ${error.message}`, + ); + } +} diff --git a/src/commands/openFile.ts b/src/commands/openFile.ts new file mode 100644 index 0000000..47687bf --- /dev/null +++ b/src/commands/openFile.ts @@ -0,0 +1,21 @@ +import * as vscode from 'vscode'; + +/** + * Opens a file in the VS Code editor. + * Ensures the file is fully opened (not in preview mode). + * Displays an error message if no file is selected. + * + * @param fileUri - The URI of the file to be opened. + */ +export async function openFile(fileUri: vscode.Uri) { + if (!fileUri) { + vscode.window.showErrorMessage('Error: No file selected.'); + return; + } + + await vscode.window.showTextDocument(fileUri, { + preview: false, // Ensures the file opens as a permanent tab (not in preview mode) + viewColumn: vscode.ViewColumn.Active, // Opens in the active editor column + preserveFocus: false, // Focuses the file when opened + }); +} diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 9d9a4d8..ffd323c 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -5,13 +5,11 @@ import { envConfig } from '../utils/envConfig'; import { getEditorAndFilePath } from '../utils/editorUtils'; import { refactorSmell } from '../api/backend'; -import { sidebarState } from '../utils/handleEditorChange'; import { FileHighlighter } from '../ui/fileHighlighter'; -import { ContextManager } from '../context/contextManager'; -import { setTimeout } from 'timers/promises'; import { serverStatus } from '../utils/serverStatus'; import { ServerStatusType } from '../utils/serverStatus'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; /* istanbul ignore next */ serverStatus.on('change', (newStatus: ServerStatusType) => { @@ -43,12 +41,13 @@ async function refactorLine( } export async function refactorSelectedSmell( - contextManager: ContextManager, + context: vscode.ExtensionContext, + smellsCacheManager: SmellsCacheManager, smellGiven?: Smell, ): Promise { const { editor, filePath } = getEditorAndFilePath(); - const pastData = contextManager.getWorkspaceData( + const pastData = context.workspaceState.get( envConfig.CURRENT_REFACTOR_DATA_KEY!, ); @@ -67,9 +66,7 @@ export async function refactorSelectedSmell( const selectedLine = editor.selection.start.line + 1; // Update to VS Code editor indexing - const smellsData: Smell[] = contextManager.getWorkspaceData( - envConfig.SMELL_MAP_KEY!, - )[filePath].smells; + const smellsData = smellsCacheManager.getCachedSmells(filePath); if (!smellsData || smellsData.length === 0) { vscode.window.showErrorMessage( @@ -126,10 +123,10 @@ export async function refactorSelectedSmell( const { refactoredData } = refactorResult; - await startRefactoringSession(contextManager, editor, refactoredData); + await startRefactoringSession(context, editor, refactoredData); if (refactorResult.updatedSmells.length) { - const fileHighlighter = FileHighlighter.getInstance(contextManager); + const fileHighlighter = FileHighlighter.getInstance(context, smellsCacheManager); fileHighlighter.highlightSmells(editor, refactorResult.updatedSmells); } else { vscode.window.showWarningMessage( @@ -140,7 +137,9 @@ export async function refactorSelectedSmell( export async function refactorAllSmellsOfType( // eslint-disable-next-line unused-imports/no-unused-vars - contextManager: ContextManager, + context: vscode.ExtensionContext, + // eslint-disable-next-line unused-imports/no-unused-vars + smellsCacheManager: SmellsCacheManager, // eslint-disable-next-line unused-imports/no-unused-vars smellId: string, ): Promise { @@ -257,55 +256,59 @@ export async function refactorAllSmellsOfType( /* istanbul ignore next */ async function startRefactoringSession( - contextManager: ContextManager, + context: vscode.ExtensionContext, editor: vscode.TextEditor, refactoredData: RefactoredData | MultiRefactoredData, ): Promise { // Store only the diff editor state - await contextManager.setWorkspaceData( + await context.workspaceState.update( envConfig.CURRENT_REFACTOR_DATA_KEY!, refactoredData, ); - await vscode.commands.executeCommand('extension.refactorSidebar.focus'); - - //Read the refactored code - const refactoredCode = vscode.Uri.file(refactoredData.targetFile.refactored); - - //Get the original code from the editor - const originalCode = editor.document.uri; - - const allFiles: ChangedFile[] = [ - refactoredData.targetFile, - ...refactoredData.affectedFiles, - ].map((file) => { - return { - original: vscode.Uri.file(file.original).toString(), - refactored: vscode.Uri.file(file.refactored).toString(), - }; - }); - - await contextManager.setWorkspaceData(envConfig.ACTIVE_DIFF_KEY!, { - files: allFiles, - firstOpen: true, - isOpen: true, - }); - - await setTimeout(500); - - const doc = await vscode.workspace.openTextDocument(originalCode); - await vscode.window.showTextDocument(doc, { preview: false }); - - //Show the diff viewer - sidebarState.isOpening = true; - vscode.commands.executeCommand( - 'vscode.diff', - originalCode, - refactoredCode, - 'Refactoring Comparison', + vscode.window.showInformationMessage( + 'Hey Niv, this needs to be connected to the new refactor sidebar :)', ); - vscode.commands.executeCommand('ecooptimizer.showRefactorSidebar'); - sidebarState.isOpening = false; + + // await vscode.commands.executeCommand('extension.refactorSidebar.focus'); + + // //Read the refactored code + // const refactoredCode = vscode.Uri.file(refactoredData.targetFile.refactored); + + // //Get the original code from the editor + // const originalCode = editor.document.uri; + + // const allFiles: ChangedFile[] = [ + // refactoredData.targetFile, + // ...refactoredData.affectedFiles, + // ].map((file) => { + // return { + // original: vscode.Uri.file(file.original).toString(), + // refactored: vscode.Uri.file(file.refactored).toString(), + // }; + // }); + + // await contextManager.setWorkspaceData(envConfig.ACTIVE_DIFF_KEY!, { + // files: allFiles, + // firstOpen: true, + // isOpen: true, + // }); + + // await setTimeout(500); + + // const doc = await vscode.workspace.openTextDocument(originalCode); + // await vscode.window.showTextDocument(doc, { preview: false }); + + // //Show the diff viewer + // sidebarState.isOpening = true; + // vscode.commands.executeCommand( + // 'vscode.diff', + // originalCode, + // refactoredCode, + // 'Refactoring Comparison', + // ); + // vscode.commands.executeCommand('ecooptimizer.showRefactorSidebar'); + // sidebarState.isOpening = false; } export async function cleanTemps(pastData: any): Promise { diff --git a/src/commands/resetConfiguration.ts b/src/commands/resetConfiguration.ts new file mode 100644 index 0000000..fdd2305 --- /dev/null +++ b/src/commands/resetConfiguration.ts @@ -0,0 +1,39 @@ +import * as vscode from 'vscode'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; // Updated import +import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; + +/** + * Resets the workspace configuration by clearing the stored path and wiping cached smells. + * Prompts the user for confirmation before performing the reset. + * + * @param context - The VS Code extension context. + * @param smellsCacheManager - The cache manager handling cached smells. + * @param treeDataProvider - The tree data provider to refresh the UI. + */ +export async function resetConfiguration( + context: vscode.ExtensionContext, + smellsCacheManager: SmellsCacheManager, + treeDataProvider: SmellsDisplayProvider, +) { + const confirm = await vscode.window.showWarningMessage( + 'Are you sure you want to reset the workspace configuration? This will remove the currently selected folder and wipe cached smells.', + { modal: true }, + 'Reset', + ); + + if (confirm === 'Reset') { + await context.workspaceState.update('workspaceConfiguredPath', undefined); + vscode.commands.executeCommand( + 'setContext', + 'workspaceState.workspaceConfigured', + false, + ); + + // Clear smells & refresh UI using SmellsCacheManager + await smellsCacheManager.clearCacheAndRefreshUI(treeDataProvider); + + vscode.window.showInformationMessage( + 'Workspace configuration has been reset. All cached smells have been cleared.', + ); + } +} diff --git a/src/commands/showLogs.ts b/src/commands/showLogs.ts index 38229d3..93b5481 100644 --- a/src/commands/showLogs.ts +++ b/src/commands/showLogs.ts @@ -4,7 +4,8 @@ import WebSocket from 'ws'; import { initLogs } from '../api/backend'; import { envConfig } from '../utils/envConfig'; import { serverStatus, ServerStatusType } from '../utils/serverStatus'; -import { globalData } from '../extension'; + +const WEBSOCKET_BASE_URL = `ws://${envConfig.SERVER_URL}/logs`; class LogInitializationError extends Error { constructor(message: string) { @@ -13,176 +14,177 @@ class LogInitializationError extends Error { } } -class WebSocketInitializationError extends Error { - constructor(message: string) { - super(message); - this.name = 'WebSocketInitializationError'; - } -} - -const WEBSOCKET_BASE_URL = `ws://${envConfig.SERVER_URL}/logs`; - -let websockets: { [key: string]: WebSocket | undefined } = { - main: undefined, - detect: undefined, - refactor: undefined, -}; - -let channels: { - [key: string]: { name: string; channel: vscode.LogOutputChannel | undefined }; -} = { - main: { - name: 'EcoOptimizer: Main', - channel: undefined, - }, - detect: { - name: 'EcoOptimizer: Detect', - channel: undefined, - }, - refactor: { - name: 'EcoOptimizer: Refactor', - channel: undefined, - }, -}; - -let CHANNELS_CREATED = false; - -serverStatus.on('change', async (newStatus: ServerStatusType) => { - console.log('Server status changed:', newStatus); - if (newStatus === ServerStatusType.DOWN) { - channels.main.channel?.appendLine('Server connection lost'); - } else { - channels.main.channel?.appendLine('Server connection re-established.'); - await startLogging(); - } -}); - -export async function startLogging(retries = 3, delay = 1000): Promise { - let logInitialized = false; - const logPath = globalData.contextManager?.context.logUri?.fsPath; - console.log('log path:', logPath); - - if (!logPath) { - console.error('Missing contextManager or logUri. Cannot initialize logging.'); - return; +export class LogManager { + private websockets: { [key: string]: WebSocket | undefined }; + private channels: { + [key: string]: { name: string; channel: vscode.LogOutputChannel | undefined }; + }; + private channelsCreated: boolean; + private context: vscode.ExtensionContext; + + constructor(context: vscode.ExtensionContext) { + this.context = context; + this.websockets = { + main: undefined, + detect: undefined, + refactor: undefined, + }; + this.channels = { + main: { name: 'EcoOptimizer: Main', channel: undefined }, + detect: { name: 'EcoOptimizer: Detect', channel: undefined }, + refactor: { name: 'EcoOptimizer: Refactor', channel: undefined }, + }; + this.channelsCreated = false; + + // Listen for server status changes + serverStatus.on('change', async (newStatus: ServerStatusType) => { + console.log('Server status changed:', newStatus); + if (newStatus === ServerStatusType.DOWN) { + this.channels.main.channel?.appendLine('Server connection lost'); + } else { + this.channels.main.channel?.appendLine('Server connection re-established.'); + await this.startLogging(); + } + }); } - for (let attempt = 1; attempt <= retries; attempt++) { - try { - if (!logInitialized) { - logInitialized = await initLogs(logPath); + /** + * Starts the logging process, including initializing logs and WebSockets. + * @param retries - Number of retry attempts. + * @param delay - Initial delay between retries (in milliseconds). + */ + public async startLogging(retries = 3, delay = 1000): Promise { + let logInitialized = false; + const logPath = this.context.logUri?.fsPath; + + if (!logPath) { + throw new LogInitializationError( + 'Missing contextManager or logUri. Cannot initialize logging.', + ); + } + for (let attempt = 1; attempt <= retries; attempt++) { + try { if (!logInitialized) { - throw new LogInitializationError( - `Failed to initialize logs at path: ${logPath}`, - ); + logInitialized = await initLogs(logPath); + + if (!logInitialized) { + throw new LogInitializationError( + `Failed to initialize logs at path: ${logPath}`, + ); + } + console.log('Log initialization successful.'); } - console.log('Log initialization successful.'); - } - try { - initializeWebSockets(); + this.initializeWebSockets(); console.log('Successfully initialized WebSockets. Logging is now active.'); return; - } catch { - throw new WebSocketInitializationError('Failed to initialize WebSockets.'); - } - } catch (error) { - const err = error as Error; - console.error(`[Attempt ${attempt}/${retries}] ${err.name}: ${err.message}`); - - if (attempt < retries) { - console.log(`Retrying in ${delay}ms...`); - await new Promise((resolve) => setTimeout(resolve, delay)); - delay *= 2; // Exponential backoff - } else { - console.error('Max retries reached. Logging process failed.'); + } catch (error) { + const err = error as Error; + console.error(`[Attempt ${attempt}/${retries}] ${err.name}: ${err.message}`); + + if (attempt < retries) { + console.log(`Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + delay *= 2; // Exponential backoff + } else { + throw new Error('Max retries reached. Logging process failed.'); + } } } } -} -function initializeWebSockets(): void { - if (!CHANNELS_CREATED) { - createOutputChannels(); - CHANNELS_CREATED = true; + /** + * Initializes WebSocket connections for logging. + */ + private initializeWebSockets(): void { + if (!this.channelsCreated) { + this.createOutputChannels(); + this.channelsCreated = true; + } + this.startWebSocket('main'); + this.startWebSocket('detect'); + this.startWebSocket('refactor'); } - startWebSocket('main'); - startWebSocket('detect'); - startWebSocket('refactor'); -} -function createOutputChannels(): void { - console.log('Creating ouput channels'); - for (const channel of Object.keys(channels)) { - channels[channel].channel = vscode.window.createOutputChannel( - channels[channel].name, - { log: true }, - ); + /** + * Creates output channels for logging. + */ + private createOutputChannels(): void { + console.log('Creating output channels'); + for (const channel of Object.keys(this.channels)) { + this.channels[channel].channel = vscode.window.createOutputChannel( + this.channels[channel].name, + { log: true }, + ); + } } -} -function startWebSocket(logType: string): void { - const url = `${WEBSOCKET_BASE_URL}/${logType}`; - const ws = new WebSocket(url); - websockets[logType] = ws; - - ws.on('message', function message(data) { - const logEvent = data.toString('utf8'); - const level = - logEvent.match(/\b(ERROR|DEBUG|INFO|WARNING|TRACE)\b/i)?.[0].trim() || - 'UNKNOWN'; - const msg = logEvent.split(`[${level}]`, 2)[1].trim(); - - console.log(logEvent); - console.log('Level:', level); - - switch (level) { - case 'ERROR': { - channels[logType].channel!.error(msg); - break; - } - case 'DEBUG': { - console.log('logging debug'); - channels[logType].channel!.debug(msg); - break; - } - case 'WARNING': { - channels[logType].channel!.warn(msg); - break; - } - case 'CRITICAL': { - channels[logType].channel!.error(msg); - break; - } - default: { - console.log('Logging info'); - channels[logType].channel!.info(msg); - break; + /** + * Starts a WebSocket connection for a specific log type. + * @param logType - The type of log (e.g., 'main', 'detect', 'refactor'). + */ + private startWebSocket(logType: string): void { + const url = `${WEBSOCKET_BASE_URL}/${logType}`; + const ws = new WebSocket(url); + this.websockets[logType] = ws; + + ws.on('message', (data) => { + const logEvent = data.toString('utf8'); + const level = + logEvent.match(/\b(ERROR|DEBUG|INFO|WARNING|TRACE)\b/i)?.[0].trim() || + 'UNKNOWN'; + const msg = logEvent.split(`[${level}]`, 2)[1].trim(); + + console.log(logEvent); + console.log('Level:', level); + + switch (level) { + case 'ERROR': { + this.channels[logType].channel!.error(msg); + break; + } + case 'DEBUG': { + this.channels[logType].channel!.debug(msg); + break; + } + case 'WARNING': { + this.channels[logType].channel!.warn(msg); + break; + } + case 'CRITICAL': { + this.channels[logType].channel!.error(msg); + break; + } + default: { + this.channels[logType].channel!.info(msg); + break; + } } - } - }); - - ws.on('error', function error(err) { - channels[logType].channel!.error(err); - }); - - ws.on('close', function close() { - channels[logType].channel!.appendLine( - `WebSocket connection closed for ${channels[logType].name}`, - ); - }); - - ws.on('open', function open() { - channels[logType].channel!.appendLine(`Connected to ${logType} via WebSocket`); - }); -} - -/** - * Stops watching log files when the extension is deactivated. - */ -export function stopWatchingLogs(): void { - Object.values(websockets).forEach((ws) => ws?.close()); + }); + + ws.on('error', (err) => { + this.channels[logType].channel!.error(`WebSocket error: ${err.message}`); + }); + + ws.on('close', () => { + this.channels[logType].channel!.appendLine( + `WebSocket connection closed for ${this.channels[logType].name}`, + ); + }); + + ws.on('open', () => { + this.channels[logType].channel!.appendLine( + `Connected to ${logType} via WebSocket`, + ); + }); + } - Object.values(channels).forEach((channel) => channel.channel?.dispose()); + /** + * Stops watching logs and cleans up resources. + */ + public stopWatchingLogs(): void { + Object.values(this.websockets).forEach((ws) => ws?.close()); + Object.values(this.channels).forEach((channel) => channel.channel?.dispose()); + } } diff --git a/src/commands/toggleSmellLinting.ts b/src/commands/toggleSmellLinting.ts index d4b4f54..c605645 100644 --- a/src/commands/toggleSmellLinting.ts +++ b/src/commands/toggleSmellLinting.ts @@ -1,50 +1,49 @@ -import * as vscode from 'vscode'; -import { ContextManager } from '../context/contextManager'; -import { detectSmells } from './detectSmells'; -import { FileHighlighter } from '../ui/fileHighlighter'; // Import the class -import { envConfig } from '../utils/envConfig'; +// import * as vscode from 'vscode'; +// import { ContextManager } from '../context/contextManager'; +// import { FileHighlighter } from '../ui/fileHighlighter'; // Import the class +// import { envConfig } from '../utils/envConfig'; -export async function toggleSmellLinting( - contextManager: ContextManager, -): Promise { - const isEnabled = contextManager.getWorkspaceData( - envConfig.SMELL_LINTING_ENABLED_KEY, - false, - ); - const newState = !isEnabled; +// export async function toggleSmellLinting( +// contextManager: ContextManager, +// ): Promise { +// const isEnabled = contextManager.getWorkspaceData( +// envConfig.SMELL_LINTING_ENABLED_KEY, +// false, +// ); +// const newState = !isEnabled; - // Update state immediately for UI responsiveness - vscode.commands.executeCommand('setContext', 'eco.smellLintingEnabled', newState); +// // Update state immediately for UI responsiveness +// vscode.commands.executeCommand('setContext', 'eco.smellLintingEnabled', newState); - // Use the singleton instance of FileHighlighter - const fileHighlighter = FileHighlighter.getInstance(contextManager); +// // Use the singleton instance of FileHighlighter +// const fileHighlighter = FileHighlighter.getInstance(contextManager); - try { - if (newState) { - // Run detection and update state on success - await detectSmells(contextManager); // in the future recieve a true/false +// try { +// if (newState) { +// // Run detection and update state on success +// await detectSmells(contextManager); // in the future recieve a true/false - await contextManager.setWorkspaceData( - envConfig.SMELL_LINTING_ENABLED_KEY, - newState, - ); - } else { - // Clear highlights and update state - fileHighlighter.resetHighlights(); // Call resetHighlights on the singleton instance - await contextManager.setWorkspaceData( - envConfig.SMELL_LINTING_ENABLED_KEY, - newState, - ); - vscode.window.showInformationMessage('Eco: Smell linting turned off.'); - } - } catch (error) { - console.error('Eco: Error toggling smell linting:', error); - vscode.window.showErrorMessage('Eco: Failed to toggle smell linting.'); - // Ensure UI state matches actual on error - vscode.commands.executeCommand( - 'setContext', - 'eco.smellLintingEnabled', - isEnabled, - ); - } -} +// await contextManager.setWorkspaceData( +// envConfig.SMELL_LINTING_ENABLED_KEY, +// newState, +// ); +// } else { +// // Clear highlights and update state +// fileHighlighter.resetHighlights(); // Call resetHighlights on the singleton instance +// await contextManager.setWorkspaceData( +// envConfig.SMELL_LINTING_ENABLED_KEY, +// newState, +// ); +// vscode.window.showInformationMessage('Eco: Smell linting turned off.'); +// } +// } catch (error) { +// console.error('Eco: Error toggling smell linting:', error); +// vscode.window.showErrorMessage('Eco: Failed to toggle smell linting.'); +// // Ensure UI state matches actual on error +// vscode.commands.executeCommand( +// 'setContext', +// 'eco.smellLintingEnabled', +// isEnabled, +// ); +// } +// } diff --git a/src/commands/wipeWorkCache.ts b/src/commands/wipeWorkCache.ts index 345a723..768c780 100644 --- a/src/commands/wipeWorkCache.ts +++ b/src/commands/wipeWorkCache.ts @@ -1,52 +1,28 @@ import * as vscode from 'vscode'; -import { ContextManager } from '../context/contextManager'; -import { envConfig } from '../utils/envConfig'; -import { updateHash } from '../utils/hashDocs'; - +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; + +/** + * Clears the smells cache and refreshes the UI. + * @param smellsCacheManager - Manages the caching of smells and file hashes. + * @param smellsDisplayProvider - The UI provider for updating the tree view. + */ export async function wipeWorkCache( - contextManager: ContextManager, - reason?: string, -): Promise { - try { - console.log('Eco: Wiping workspace cache...'); - - // Clear stored smells cache - await contextManager.setWorkspaceData(envConfig.SMELL_MAP_KEY!, {}); - - if (reason === 'manual') { - await contextManager.setWorkspaceData(envConfig.FILE_CHANGES_KEY!, {}); - } - - // Update file hashes for all open editors - const visibleEditors = vscode.window.visibleTextEditors; - - if (visibleEditors.length === 0) { - console.log('Eco: No open files to update hash.'); - } else { - console.log(`Eco: Updating cache for ${visibleEditors.length} visible files.`); - } - - for (const editor of visibleEditors) { - if (editor.document) { - await updateHash(contextManager, editor.document); - } - } - - // ✅ Determine the appropriate message - let message = 'Eco: Successfully wiped workspace cache! ✅'; - if (reason === 'settings') { - message = - 'Eco: Smell detection settings changed. Cache wiped to apply updates. ✅'; - } else if (reason === 'manual') { - message = 'Eco: Workspace cache manually wiped by user. ✅'; - } - - vscode.window.showInformationMessage(message); - console.log('Eco:', message); - } catch (error: any) { - console.error('Eco: Error while wiping workspace cache:', error); - vscode.window.showErrorMessage( - `Eco: Failed to wipe workspace cache. See console for details.`, - ); + smellsCacheManager: SmellsCacheManager, + smellsDisplayProvider: SmellsDisplayProvider, +) { + const userResponse = await vscode.window.showWarningMessage( + 'Are you sure you want to clear the smells cache? This action cannot be undone.', + { modal: true }, + 'Confirm', + ); + + if (userResponse === 'Confirm') { + // Use SmellsCacheManager to clear cache & refresh UI + await smellsCacheManager.clearCacheAndRefreshUI(smellsDisplayProvider); + + vscode.window.showInformationMessage('Smells cache cleared successfully.'); + } else { + vscode.window.showInformationMessage('Operation cancelled.'); } } diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts new file mode 100644 index 0000000..b015ea9 --- /dev/null +++ b/src/context/SmellsCacheManager.ts @@ -0,0 +1,136 @@ +import * as vscode from 'vscode'; +import { createHash } from 'crypto'; +import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { envConfig } from '../utils/envConfig'; + +/** + * Manages caching of detected smells and file hashes to avoid redundant backend calls. + */ +export class SmellsCacheManager { + constructor(private context: vscode.ExtensionContext) {} + + // ============================ + // Smell Caching Methods + // ============================ + + /** + * Retrieves cached smells for a given file. + * If the file has been analyzed and found clean, this will return an empty array. + * + * @param filePath - The absolute path of the file. + * @returns An array of detected smells or `undefined` if the file has not been analyzed. + */ + public getCachedSmells(filePath: string): Smell[] | undefined { + const cache = this.context.workspaceState.get>( + envConfig.SMELL_CACHE_KEY!, + {}, + ); + + return cache[filePath]; // Returns an array of smells or `undefined` if not cached + } + + /** + * Caches detected smells for a given file. + * If no smells are detected, caches an empty array to avoid redundant backend calls. + * + * @param filePath - The absolute path of the file. + * @param smells - The detected smells to store (empty array if no smells found). + */ + public async setCachedSmells(filePath: string, smells: Smell[]): Promise { + const cache = this.context.workspaceState.get>( + envConfig.SMELL_CACHE_KEY!, + {}, + ); + + cache[filePath] = smells; // Store detected smells or an empty array if clean + + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + } + + /** + * Clears all cached smells from the workspace. + * This forces a fresh analysis of all files when `detectSmellsFile` is called. + */ + public async clearSmellsCache(): Promise { + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, undefined); + } + + /** + * Clears cached smells for a specific file. + * + * @param filePath - The path of the file to clear. + */ + public async clearCachedSmellsForFile(filePath: string): Promise { + const cache = this.context.workspaceState.get>( + envConfig.SMELL_CACHE_KEY!, + {}, + ); + + delete cache[filePath]; // Remove the file's cached smells + + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + } + + // ============================ + // File Hash Caching Methods + // ============================ + + /** + * Computes a SHA256 hash of a file's contents. + * @param content - The file content as a string. + * @returns A SHA256 hash string. + */ + public computeFileHash(content: string): string { + return createHash('sha256').update(content).digest('hex'); + } + + /** + * Stores a hash of a file's contents in workspaceState. + * @param filePath - Absolute path of the file. + * @param hash - The computed file hash. + */ + public async storeFileHash(filePath: string, hash: string): Promise { + const hashes = this.context.workspaceState.get>( + envConfig.FILE_HASH_CACHE_KEY!, + {}, + ); + hashes[filePath] = hash; + await this.context.workspaceState.update(envConfig.FILE_HASH_CACHE_KEY!, hashes); + } + + /** + * Retrieves the stored hash for a given file. + * @param filePath - Absolute path of the file. + * @returns The stored hash or undefined if not found. + */ + public getStoredFileHash(filePath: string): string | undefined { + const hashes = this.context.workspaceState.get>( + envConfig.FILE_HASH_CACHE_KEY!, + {}, + ); + return hashes[filePath]; + } + + // ============================ + // UI Refresh Methods + // ============================ + + /** + * Clears all cached smells and refreshes the UI. + * Used by both "Clear Smells Cache" and "Reset Configuration". + * + * @param smellsDisplayProvider - The tree view provider responsible for the UI. + */ + public async clearCacheAndRefreshUI( + smellsDisplayProvider: SmellsDisplayProvider, + ): Promise { + // Remove all cached smells from the workspace state + await this.clearSmellsCache(); + + // Reset the UI state, including icons and dropdowns + smellsDisplayProvider.resetAllSmells(); + + // Refresh the UI to reflect the cleared cache + smellsDisplayProvider.refresh(); + } +} diff --git a/src/context/contextManager.ts b/src/context/contextManager.ts index 896255c..067b008 100644 --- a/src/context/contextManager.ts +++ b/src/context/contextManager.ts @@ -1,5 +1,9 @@ import * as vscode from 'vscode'; +/** + * Manages persistent data storage within VS Code's workspace state. + * This includes global and workspace-specific data. + */ export class ContextManager { public context: vscode.ExtensionContext; @@ -7,7 +11,17 @@ export class ContextManager { this.context = context; } - // Global state example + // ============================ + // Global State (Persists across VS Code sessions) + // ============================ + + /** + * Retrieves globally stored data that persists across VS Code sessions. + * + * @param key - The key associated with the stored value. + * @param defaultVal - The default value to return if the key does not exist. + * @returns The stored data or the default value. + */ public getGlobalData( key: string, defaultVal: any = undefined, @@ -15,11 +29,27 @@ export class ContextManager { return this.context.globalState.get(key, defaultVal); } + /** + * Updates global data that persists across VS Code sessions. + * + * @param key - The key for storing the value. + * @param value - The value to store. + */ public setGlobalData(key: string, value: any): Thenable { return this.context.globalState.update(key, value); } - // Workspace state example + // ============================ + // Workspace State (Resets per workspace) + // ============================ + + /** + * Retrieves workspace-specific data that resets when the user changes workspaces. + * + * @param key - The key associated with the stored value. + * @param defaultVal - The default value to return if the key does not exist. + * @returns The stored data or the default value. + */ public getWorkspaceData( key: string, defaultVal: any = undefined, @@ -27,6 +57,12 @@ export class ContextManager { return this.context.workspaceState.get(key, defaultVal); } + /** + * Updates workspace-specific data. + * + * @param key - The key for storing the value. + * @param value - The value to store. + */ public setWorkspaceData(key: string, value: any): Thenable { return this.context.workspaceState.update(key, value); } diff --git a/src/extension.ts b/src/extension.ts index 10fc13a..940c227 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,70 +1,107 @@ +// eslint-disable-next-line unused-imports/no-unused-imports import { envConfig } from './utils/envConfig'; + import * as vscode from 'vscode'; -import { detectSmells } from './commands/detectSmells'; +import { configureWorkspace } from './commands/configureWorkspace'; +import { resetConfiguration } from './commands/resetConfiguration'; +import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; +import { openFile } from './commands/openFile'; +import { registerFilterSmellCommands } from './commands/filterSmells'; +import { jumpToSmell } from './commands/jumpToSmell'; +import { wipeWorkCache } from './commands/wipeWorkCache'; +import { SmellsDisplayProvider } from './providers/SmellsViewProvider'; +import { checkServerStatus } from './api/backend'; +import { FilterSmellsProvider } from './providers/FilterSmellsProvider'; +import { SmellsCacheManager } from './context/SmellsCacheManager'; +import { registerFileSaveListener } from './listeners/fileSaveListener'; +import { serverStatus } from './utils/serverStatus'; import { - refactorSelectedSmell, refactorAllSmellsOfType, + refactorSelectedSmell, } from './commands/refactorSmell'; -import { wipeWorkCache } from './commands/wipeWorkCache'; -import { stopWatchingLogs } from './commands/showLogs'; -import { ContextManager } from './context/contextManager'; -import { - getEnabledSmells, - handleSmellFilterUpdate, -} from './utils/handleSmellSettings'; -import { updateHash } from './utils/hashDocs'; -import { RefactorSidebarProvider } from './ui/refactorView'; -import { handleEditorChanges } from './utils/handleEditorChange'; +import { LogManager } from './commands/showLogs'; import { LineSelectionManager } from './ui/lineSelectionManager'; -import { checkServerStatus } from './api/backend'; -import { serverStatus } from './utils/serverStatus'; - -import { toggleSmellLinting } from './commands/toggleSmellLinting'; -export const globalData: { contextManager?: ContextManager } = { - contextManager: undefined, -}; +let logManager: LogManager; +/** + * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. + * @param context - The VS Code extension context. + */ export function activate(context: vscode.ExtensionContext): void { - console.log('Eco: Refactor Plugin Activated Successfully'); - const contextManager = new ContextManager(context); + console.log('Activating Eco-Optimizer extension...'); - globalData.contextManager = contextManager; + // Iniiialize the log manager + logManager = new LogManager(context); - // Show the settings popup if needed - // TODO: Setting to re-enable popup if disabled - const settingsPopupChoice = - contextManager.getGlobalData('showSettingsPopup'); + // Initialize the SmellsCacheManager for managing caching of smells and file hashes. + const smellsCacheManager = new SmellsCacheManager(context); - if (settingsPopupChoice === undefined || settingsPopupChoice) { - showSettingsPopup(); - } - - console.log('environment variables:', envConfig); + // Initialize the Code Smells View. + const smellsDisplayProvider = new SmellsDisplayProvider(context); + const codeSmellsView = vscode.window.createTreeView('ecooptimizer.view', { + treeDataProvider: smellsDisplayProvider, + }); + context.subscriptions.push(codeSmellsView); + // Start periodic backend status checks (every 10 seconds). checkServerStatus(); + setInterval(checkServerStatus, 10000); - let smellsData = contextManager.getWorkspaceData(envConfig.SMELL_MAP_KEY!) || {}; - contextManager.setWorkspaceData(envConfig.SMELL_MAP_KEY!, smellsData); + // Track the workspace configuration state. + const workspaceConfigured = Boolean( + context.workspaceState.get('workspaceConfiguredPath'), + ); + vscode.commands.executeCommand( + 'setContext', + 'workspaceState.workspaceConfigured', + workspaceConfigured, + ); - let fileHashes = - contextManager.getWorkspaceData(envConfig.FILE_CHANGES_KEY!) || {}; - contextManager.setWorkspaceData(envConfig.FILE_CHANGES_KEY!, fileHashes); + // Register workspace-related commands. + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.configureWorkspace', () => + configureWorkspace(context, smellsDisplayProvider), + ), + ); - // Check server health every 10 seconds - setInterval(checkServerStatus, 10000); + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.resetConfiguration', () => + resetConfiguration(context, smellsCacheManager, smellsDisplayProvider), + ), + ); + + // Initialize the Filter Smells View. + const filterSmellsProvider = new FilterSmellsProvider(context); + const filterSmellsView = vscode.window.createTreeView('ecooptimizer.filterView', { + treeDataProvider: filterSmellsProvider, + showCollapseAll: true, + }); + + // Associate the TreeView instance with the provider. + filterSmellsProvider.setTreeView(filterSmellsView); - // =============================================================== - // REGISTER COMMANDS - // =============================================================== + // Register filter-related commands. + registerFilterSmellCommands(context, filterSmellsProvider); - // Detect Smells Command + // Register code smell analysis commands. context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.detectSmells', async () => { - console.log('Eco: Detect Smells Command Triggered'); - detectSmells(contextManager); - }), + vscode.commands.registerCommand('ecooptimizer.openFile', openFile), + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'ecooptimizer.detectSmellsFolder', + (folderPath) => + detectSmellsFolder(smellsCacheManager, smellsDisplayProvider, folderPath), + ), + ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileUri) => + detectSmellsFile(smellsCacheManager, smellsDisplayProvider, fileUri), + ), ); // Refactor Selected Smell Command @@ -72,7 +109,7 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('ecooptimizer.refactorSmell', () => { if (serverStatus.getStatus() === 'up') { console.log('Eco: Refactor Selected Smell Command Triggered'); - refactorSelectedSmell(contextManager); + refactorSelectedSmell(context, smellsCacheManager); } else { vscode.window.showWarningMessage('Action blocked: Server is down.'); } @@ -88,7 +125,7 @@ export function activate(context: vscode.ExtensionContext): void { console.log( `Eco: Refactor All Smells of Type Command Triggered for ${smellId}`, ); - refactorAllSmellsOfType(contextManager, smellId); + refactorAllSmellsOfType(context, smellsCacheManager, smellId); } else { vscode.window.showWarningMessage('Action blocked: Server is down.'); } @@ -96,72 +133,29 @@ export function activate(context: vscode.ExtensionContext): void { ), ); - // Wipe Cache Command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.wipeWorkCache', async () => { - console.log('Eco: Wipe Work Cache Command Triggered'); - vscode.window.showInformationMessage( - 'Eco: Manually wiping workspace memory... ✅', - ); - await wipeWorkCache(contextManager, 'manual'); - }), - ); - - // screen button go brr - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.toggleSmellLinting', () => { - console.log('Eco: Toggle Smell Linting Command Triggered'); - toggleSmellLinting(contextManager); - }), - ); - - // =============================================================== - // REGISTER VIEWS - // =============================================================== - - const refactorProvider = new RefactorSidebarProvider(context); - context.subscriptions.push( - vscode.window.registerWebviewViewProvider( - RefactorSidebarProvider.viewType, - refactorProvider, - ), - ); - - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.showRefactorSidebar', () => - refactorProvider.updateView(), - ), - ); + // Register the "Toggle Smell Auto Lint" command. + // TODO: Uncomment this block after implementing smell linting + // context.subscriptions.push( + // vscode.commands.registerCommand('ecooptimizer.toggleSmellLinting', () => { + // console.log('Eco: Toggle Smell Linting Command Triggered'); + // toggleSmellLinting(contextManager); + // }), + // ); + // Register the "Jump to Smell" command. context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.pauseRefactorSidebar', () => - refactorProvider.pauseView(), - ), + vscode.commands.registerCommand('ecooptimizer.jumpToSmell', jumpToSmell), ); + // Register the "Clear Smells Cache" command. context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.clearRefactorSidebar', () => - refactorProvider.clearView(), - ), - ); - - // =============================================================== - // ADD LISTENERS - // =============================================================== - - // Register a listener for configuration changes - context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration((event) => { - handleConfigurationChange(event); + vscode.commands.registerCommand('ecooptimizer.wipeWorkCache', async () => { + await wipeWorkCache(smellsCacheManager, smellsDisplayProvider); }), ); - vscode.window.onDidChangeVisibleTextEditors(async (editors) => { - handleEditorChanges(contextManager, editors); - }); - // Adds comments to lines describing the smell - const lineSelectManager = new LineSelectionManager(contextManager); + const lineSelectManager = new LineSelectionManager(smellsCacheManager); context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection((event) => { console.log('Eco: Detected line selection event'); @@ -169,85 +163,54 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // Updates directory of file states (for checking if modified) - context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(async (document) => { - console.log('Eco: Detected document saved event'); - await updateHash(contextManager, document); - }), - ); - - // Handles case of documents already being open on VS Code open - vscode.window.visibleTextEditors.forEach(async (editor) => { - if (editor.document) { - await updateHash(contextManager, editor.document); - } - }); - - // Initializes first state of document when opened while extension is active - context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(async (document) => { - console.log('Eco: Detected document opened event'); - await updateHash(contextManager, document); - }), - ); - - // Listen for file save events + // Register a listener for configuration changes context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(async (document) => { - console.log('Eco: Detected document saved event'); - - // Check if smell linting is enabled - const isEnabled = contextManager.getWorkspaceData( - envConfig.SMELL_LINTING_ENABLED_KEY, - false, - ); - if (isEnabled) { - console.log('Eco: Smell linting is enabled. Detecting smells...'); - await detectSmells(contextManager); - } + vscode.workspace.onDidChangeConfiguration((event) => { + handleConfigurationChange(event); }), ); // Listen for editor changes - context.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor(async (editor) => { - if (editor) { - console.log('Eco: Detected editor change event'); - - // Check if the file is a Python file - if (editor.document.languageId === 'python') { - console.log('Eco: Active file is a Python file.'); - - // Check if smell linting is enabled - const isEnabled = contextManager.getWorkspaceData( - envConfig.SMELL_LINTING_ENABLED_KEY, - false, - ); - if (isEnabled) { - console.log('Eco: Smell linting is enabled. Detecting smells...'); - await detectSmells(contextManager); - } - } - } - }), + // TODO: Uncomment this block after implementing smell linting + // context.subscriptions.push( + // vscode.window.onDidChangeActiveTextEditor(async (editor) => { + // if (editor) { + // console.log('Eco: Detected editor change event'); + + // // Check if the file is a Python file + // if (editor.document.languageId === 'python') { + // console.log('Eco: Active file is a Python file.'); + + // // Check if smell linting is enabled + // const isEnabled = context.workspaceState.get( + // envConfig.SMELL_LINTING_ENABLED_KEY, + // false, + // ); + // if (isEnabled) { + // console.log('Eco: Smell linting is enabled. Detecting smells...'); + // await detectSmells(contextManager); + // } + // } + // } + // }), + // ); + + // Register the file save listener to detect outdated files. + const fileSaveListener = registerFileSaveListener( + smellsCacheManager, + smellsDisplayProvider, ); + context.subscriptions.push(fileSaveListener); - // =============================================================== - // HANDLE SMELL FILTER CHANGES - // =============================================================== - - let previousSmells = getEnabledSmells(); - vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration('ecooptimizer.enableSmells')) { - console.log('Eco: Smell preferences changed! Wiping cache.'); - handleSmellFilterUpdate(previousSmells, contextManager); - previousSmells = getEnabledSmells(); - } - }); + // TODO: Setting to re-enable popup if disabled + const settingsPopupChoice = context.globalState.get('showSettingsPopup'); + + if (settingsPopupChoice === undefined || settingsPopupChoice) { + showSettingsPopup(context); + } } -function showSettingsPopup(): void { +function showSettingsPopup(context: vscode.ExtensionContext): void { // Check if the required settings are already configured const config = vscode.workspace.getConfiguration('ecooptimizer'); const workspacePath = config.get('projectWorkspacePath', ''); @@ -277,7 +240,7 @@ function showSettingsPopup(): void { 'You can configure the paths later in the settings.', ); } else if (selection === 'Never show this again') { - globalData.contextManager!.setGlobalData('showSettingsPopup', false); + context.globalState.update('showSettingsPopup', false); vscode.window.showInformationMessage( 'You can re-enable this popup again in the settings.', ); @@ -290,7 +253,6 @@ function handleConfigurationChange(event: vscode.ConfigurationChangeEvent): void // Check if any relevant setting was changed if ( event.affectsConfiguration('ecooptimizer.projectWorkspacePath') || - event.affectsConfiguration('ecooptimizer.unitTestCommand') || event.affectsConfiguration('ecooptimizer.logsOutputPath') ) { // Display a warning message about changing critical settings @@ -300,7 +262,10 @@ function handleConfigurationChange(event: vscode.ConfigurationChangeEvent): void } } +/** + * Deactivates the Eco-Optimizer extension. + */ export function deactivate(): void { - console.log('Eco: Deactivating Plugin - Stopping Log Watching'); - stopWatchingLogs(); + console.log('Deactivating Eco-Optimizer extension...'); + logManager.stopWatchingLogs(); } diff --git a/src/listeners/fileSaveListener.ts b/src/listeners/fileSaveListener.ts new file mode 100644 index 0000000..ae07102 --- /dev/null +++ b/src/listeners/fileSaveListener.ts @@ -0,0 +1,48 @@ +import * as vscode from 'vscode'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import path from 'path'; + +/** + * Listens for file save events to detect outdated files. + * @param smellsCacheManager - Manages the caching of smells and file hashes. + * @param smellsDisplayProvider - The UI provider for updating the tree view. + */ +export function registerFileSaveListener( + smellsCacheManager: SmellsCacheManager, + smellsDisplayProvider: SmellsDisplayProvider, +): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(async (document) => { + const filePath = document.fileName; + + // Ignore files that have no cached smells + const cachedSmells = smellsCacheManager.getCachedSmells(filePath); + if (!cachedSmells) return; + + // Compute the new hash and compare it with the stored hash + const newHash = smellsCacheManager.computeFileHash(document.getText()); + const oldHash = smellsCacheManager.getStoredFileHash(filePath); + + if (oldHash && newHash !== oldHash) { + vscode.window.showWarningMessage( + `The file "${path.basename( + filePath, + )}" has been modified since the last analysis.`, + ); + + // Check if smell linting is enabled + // TODO: Uncomment this block after implementing smell linting + // const isEnabled = contextManager.getWorkspaceData( + // envConfig.SMELL_LINTING_ENABLED_KEY, + // false, + // ); + // if (isEnabled) { + // console.log('Eco: Smell linting is enabled. Detecting smells...'); + // await detectSmells(contextManager); + // } + + // Mark file as outdated in the UI + smellsDisplayProvider.markFileAsOutdated(filePath); + } + }); +} diff --git a/src/managers/SmellsViewStateManager.ts b/src/managers/SmellsViewStateManager.ts new file mode 100644 index 0000000..1e56c31 --- /dev/null +++ b/src/managers/SmellsViewStateManager.ts @@ -0,0 +1,135 @@ +import * as path from 'path'; + +interface DetectedSmell { + messageId: string; + symbol: string; + occurences: { line: number; endLine?: number }[]; +} + +interface ProcessedSmell { + acronym: string; + occurrences: { line: number; endLine?: number }[]; +} + +export class SmellsStateManager { + private fileStatusMap: Map = new Map(); + private detectedSmells: Map = new Map(); + private smellToFileMap: Map = new Map(); + private modifiedFiles: Map = new Map(); + + /** + * Updates the detected smells for a file. + * @param filePath - The analyzed file path. + * @param smells - The detected smells in the file. + * @param smellMetadata - Metadata containing message ID and acronym for each smell. + */ + updateSmells( + filePath: string, + smells: DetectedSmell[], + smellMetadata: Record, + ): void { + this.fileStatusMap.set(filePath, 'passed'); + + console.log('Smells:', smells, typeof smells); + const formattedSmells: ProcessedSmell[] = smells.map((smell) => { + const foundEntry = Object.values(smellMetadata).find( + (smellData) => smellData.message_id === smell.messageId, + ) as { message_id: string; acronym: string }; + + return { + acronym: foundEntry ? foundEntry.acronym : smell.messageId, + occurrences: smell.occurences.map((occ) => ({ + line: occ.line, + endLine: occ.endLine, + })), + }; + }); + + this.detectedSmells.set(filePath, formattedSmells); + + const folderPath = path.dirname(filePath); + if (!this.detectedSmells.has(folderPath)) { + this.detectedSmells.set(folderPath, []); + } + this.detectedSmells.get(folderPath)?.push(...formattedSmells); + } + + /** + * Marks a file as outdated. + * @param filePath - The path of the modified file. + */ + markFileAsOutdated(filePath: string): void { + this.modifiedFiles.set(filePath, true); + } + + /** + * Clears the outdated status for a file. + * @param filePath - The path of the file to clear. + */ + clearOutdatedStatus(filePath: string): void { + this.modifiedFiles.delete(filePath); + } + + /** + * Updates the status of a specific file or folder. + * @param filePath - The file or folder path. + * @param status - The new status to set. + */ + updateFileStatus(filePath: string, status: string): void { + this.fileStatusMap.set(filePath, status); + } + + /** + * Checks if a file is marked as outdated. + * @param filePath - The path of the file to check. + * @returns `true` if the file is outdated, `false` otherwise. + */ + isFileOutdated(filePath: string): boolean { + return this.modifiedFiles.has(filePath); + } + + /** + * Clears all detected smells and resets file statuses. + */ + resetAllSmells(): void { + this.detectedSmells.clear(); + this.fileStatusMap.clear(); + this.modifiedFiles.clear(); + } + + /** + * Retrieves the status of a file. + * @param filePath - The path of the file. + * @returns The status of the file. + */ + getFileStatus(filePath: string): string { + return this.fileStatusMap.get(filePath) || 'not_detected'; + } + + /** + * Retrieves the detected smells for a file. + * @param filePath - The path of the file. + * @returns An array of smell entries. + */ + getSmellsForFile(filePath: string): ProcessedSmell[] { + return this.detectedSmells.get(filePath) || []; + } + + /** + * Maps a smell description to a file path. + * @param smellDescription - The description of the smell. + * @param filePath - The path of the file. + */ + mapSmellToFile(smellDescription: string, filePath: string): void { + this.smellToFileMap.set(smellDescription, filePath); + } + + /** + * Retrieves the file path for a smell description. + * @param smellDescription - The description of the smell. + * @returns The file path, or `undefined` if not found. + */ + getFileForSmell(smellDescription: string): string | undefined { + return this.smellToFileMap.get(smellDescription); + } +} diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts new file mode 100644 index 0000000..d164dca --- /dev/null +++ b/src/managers/SmellsViewUIManager.ts @@ -0,0 +1,185 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { SmellsStateManager } from './SmellsViewStateManager'; + +export class SmellsUIManager { + constructor(private stateManager: SmellsStateManager) {} + + /** + * Creates a tree item for a given element (folder, file, or smell). + * @param element - The file or folder path, or a detected smell. + */ + createTreeItem(element: string): vscode.TreeItem { + const status = this.stateManager.getFileStatus(element); + const hasSmells = this.stateManager.getSmellsForFile(element).length > 0; + const isDirectory = fs.existsSync(element) && fs.statSync(element).isDirectory(); + const isSmellItem = !fs.existsSync(element) && !isDirectory; + + // Check if the file is outdated + const isOutdated = + !isDirectory && !isSmellItem && this.stateManager.isFileOutdated(element); + + // Set the collapsible state + let collapsibleState: vscode.TreeItemCollapsibleState; + if (isDirectory) { + // Directories are always collapsible + collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + } else if (isSmellItem) { + // Smell items are never collapsible + collapsibleState = vscode.TreeItemCollapsibleState.None; + } else if (isOutdated) { + // Outdated files are not collapsible + collapsibleState = vscode.TreeItemCollapsibleState.None; + } else { + // Files with smells are collapsible + collapsibleState = hasSmells + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None; + } + + const item = new vscode.TreeItem(path.basename(element), collapsibleState); + + if (isDirectory) { + item.contextValue = 'ecoOptimizerFolder'; + } else if (!isSmellItem) { + item.contextValue = 'ecoOptimizerFile'; + this.assignOpenFileCommand(item, element); + this.updateFileItem(item, status, isOutdated); + } else { + item.contextValue = 'ecoOptimizerSmell'; + const parentFile = this.stateManager.getFileForSmell(element); + if (parentFile) { + const [, lineStr] = element.split(': Line '); + const lines = lineStr.split(',').map((line) => parseInt(line.trim(), 10)); + const firstLine = lines.length > 0 ? lines[0] - 1 : 0; + this.assignJumpToSmellCommand(item, parentFile, firstLine); + } + this.setSmellTooltip(item, element); + } + + return item; + } + + /** + * Assigns a command to open a file when the tree item is clicked. + * @param item - The tree item to update. + * @param filePath - The path of the file to open. + */ + private assignOpenFileCommand(item: vscode.TreeItem, filePath: string): void { + item.command = { + command: 'ecooptimizer.openFile', + title: 'Open File', + arguments: [vscode.Uri.file(filePath)], + }; + } + + /** + * Updates the file item's status, including icon, message, and description. + * @param item - The tree item to update. + * @param status - The analysis status (e.g., "queued", "passed", "failed", "outdated"). + * @param isOutdated - Whether the file is outdated. + */ + private updateFileItem( + item: vscode.TreeItem, + status: string, + isOutdated: boolean, + ): void { + if (isOutdated) { + item.description = 'outdated'; + item.iconPath = new vscode.ThemeIcon( + 'warning', + new vscode.ThemeColor('charts.orange'), + ); + } else { + item.iconPath = this.getStatusIcon(status); + } + item.tooltip = `${path.basename( + item.label as string, + )} (${this.getStatusMessage(status)})`; + } + + /** + * Assigns a command to jump to a specific line in a file when the tree item is clicked. + * @param item - The tree item to update. + * @param filePath - The path of the file containing the smell. + * @param line - The line number to jump to. + */ + private assignJumpToSmellCommand( + item: vscode.TreeItem, + filePath: string, + line: number, + ): void { + item.command = { + command: 'ecooptimizer.jumpToSmell', + title: 'Jump to Smell', + arguments: [filePath, line], + }; + } + + /** + * Sets the tooltip for a smell item. + * @param item - The tree item to update. + * @param smellDescription - The description of the smell. + */ + private setSmellTooltip(item: vscode.TreeItem, smellDescription: string): void { + item.tooltip = smellDescription; + } + + /** + * Retrieves the appropriate VS Code icon based on the smell analysis status. + * @param status - The analysis status. + * @returns The corresponding VS Code theme icon. + */ + private getStatusIcon(status: string): vscode.ThemeIcon { + switch (status) { + case 'queued': + return new vscode.ThemeIcon( + 'sync~spin', + new vscode.ThemeColor('charts.yellow'), + ); + case 'passed': + return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.green')); + case 'no_issues': + return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.blue')); + case 'failed': + return new vscode.ThemeIcon('error', new vscode.ThemeColor('charts.red')); + case 'outdated': + return new vscode.ThemeIcon( + 'warning', + new vscode.ThemeColor('charts.orange'), + ); + case 'server_down': + return new vscode.ThemeIcon( + 'server-process', + new vscode.ThemeColor('charts.red'), + ); + default: + return new vscode.ThemeIcon('circle-outline'); + } + } + + /** + * Retrieves the status message corresponding to the smell analysis state. + * @param status - The analysis status. + * @returns A descriptive status message. + */ + private getStatusMessage(status: string): string { + switch (status) { + case 'queued': + return 'Analyzing Smells'; + case 'passed': + return 'Smells Successfully Detected'; + case 'failed': + return 'Error Detecting Smells'; + case 'no_issues': + return 'No Smells Found'; + case 'outdated': + return 'File Outdated - Needs Reanalysis'; + case 'server_down': + return 'Server Unavailable'; + default: + return 'Smells Not Yet Detected'; + } + } +} diff --git a/src/providers/FilterSmellsProvider.ts b/src/providers/FilterSmellsProvider.ts new file mode 100644 index 0000000..f5e975f --- /dev/null +++ b/src/providers/FilterSmellsProvider.ts @@ -0,0 +1,181 @@ +import * as vscode from 'vscode'; +import { FilterSmellConfig, loadSmells, saveSmells } from '../utils/smellsData'; + +/** + * Provides a tree view for filtering code smells within the VS Code extension. + */ +export class FilterSmellsProvider + implements vscode.TreeDataProvider +{ + private _onDidChangeTreeData: vscode.EventEmitter< + vscode.TreeItem | undefined | void + > = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + + private treeView?: vscode.TreeView; + private smells: Record = {}; + + constructor(private context: vscode.ExtensionContext) { + this.smells = loadSmells(); + } + + /** + * Associates a TreeView instance with the provider and listens for checkbox state changes. + * @param treeView - The TreeView instance. + */ + setTreeView(treeView: vscode.TreeView): void { + this.treeView = treeView; + + this.treeView.onDidChangeCheckboxState((event) => { + event.items.forEach((item) => { + if (item[0] instanceof SmellItem) { + this.toggleSmell(item[0].key); + } + }); + }); + } + + /** + * Returns the tree item representation for a given element. + * @param element - The tree item element. + */ + getTreeItem(element: vscode.TreeItem): vscode.TreeItem { + return element; + } + + /** + * Retrieves the child elements for a given tree item. + * If no parent element is provided, returns the list of smells. + * @param element - The parent tree item (optional). + */ + getChildren(element?: SmellItem): Thenable { + if (!element) { + return Promise.resolve( + Object.keys(this.smells).map((smellKey) => { + const smell = this.smells[smellKey]; + return new SmellItem( + smellKey, + smell.name, + smell.enabled, + smell.analyzer_options && Object.keys(smell.analyzer_options).length > 0 + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None, + ); + }), + ); + } + + const options = this.smells[element.key]?.analyzer_options; + return options + ? Promise.resolve( + Object.entries(options).map( + ([optionKey, optionData]) => + new SmellOptionItem( + optionKey, + optionData.label, + optionData.value, + optionData.description, + element.key, + ), + ), + ) + : Promise.resolve([]); + } + + /** + * Toggles the enabled state of a specific smell and updates the configuration file. + * @param smellKey - The key of the smell to toggle. + */ + toggleSmell(smellKey: string): void { + if (this.smells[smellKey]) { + this.smells[smellKey].enabled = !this.smells[smellKey].enabled; + saveSmells(this.smells); + this._onDidChangeTreeData.fire(); + } + } + + /** + * Updates the value of a specific smell option and saves the configuration. + * @param smellKey - The key of the smell. + * @param optionKey - The key of the option. + * @param newValue - The new value to set. + */ + updateOption( + smellKey: string, + optionKey: string, + newValue: number | string, + ): void { + if (this.smells[smellKey]?.analyzer_options?.[optionKey]) { + this.smells[smellKey].analyzer_options[optionKey].value = newValue; + saveSmells(this.smells); + this._onDidChangeTreeData.fire(); + } else { + vscode.window.showErrorMessage( + `Error: No analyzer option found for ${optionKey}`, + ); + } + } + + /** + * Refreshes the tree view, updating the UI. + */ + refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Enables or disables all smells in the filter and updates the configuration. + * @param enabled - Whether all smells should be enabled or disabled. + */ + setAllSmellsEnabled(enabled: boolean): void { + Object.keys(this.smells).forEach((key) => { + this.smells[key].enabled = enabled; + }); + saveSmells(this.smells); + this._onDidChangeTreeData.fire(); + } +} + +/** + * Represents a smell item in the tree view. + */ +class SmellItem extends vscode.TreeItem { + constructor( + public readonly key: string, + public readonly name: string, + public enabled: boolean, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + ) { + super(name, collapsibleState); + this.contextValue = 'smellItem'; + this.checkboxState = enabled + ? vscode.TreeItemCheckboxState.Checked + : vscode.TreeItemCheckboxState.Unchecked; + } +} + +/** + * Represents an option item for a smell in the tree view. + */ +class SmellOptionItem extends vscode.TreeItem { + constructor( + public readonly optionKey: string, + public readonly label: string, + public value: number | string, + public readonly description: string, + public readonly smellKey: string, + ) { + super('placeholder', vscode.TreeItemCollapsibleState.None); + + this.contextValue = 'smellOption'; + this.label = `${label}: ${value}`; + this.tooltip = description; + this.description = ''; + this.command = { + command: 'ecooptimizer.editSmellFilterOption', + title: 'Edit Option', + arguments: [this], + }; + } +} diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts new file mode 100644 index 0000000..95d48b3 --- /dev/null +++ b/src/providers/SmellsViewProvider.ts @@ -0,0 +1,130 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { SmellsStateManager } from '../managers/SmellsViewStateManager'; +import { SmellsUIManager } from '../managers/SmellsViewUIManager'; + +export class SmellsDisplayProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private stateManager: SmellsStateManager; + private uiManager: SmellsUIManager; + + constructor(private context: vscode.ExtensionContext) { + this.stateManager = new SmellsStateManager(); + this.uiManager = new SmellsUIManager(this.stateManager); + } + + /** + * Refreshes the tree view, triggering a UI update. + */ + refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Returns a tree item representing a file, folder, or detected smell. + * @param element - The file or folder path, or a detected smell. + */ + getTreeItem(element: string): vscode.TreeItem { + return this.uiManager.createTreeItem(element); + } + + /** + * Retrieves child elements for a given tree item. + * @param element - The parent tree item (optional). + */ + async getChildren(element?: string): Promise { + if (!element) { + const configuredPath = this.context.workspaceState.get( + 'workspaceConfiguredPath', + ); + return configuredPath ? [configuredPath] : []; + } + + const isDirectory = fs.existsSync(element) && fs.statSync(element).isDirectory(); + + if (isDirectory) { + return fs + .readdirSync(element) + .filter((file) => file.endsWith('.py')) + .map((file) => path.join(element, file)); + } + + // Check if the file is outdated + if (this.stateManager.isFileOutdated(element)) { + return []; // Return an empty array if the file is outdated + } + + // If the file is not outdated, return the detected smells + const smells = this.stateManager.getSmellsForFile(element); + return smells.map((smell) => { + const smellItem = `${smell.acronym}: Line ${smell.occurrences + .map((o) => o.line) + .join(', ')}`; + this.stateManager.mapSmellToFile(smellItem, element); + return smellItem; + }); + } + + /** + * Updates the detected smells for a file and refreshes the tree view. + * @param filePath - The analyzed file path. + * @param smells - The detected smells in the file. + * @param smellMetadata - Metadata containing message ID and acronym for each smell. + */ + updateSmells( + filePath: string, + smells: Smell[], + smellMetadata: Record, + ): void { + this.stateManager.updateSmells(filePath, smells, smellMetadata); + this.refresh(); + } + + /** + * Marks a file as outdated, updating its appearance in the UI. + * @param filePath - The path of the modified file. + */ + markFileAsOutdated(filePath: string): void { + this.stateManager.markFileAsOutdated(filePath); + this.refresh(); + } + + /** + * Updates the status of a specific file or folder. + * @param element - The file or folder path. + * @param status - The new status to set. + */ + updateStatus(element: string, status: string): void { + this.stateManager.updateFileStatus(element, status); + this.refresh(); + } + + /** + * Clears the outdated status for a file. + * @param filePath - The path of the file to clear. + */ + clearOutdatedStatus(filePath: string): void { + this.stateManager.clearOutdatedStatus(filePath); + this.refresh(); + } + + /** + * Checks if a file is marked as outdated. + * @param filePath - The path of the file to check. + * @returns `true` if the file is outdated, `false` otherwise. + */ + isFileOutdated(filePath: string): boolean { + return this.stateManager.isFileOutdated(filePath); + } + + /** + * Clears all detected smells and resets file statuses. + */ + resetAllSmells(): void { + this.stateManager.resetAllSmells(); + this.refresh(); + } +} diff --git a/src/ui/fileHighlighter.ts b/src/ui/fileHighlighter.ts index 1d95989..c620507 100644 --- a/src/ui/fileHighlighter.ts +++ b/src/ui/fileHighlighter.ts @@ -1,20 +1,23 @@ import * as vscode from 'vscode'; import { getEditor } from '../utils/editorUtils'; -import { ContextManager } from '../context/contextManager'; import { HoverManager } from './hoverManager'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; export class FileHighlighter { private static instance: FileHighlighter; - private contextManager: ContextManager; private decorations: vscode.TextEditorDecorationType[] = []; - private constructor(contextManager: ContextManager) { - this.contextManager = contextManager; - } + constructor( + private context: vscode.ExtensionContext, + private smellsCacheManager: SmellsCacheManager, + ) {} - public static getInstance(contextManager: ContextManager): FileHighlighter { + public static getInstance( + context: vscode.ExtensionContext, + smellsCacheManager: SmellsCacheManager, + ): FileHighlighter { if (!FileHighlighter.instance) { - FileHighlighter.instance = new FileHighlighter(contextManager); + FileHighlighter.instance = new FileHighlighter(context, smellsCacheManager); } return FileHighlighter.instance; } @@ -71,7 +74,11 @@ export class FileHighlighter { const indexEnd = lineText.trimEnd().length + 2; const range = new vscode.Range(line, indexStart, line, indexEnd); - const hoverManager = HoverManager.getInstance(this.contextManager, smells); + const hoverManager = HoverManager.getInstance( + this.context, + this.smellsCacheManager, + smells, + ); return { range, hoverMessage: hoverManager.hoverContent || undefined }; }); diff --git a/src/ui/hoverManager.ts b/src/ui/hoverManager.ts index 09df83e..621b539 100644 --- a/src/ui/hoverManager.ts +++ b/src/ui/hoverManager.ts @@ -3,17 +3,20 @@ import { refactorSelectedSmell, refactorAllSmellsOfType, } from '../commands/refactorSmell'; -import { ContextManager } from '../context/contextManager'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; export class HoverManager { private static instance: HoverManager; private smells: Smell[]; public hoverContent: vscode.MarkdownString; - private vscodeContext: vscode.ExtensionContext; - static getInstance(contextManager: ContextManager, smells: Smell[]): HoverManager { + static getInstance( + context: vscode.ExtensionContext, + smellsCacheManager: SmellsCacheManager, + smells: Smell[], + ): HoverManager { if (!HoverManager.instance) { - HoverManager.instance = new HoverManager(contextManager, smells); + HoverManager.instance = new HoverManager(context, smellsCacheManager, smells); } else { HoverManager.instance.updateSmells(smells); } @@ -21,11 +24,11 @@ export class HoverManager { } public constructor( - private contextManager: ContextManager, + private context: vscode.ExtensionContext, + private smellsCacheManager: SmellsCacheManager, smells: Smell[], ) { this.smells = smells || []; - this.vscodeContext = contextManager.context; this.hoverContent = this.registerHoverProvider() ?? new vscode.MarkdownString(); this.registerCommands(); } @@ -36,7 +39,7 @@ export class HoverManager { // Register hover provider for Python files public registerHoverProvider(): void { - this.vscodeContext.subscriptions.push( + this.context.subscriptions.push( vscode.languages.registerHoverProvider( { scheme: 'file', language: 'python' }, { @@ -94,20 +97,22 @@ export class HoverManager { // Register commands for refactor actions public registerCommands(): void { - this.vscodeContext.subscriptions.push( + this.context.subscriptions.push( vscode.commands.registerCommand( 'extension.refactorThisSmell', async (smell: Smell) => { - const contextManager = new ContextManager(this.vscodeContext); - await refactorSelectedSmell(contextManager, smell); + await refactorSelectedSmell(this.context, this.smellsCacheManager, smell); }, ), // clicking "Refactor All Smells of this Type..." vscode.commands.registerCommand( 'extension.refactorAllSmellsOfType', async (smell: Smell) => { - const contextManager = new ContextManager(this.vscodeContext); - await refactorAllSmellsOfType(contextManager, smell.messageId); + await refactorAllSmellsOfType( + this.context, + this.smellsCacheManager, + smell.messageId, + ); }, ), ); diff --git a/src/ui/lineSelectionManager.ts b/src/ui/lineSelectionManager.ts index 29340ab..a34ec0b 100644 --- a/src/ui/lineSelectionManager.ts +++ b/src/ui/lineSelectionManager.ts @@ -1,16 +1,10 @@ import * as vscode from 'vscode'; -import { ContextManager } from '../context/contextManager'; -import { envConfig } from '../utils/envConfig'; -import { SmellDetectRecord } from '../commands/detectSmells'; -import { hashContent } from '../utils/hashDocs'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; export class LineSelectionManager { - private contextManager; private decoration: vscode.TextEditorDecorationType | null = null; - public constructor(contextManager: ContextManager) { - this.contextManager = contextManager; - } + public constructor(private smellsCacheManager: SmellsCacheManager) {} public removeLastComment(): void { if (this.decoration) { @@ -27,15 +21,9 @@ export class LineSelectionManager { } const filePath = editor.document.fileName; - const smellsDetectRecord = this.contextManager.getWorkspaceData( - envConfig.SMELL_MAP_KEY!, - )[filePath] as SmellDetectRecord; - - if (!smellsDetectRecord) { - return; - } + const smells = this.smellsCacheManager.getCachedSmells(filePath); - if (smellsDetectRecord.hash !== hashContent(editor.document.getText())) { + if (!smells || smells.length === 0) { return; } @@ -48,8 +36,6 @@ export class LineSelectionManager { const selectedLine = selection.start.line; console.log(`selection: ${selectedLine}`); - const smells = smellsDetectRecord.smells; - const smellsAtLine = smells.filter((smell) => { return smell.occurences[0].line === selectedLine + 1; }); @@ -59,7 +45,6 @@ export class LineSelectionManager { } let comment; - if (smellsAtLine.length > 1) { comment = `🍂 Smell: ${smellsAtLine[0].symbol} | (+${ smellsAtLine.length - 1 diff --git a/src/ui/refactorView.ts b/src/ui/refactorView.ts deleted file mode 100644 index ebd9709..0000000 --- a/src/ui/refactorView.ts +++ /dev/null @@ -1,198 +0,0 @@ -import * as vscode from 'vscode'; -import path from 'path'; -import * as fs from 'fs'; - -import { envConfig } from '../utils/envConfig'; -import { readFileSync } from 'fs'; -import { sidebarState } from '../utils/handleEditorChange'; -import { MultiRefactoredData } from '../commands/refactorSmell'; - -export class RefactorSidebarProvider implements vscode.WebviewViewProvider { - public static readonly viewType = 'extension.refactorSidebar'; - private _view?: vscode.WebviewView; - private _file_map: Map = new Map(); - - constructor(private readonly _context: vscode.ExtensionContext) {} - - resolveWebviewView( - webviewView: vscode.WebviewView, - // eslint-disable-next-line unused-imports/no-unused-vars - context: vscode.WebviewViewResolveContext, - _token: vscode.CancellationToken, - ): void { - this._view = webviewView; - const webview = webviewView.webview; - - webview.options = { - enableScripts: true, - }; - - webview.html = this._getHtml(webview); - - webviewView.onDidChangeVisibility(async () => { - console.log('Webview is visible'); - if (webviewView.visible) { - // Use acquireVsCodeApi to get the webview state - const savedState = this._context.workspaceState.get( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - ); - - if (savedState) { - this.updateView(); - return; - } - } - }); - - webviewView.onDidDispose(() => { - console.log('Webview Disposed'); - }); - - webviewView.webview.onDidReceiveMessage(async (message) => { - switch (message.command) { - case 'selectFile': - sidebarState.isOpening = true; - console.log('Switching diff file view.'); - await vscode.commands.executeCommand( - 'vscode.diff', - vscode.Uri.file(message.original), - vscode.Uri.file(message.refactored), - 'Refactoring Comparison', - ); - sidebarState.isOpening = false; - break; - case 'accept': - await this.applyRefactoring(); - await this.closeViews(); - break; - case 'reject': - await this.closeViews(); - break; - } - }); - console.log('Initialized sidebar view'); - } - - async updateView(): Promise { - console.log('Updating view'); - const refactoredData = this._context.workspaceState.get( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - )!; - - this._file_map.set( - vscode.Uri.file(refactoredData.targetFile.original), - vscode.Uri.file(refactoredData.targetFile.refactored), - ); - - refactoredData.affectedFiles.forEach(({ original, refactored }) => { - this._file_map!.set(vscode.Uri.file(original), vscode.Uri.file(refactored)); - }); - - if (this._view) { - this.openView(refactoredData); - } - } - - private async openView(refactoredData: RefactoredData): Promise { - const diffView = this._context.workspaceState.get( - envConfig.ACTIVE_DIFF_KEY!, - )!; - - if (diffView.isOpen) { - console.log('starting view'); - this._view!.show(true); - this._view!.webview.postMessage({ - command: 'update', - data: refactoredData, - sep: path.sep, - }); - } else { - console.log('Gonna pause'); - this.pauseView(); - } - } - - async pauseView(): Promise { - console.log('pausing view'); - this._view!.webview.postMessage({ command: 'pause' }); - } - - async clearView(): Promise { - await this._view?.webview.postMessage({ command: 'clear' }); - this._file_map = new Map(); - - console.log('View cleared'); - } - - private _getHtml(webview: vscode.Webview): string { - const scriptUri = webview.asWebviewUri( - vscode.Uri.file(path.join(this._context.extensionPath, 'media', 'script.js')), - ); - const customCssUri = webview.asWebviewUri( - vscode.Uri.file(path.join(this._context.extensionPath, 'media', 'style.css')), - ); - const vscodeCssUri = webview.asWebviewUri( - vscode.Uri.file(path.join(this._context.extensionPath, 'media', 'vscode.css')), - ); - const htmlPath = path.join(this._context.extensionPath, 'media', 'webview.html'); - let htmlContent = readFileSync(htmlPath, 'utf8'); - - // Inject the script URI dynamically - htmlContent = htmlContent.replace('${vscodeCssUri}', vscodeCssUri.toString()); - htmlContent = htmlContent.replace('${customCssUri}', customCssUri.toString()); - htmlContent = htmlContent.replace('${scriptUri}', scriptUri.toString()); - - return htmlContent; - } - - private async closeViews(): Promise { - await this.clearView(); - try { - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - await vscode.commands.executeCommand('workbench.view.explorer'); - - await this._context.workspaceState.update( - envConfig.ACTIVE_DIFF_KEY!, - undefined, - ); - - const tempDirs = - this._context.workspaceState.get( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - )?.tempDir || - this._context.workspaceState.get( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - )?.tempDirs; - - if (Array.isArray(tempDirs)) { - for (const dir in tempDirs) { - await fs.promises.rm(dir, { recursive: true, force: true }); - } - } else if (tempDirs) { - await fs.promises.rm(tempDirs, { recursive: true, force: true }); - } - } catch (err) { - console.error('Error closing views', err); - } - - console.log('Closed views'); - - await this._context.workspaceState.update( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - undefined, - ); - } - - private async applyRefactoring(): Promise { - try { - for (const [original, refactored] of this._file_map.entries()) { - const content = await vscode.workspace.fs.readFile(refactored); - await vscode.workspace.fs.writeFile(original, content); - console.log(`Applied refactoring to ${original.fsPath}`); - } - vscode.window.showInformationMessage('Refactoring applied successfully!'); - } catch (error) { - console.error('Error applying refactoring:', error); - } - } -} diff --git a/src/utils/envConfig.ts b/src/utils/envConfig.ts index 60bd31d..d55ccdc 100644 --- a/src/utils/envConfig.ts +++ b/src/utils/envConfig.ts @@ -8,8 +8,10 @@ export interface EnvConfig { FILE_CHANGES_KEY?: string; LAST_USED_SMELLS_KEY?: string; CURRENT_REFACTOR_DATA_KEY?: string; + SMELL_CACHE_KEY?: string; + FILE_HASH_CACHE_KEY?: string; ACTIVE_DIFF_KEY?: string; - SMELL_LINTING_ENABLED_KEY: string; + SMELL_LINTING_ENABLED_KEY?: string; } export const envConfig: EnvConfig = { @@ -19,6 +21,5 @@ export const envConfig: EnvConfig = { LAST_USED_SMELLS_KEY: process.env.LAST_USED_SMELLS_KEY, CURRENT_REFACTOR_DATA_KEY: process.env.CURRENT_REFACTOR_DATA_KEY, ACTIVE_DIFF_KEY: process.env.ACTIVE_DIFF_KEY, - SMELL_LINTING_ENABLED_KEY: - process.env.SMELL_LINTING_ENABLED_KEY || 'eco.smellLintingEnabled', + SMELL_LINTING_ENABLED_KEY: process.env.SMELL_LINTING_ENABLED_KEY, }; diff --git a/src/utils/handleEditorChange.ts b/src/utils/handleEditorChange.ts deleted file mode 100644 index 123745c..0000000 --- a/src/utils/handleEditorChange.ts +++ /dev/null @@ -1,126 +0,0 @@ -import * as vscode from 'vscode'; -import { setTimeout } from 'timers/promises'; - -import { envConfig } from './envConfig'; -import { ContextManager } from '../context/contextManager'; - -interface DiffInfo { - original: vscode.Uri; - modified: vscode.Uri; -} - -export let sidebarState = { isOpening: false }; - -export async function handleEditorChanges( - contextManager: ContextManager, - editors: readonly vscode.TextEditor[], -): Promise { - console.log('Detected visible editor change'); - const diffState = contextManager.getWorkspaceData( - envConfig.ACTIVE_DIFF_KEY!, - ); - const refactorData = contextManager.getWorkspaceData( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - ); - - if (sidebarState.isOpening) { - return; - } - - if (!diffState) { - console.log('No active refactoring session'); - return; - } - - // console.log(`diffstate: ${diffState.isOpen}`); - // console.log(`diffstate: ${JSON.stringify(diffState)}`); - // console.log(`Editors: ${JSON.stringify(editors)}`); - - // Is a diff editor for a refactoring - const isDiffRefactorEditor = isDiffEditorOpen(editors, diffState); - - if (diffState.isOpen) { - // User either closed or switched diff editor - // console.log(`refactor data: ${JSON.stringify(refactorData)}`); - // console.log(`is diff editor: ${isDiffRefactorEditor}`); - - if (isDiffRefactorEditor === undefined) { - return; - } - - if ((!isDiffRefactorEditor || !refactorData) && !diffState.firstOpen) { - console.log('Diff editor no longer active'); - diffState.isOpen = false; - // console.log(`diffstate: ${diffState.isOpen}`); - // console.log(`diffstate: ${JSON.stringify(diffState)}`); - contextManager.setWorkspaceData(envConfig.ACTIVE_DIFF_KEY!, diffState); - await setTimeout(500); - // vscode.commands.executeCommand( - // 'ecooptimizer.pauseRefactorSidebar' - // ); - return; - } - if (diffState.firstOpen) { - diffState.firstOpen = false; - contextManager.setWorkspaceData(envConfig.ACTIVE_DIFF_KEY!, diffState); - await setTimeout(500); - } - // switched from one diff editor to another, no handling needed - console.log('continuing'); - return; - } - - // Diff editor was reopened (switch back to) - else if (isDiffRefactorEditor) { - console.log('Opening Sidebar'); - // console.log(`diffstate: ${diffState.isOpen}`); - diffState.isOpen = true; - // console.log(`diffstate: ${JSON.stringify(diffState)}`); - contextManager.setWorkspaceData(envConfig.ACTIVE_DIFF_KEY!, diffState); - await setTimeout(500); - vscode.commands.executeCommand('ecooptimizer.showRefactorSidebar'); - } - console.log('Doing nothing'); -} - -function isDiffEditorOpen( - editors: readonly vscode.TextEditor[], - diffState: ActiveDiff, -): boolean | undefined { - console.log('Checking if editor is a diff editor'); - if (!editors.length) { - console.log('No editors found'); - return undefined; - } - - // @ts-ignore - const diffInfo: DiffInfo[] = editors[0].diffInformation; - // console.log(`Diff Info: ${JSON.stringify(diffInfo)}`); - - if (!diffInfo && editors.length === 2) { - console.log('Checking first case'); - - return diffState.files.some((file) => { - // console.log(`file: ${JSON.stringify(file)}`); - return ( - (file.original === editors[0].document.uri.toString() && - file.refactored === editors[1].document.uri.toString()) || - (file.refactored === editors[0].document.uri.toString() && - file.original === editors[1].document.uri.toString()) - ); - }); - } else if (diffInfo && diffInfo.length === 1) { - console.log('Checking second case'); - return diffState.files.some((file) => { - // console.log(`file: ${JSON.stringify(file)}`); - return ( - (file.original === diffInfo[0].original.toString() && - file.refactored === diffInfo[0].modified.toString()) || - (file.original === diffInfo[0].modified.toString() && - file.refactored === diffInfo[0].original.toString()) - ); - }); - } - - return false; -} diff --git a/src/utils/handleSmellSettings.ts b/src/utils/handleSmellSettings.ts deleted file mode 100644 index 2fad66b..0000000 --- a/src/utils/handleSmellSettings.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as vscode from 'vscode'; -import { wipeWorkCache } from '../commands/wipeWorkCache'; -import { ContextManager } from '../context/contextManager'; - -/** - * Fetches the current enabled smells from VS Code settings. - */ -export function getEnabledSmells(): { - [key: string]: boolean; -} { - const smellConfig = vscode.workspace - .getConfiguration('detection') - .get('smells', {}) as { [key: string]: { enabled: boolean; colour: string } }; - - return Object.fromEntries( - Object.entries(smellConfig).map(([smell, config]) => [smell, config.enabled]), - ); -} - -/** - * Handles when a user updates the smell filter in settings. - * It wipes the cache and notifies the user about changes. - */ -export function handleSmellFilterUpdate( - previousSmells: { [key: string]: boolean }, - contextManager: ContextManager, -): void { - const currentSmells = getEnabledSmells(); - let smellsChanged = false; - - Object.entries(currentSmells).forEach(([smell, isEnabled]) => { - if (previousSmells[smell] !== isEnabled) { - smellsChanged = true; - vscode.window.showInformationMessage( - isEnabled - ? `Eco: Enabled detection of ${formatSmellName(smell)}.` - : `Eco: Disabled detection of ${formatSmellName(smell)}.`, - ); - } - }); - - if (smellsChanged) { - console.log('Eco: Smell preferences changed! Wiping cache.'); - wipeWorkCache(contextManager, 'settings'); - } -} - -/** - * Formats the smell name from kebab-case to a readable format. - */ -export function formatSmellName(smellKey: string): string { - return smellKey.replace(/-/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()); -} diff --git a/src/utils/hashDocs.ts b/src/utils/hashDocs.ts deleted file mode 100644 index 3baa19c..0000000 --- a/src/utils/hashDocs.ts +++ /dev/null @@ -1,31 +0,0 @@ -import crypto from 'crypto'; -import { ContextManager } from '../context/contextManager'; -import { envConfig } from './envConfig'; -import * as vscode from 'vscode'; - -// Function to hash the document content -export function hashContent(content: string): string { - return crypto.createHash('sha256').update(content).digest('hex'); -} - -// Function to update the stored hashes in workspace storage -export async function updateHash( - contextManager: ContextManager, - document: vscode.TextDocument, -): Promise { - const lastSavedHashes = contextManager.getWorkspaceData( - envConfig.FILE_CHANGES_KEY!, - {}, - ); - const lastHash = lastSavedHashes[document.fileName]; - const currentHash = hashContent(document.getText()); - - if (!lastHash || lastHash !== currentHash) { - console.log(`Document ${document.fileName} has changed since last save.`); - lastSavedHashes[document.fileName] = currentHash; - await contextManager.setWorkspaceData( - envConfig.FILE_CHANGES_KEY!, - lastSavedHashes, - ); - } -} diff --git a/src/utils/smellDetails.ts b/src/utils/smellDetails.ts deleted file mode 100644 index bcc2b8c..0000000 --- a/src/utils/smellDetails.ts +++ /dev/null @@ -1,74 +0,0 @@ -export const SMELL_MAP: Map = new Map([ - [ - 'R1729', - { - symbol: 'use-a-generator', - message: - 'Refactor to use a generator expression instead of a list comprehension inside `any()` or `all()`. This improves memory efficiency by avoiding the creation of an intermediate list.', - }, - ], - [ - 'R0913', - { - symbol: 'too-many-arguments', - message: - 'Refactor the function to reduce the number of parameters. Functions with too many arguments can become difficult to maintain and understand. Consider breaking it into smaller, more manageable functions.', - }, - ], - [ - 'R6301', - { - symbol: 'no-self-use', - message: - "Refactor the method to make it static, as it does not use `self`. Static methods do not require an instance and improve clarity and performance when the method doesn't depend on instance data.", - }, - ], - [ - 'LLE001', - { - symbol: 'long-lambda-expression', - message: - 'Refactor the lambda expression to improve readability. Long lambda expressions can be confusing; breaking them into named functions can make the code more understandable and maintainable.', - }, - ], - [ - 'LMC001', - { - symbol: 'long-message-chain', - message: - 'Refactor the message chain to improve readability and performance. Long chains of method calls can be hard to follow and may impact performance. Consider breaking them into smaller steps.', - }, - ], - [ - 'UVA001', - { - symbol: 'unused-variables-and-attributes', - message: - 'Remove unused variables or attributes to clean up the code. Keeping unused elements in the code increases its complexity without providing any benefit, making it harder to maintain.', - }, - ], - [ - 'LEC001', - { - symbol: 'long-element-chain', - message: - 'Refactor the long element chain for better performance and clarity. Chains of nested elements are harder to read and can lead to inefficiency, especially when accessing deep levels repeatedly.', - }, - ], - [ - 'CRC001', - { - symbol: 'cached-repeated-calls', - message: - 'Refactor by caching repeated function calls to improve performance. Repeated calls to the same function can be avoided by storing the result, which saves processing time and enhances performance.', - }, - ], - [ - 'SCL001', - { - symbol: 'string-concat-loop', - message: - 'Refactor to use list accumulation instead of string concatenation inside a loop. Concatenating strings in a loop is inefficient; list accumulation and joining are faster and use less memory.', - }, - ], -]); diff --git a/src/utils/smellsData.ts b/src/utils/smellsData.ts new file mode 100644 index 0000000..14b6e72 --- /dev/null +++ b/src/utils/smellsData.ts @@ -0,0 +1,97 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Defines the structure of the smell configuration in smells.json. + * Used by FilterSmellsProvider.ts (modifies JSON based on user input). + */ +export interface FilterSmellConfig { + name: string; + message_id: string; + acronym: string; + enabled: boolean; + analyzer_options?: Record< + string, + { label: string; description: string; value: number | string } + >; +} + +/** + * Defines the structure of enabled smells sent to the backend. + */ +interface DetectSmellConfig { + message_id: string; + acronym: string; + options: Record; +} + +/** + * Loads the full smells configuration from smells.json. + * @returns A dictionary of smells with their respective configuration. + */ +export function loadSmells(): Record { + const filePath = path.join(__dirname, '..', 'data', 'smells.json'); + + if (!fs.existsSync(filePath)) { + vscode.window.showErrorMessage( + 'Configuration file missing: smells.json could not be found.', + ); + return {}; + } + + try { + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } catch (error) { + vscode.window.showErrorMessage( + 'Error loading smells.json. Please check the file format.', + ); + console.error('ERROR: Failed to parse smells.json', error); + return {}; + } +} + +/** + * Saves the smells configuration to smells.json. + * @param smells - The smells data to be saved. + */ +export function saveSmells(smells: Record): void { + const filePath = path.join(__dirname, '..', 'data', 'smells.json'); + try { + fs.writeFileSync(filePath, JSON.stringify(smells, null, 2)); + } catch (error) { + vscode.window.showErrorMessage('Error saving smells.json.'); + console.error('ERROR: Failed to write smells.json', error); + } +} + +/** + * Extracts enabled smells from the loaded configuration. + * @returns A dictionary of enabled smells formatted for backend processing. + */ +export function getEnabledSmells(): Record { + const smells = loadSmells(); + + return Object.fromEntries( + Object.entries(smells) + .filter(([, smell]) => smell.enabled) + .map(([smellKey, smellData]) => [ + smellKey, + { + message_id: smellData.message_id, + acronym: smellData.acronym, + options: Object.fromEntries( + Object.entries(smellData.analyzer_options ?? {}).map( + ([optionKey, optionData]) => [ + optionKey, + typeof optionData.value === 'string' || + typeof optionData.value === 'number' + ? optionData.value + : String(optionData.value), + ], + ), + ), + }, + ]), + ); +} From d9fc39ea15fdf8f0807869e4448ff9557b151b61 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 00:41:34 -0400 Subject: [PATCH 002/121] Fixed package-lock.json --- package-lock.json | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0688744..1c722b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -239,13 +239,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" @@ -530,10 +531,11 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" From f0f3d92a108bc86616bc1cfd5d7842cd43853b04 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 01:49:45 -0400 Subject: [PATCH 003/121] temporarily moved out old logic + files (will slowly add back in) --- .env | 6 - package.json | 130 +--------- src/api/backend.ts | 89 ------- src/commands/detectSmells.ts | 14 -- src/commands/refactorSmell.ts | 336 ++----------------------- src/commands/showLogs.ts | 190 -------------- src/extension.ts | 151 ----------- src/global.d.ts | 29 --- src/listeners/fileSaveListener.ts | 11 - src/managers/SmellsViewStateManager.ts | 1 - src/types.d.ts | 57 ----- src/ui/fileHighlighter.ts | 137 ---------- src/ui/hoverManager.ts | 120 --------- src/ui/lineSelectionManager.ts | 79 ------ src/utils/configManager.ts | 68 ----- src/utils/editorUtils.ts | 22 -- src/utils/envConfig.ts | 14 +- 17 files changed, 21 insertions(+), 1433 deletions(-) delete mode 100644 src/commands/showLogs.ts delete mode 100644 src/types.d.ts delete mode 100644 src/ui/fileHighlighter.ts delete mode 100644 src/ui/hoverManager.ts delete mode 100644 src/ui/lineSelectionManager.ts delete mode 100644 src/utils/configManager.ts delete mode 100644 src/utils/editorUtils.ts diff --git a/.env b/.env index dfd4ddf..f668a4c 100644 --- a/.env +++ b/.env @@ -1,9 +1,3 @@ SERVER_URL='127.0.0.1:8000' -SMELL_MAP_KEY='workspaceSmells' -FILE_CHANGES_KEY='lastSavedHashes' -LAST_USED_SMELLS_KEY='lastUsedSmells' -CURRENT_REFACTOR_DATA_KEY='refactorData' -ACTIVE_DIFF_KEY='activeDiff' FILE_HASH_CACHE_KEY='fileHashCache' SMELL_CACHE_KEY='smellCache' -SMELL_LINTING_ENABLED_KEY='smellLintingEnabled' diff --git a/package.json b/package.json index d96c02f..56110de 100644 --- a/package.json +++ b/package.json @@ -71,11 +71,7 @@ "!src/**/index.ts", "!test/mocks/*", "!src/extension.ts", - "!src/context/*", - "!src/utils/configManager.ts", - "!src/commands/showLogs.ts", - "!src/ui/refactorView.ts", - "!src/utils/handleEditorChange.ts" + "!src/context/*" ] }, "lint-staged": { @@ -220,17 +216,6 @@ "command": "ecooptimizer.wipeWorkCache", "title": "Clear Smells Cache", "category": "Eco Optimizer" - }, - { - "command": "ecooptimizer.startLogging", - "title": "Show Backend Logs", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.toggleSmellLinting", - "title": "🍃 Toggle Smell Linting", - "category": "Eco" } ], "menus": { @@ -277,120 +262,7 @@ "when": "viewItem == ecoOptimizerFile", "group": "inline" } - ], - "editor/title": [ - { - "command": "ecooptimizer.toggleSmellLinting", - "group": "navigation", - "when": "eco.smellLintingEnabled == false", - "icon": { - "light": "off.svg", - "dark": "off.svg" - } - }, - { - "command": "ecooptimizer.toggleSmellLinting", - "group": "navigation", - "when": "eco.smellLintingEnabled == true", - "icon": { - "light": "on.svg", - "dark": "on.svg" - } - } ] - }, - "configuration": { - "title": "EcoOptimizer", - "properties": { - "ecooptimizer.projectWorkspacePath": { - "type": "string", - "default": "", - "description": "Path to the folder to be targeted, relative to current workspace. Defaults to the currently open folder in VS Code." - }, - "ecooptimizer.logsOutputPath": { - "type": "string", - "default": "", - "description": "Path to store log files and output reports. Defaults to a 'logs' folder inside the workspace." - }, - "detection.smellColours": { - "order": 1, - "type": "object", - "additionalProperties": false, - "description": "Configure the highlight colours of each smell (css syntax).", - "default": { - "long-element-chain": "lightblue", - "too-many-arguments": "lightcoral", - "long-lambda-expression": "mediumpurple", - "long-message-chain": "lightpink", - "cached-repeated-calls": "lightgreen", - "string-concat-loop": "lightsalmon", - "no-self-use": "lightcyan", - "use-a-generator": "yellow" - }, - "properties": { - "long-element-chain": { - "type": "string", - "default": "lightblue" - }, - "too-many-arguments": { - "type": "string", - "default": "lightcoral" - }, - "long-lambda-expression": { - "type": "string", - "default": "mediumpurple" - }, - "long-message-chain": { - "type": "string", - "default": "lightpink" - }, - "cached-repeated-calls": { - "type": "string", - "default": "lightgreen" - }, - "string-concat-loop": { - "type": "string", - "default": "lightsalmon" - }, - "no-self-use": { - "type": "string", - "default": "lightcyan" - }, - "use-a-generator": { - "type": "string", - "default": "yellow" - } - } - }, - "ecooptimizer.detection.useSingleColour": { - "order": 2, - "type": "boolean", - "default": false, - "description": "Use a single colour for all smells. If enabled, the colour defined below will be used." - }, - "ecooptimizer.detection.singleHighlightColour": { - "order": 3, - "type": "string", - "default": "yellow", - "markdownDescription": "Colour (css syntax) to use for all smells if **Use Single Colour** is enabled." - }, - "ecooptimizer.detection.highlightStyle": { - "order": 0, - "type": "string", - "enum": [ - "underline", - "flashlight", - "border-arrow" - ], - "markdownEnumDescriptions": [ - "Your average wavy line", - "No pixel left untouched", - "Basically how it sounds" - ], - "default": "underline", - "description": "Choose a highlight style for all smells." - } - } } } } diff --git a/src/api/backend.ts b/src/api/backend.ts index 49aefc9..544ddea 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -1,4 +1,3 @@ -import * as vscode from 'vscode'; import { envConfig } from '../utils/envConfig'; import { serverStatus } from '../utils/serverStatus'; @@ -19,39 +18,6 @@ export async function checkServerStatus(): Promise { } } -/** - * Initializes and synchronizes logs with the backend. - * - * @param {string} log_dir - The directory where logs are stored. - * @returns {Promise} - Returns `true` if the logs are successfully initialized and synchronized, otherwise throws an error. - * @throws {Error} - Throws an error if the initialization fails due to network issues or backend errors. - */ -export async function initLogs(log_dir: string): Promise { - const url = `${BASE_URL}/logs/init`; - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ log_dir }), - }); - - if (!response.ok) { - throw new Error(`Unable to initialize logging: ${response.statusText}`); - } - - return true; - } catch (error: any) { - if (error instanceof Error) { - throw new Error(`Eco: Unable to initialize logging: ${error.message}`); - } else { - throw new Error('Eco: An unexpected error occurred while initializing logs.'); - } - } -} - /** * Sends a request to the backend to detect code smells in the specified file. * @@ -95,59 +61,4 @@ export async function fetchSmells( `Failed to connect to the backend: ${error.message}. Please check your network and try again.` ); } -} - - -/** - * Refactors a specific code smell in a given file. - * - * @param {string} filePath - The path to the file containing the code smell. - * @param {Smell} smell - The code smell to refactor. - * @returns {Promise} - The result of the refactoring operation. - * @throws {Error} - Throws an error if the workspace folder cannot be determined, the API request fails, or an unexpected error occurs. - */ -export async function refactorSmell( - filePath: string, - smell: Smell, -): Promise { - const url = `${BASE_URL}/refactor`; - - const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => - filePath.includes(folder.uri.fsPath), - ); - - if (!workspaceFolder) { - throw new Error(`Unable to determine workspace folder for file: ${filePath}`); - } - - const workspaceFolderPath = workspaceFolder.uri.fsPath; - - const payload = { - source_dir: workspaceFolderPath, - smell, - }; - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Refactoring failed for smell "${smell.symbol}": ${errorText}`); - } - - const refactorResult = (await response.json()) as RefactorOutput; - return refactorResult; - } catch (error) { - if (error instanceof Error) { - throw new Error(`Unexpected error during refactoring: ${error.message}`); - } else { - throw new Error('An unexpected error occurred during refactoring.'); - } - } } \ No newline at end of file diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 5681670..3646d7a 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -30,7 +30,6 @@ export async function detectSmellsFile( const filePath = typeof fileUri === 'string' ? fileUri : fileUri.fsPath; // Handle outdated files before proceeding - console.log('Handling outdated file:', filePath); await handleOutdatedFile(filePath, smellsCacheManager, treeDataProvider); // Open the file and compute its hash @@ -38,11 +37,9 @@ export async function detectSmellsFile( const fileContent = document.getText(); // Store the file hash after analyzing - console.log('Storing file hash for:', filePath); await smellsCacheManager.storeFileHash(filePath, fileContent); // Retrieve enabled smells from configuration - console.log('Retrieving enabled smells...'); const enabledSmells = getEnabledSmells(); // Ensure that at least one smell type is enabled @@ -54,7 +51,6 @@ export async function detectSmellsFile( } // Check if smells are already cached - console.log('Checking for cached smells...'); const cachedSmells = smellsCacheManager.getCachedSmells(filePath); if (cachedSmells !== undefined) { // Use cached smells if available @@ -63,13 +59,11 @@ export async function detectSmellsFile( ); if (cachedSmells.length > 0) { - console.log('Updating UI with cached smells...'); treeDataProvider.updateSmells(filePath, cachedSmells, enabledSmells); } else { treeDataProvider.updateStatus(filePath, 'no_issues'); } - console.log('Analysis complete: Using cached smells.'); return; } @@ -86,25 +80,21 @@ export async function detectSmellsFile( try { // Prepare enabled smells for backend request - console.log('Preparing enabled smells for backend...'); const enabledSmellsForBackend = Object.fromEntries( Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), ); // Request smell analysis from the backend - console.log('Requesting smell analysis from the backend...'); const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); // Handle response and update UI if (status === 200) { // Cache detected smells, even if no smells are found - console.log('Caching detected smells...'); await smellsCacheManager.setCachedSmells(filePath, smells); // Remove the file from modifiedFiles after re-analysis treeDataProvider.clearOutdatedStatus(filePath); - console.log('Updating UI with detected smells...'); if (smells.length > 0) { treeDataProvider.updateSmells(filePath, smells, enabledSmells); vscode.window.showInformationMessage( @@ -118,8 +108,6 @@ export async function detectSmellsFile( `Analysis complete: No code smells found in ${path.basename(filePath)}.`, ); } - - console.log('Analysis complete: Detected smells.'); } else { throw new Error(`Unexpected status code: ${status}`); } @@ -143,7 +131,6 @@ export async function detectSmellsFolder( treeDataProvider: SmellsDisplayProvider, folderPath: string, ) { - console.log('Detecting smells for all Python files in:', folderPath); // Notify the user that folder analysis has started vscode.window.showInformationMessage( `Detecting code smells for all Python files in: ${path.basename(folderPath)}`, @@ -176,7 +163,6 @@ export async function detectSmellsFolder( // Analyze each Python file in the folder for (const file of pythonFiles) { - console.log('Analyzing:', file); await detectSmellsFile(smellsCacheManager, treeDataProvider, file); } diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index ffd323c..7919b7a 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,326 +1,26 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; - -import { envConfig } from '../utils/envConfig'; - -import { getEditorAndFilePath } from '../utils/editorUtils'; -import { refactorSmell } from '../api/backend'; - -import { FileHighlighter } from '../ui/fileHighlighter'; -import { serverStatus } from '../utils/serverStatus'; -import { ServerStatusType } from '../utils/serverStatus'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; - -/* istanbul ignore next */ -serverStatus.on('change', (newStatus: ServerStatusType) => { - console.log('Server status changed:', newStatus); - if (newStatus === ServerStatusType.DOWN) { - vscode.window.showWarningMessage('No refactoring is possible at this time.'); - } -}); - -export interface MultiRefactoredData { - tempDirs: string[]; - targetFile: ChangedFile; - affectedFiles: ChangedFile[]; - energySaved: number; -} - -async function refactorLine( - smell: Smell, - filePath: string, -): Promise { - try { - const refactorResult = await refactorSmell(filePath, smell); - return refactorResult; - } catch (error) { - console.error('Error refactoring smell:', error); - vscode.window.showErrorMessage((error as Error).message); - return; - } -} - -export async function refactorSelectedSmell( - context: vscode.ExtensionContext, - smellsCacheManager: SmellsCacheManager, - smellGiven?: Smell, -): Promise { - const { editor, filePath } = getEditorAndFilePath(); - - const pastData = context.workspaceState.get( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - ); - - // Clean up temp directory if not removed - if (pastData) { - console.log('cleaning up temps'); - cleanTemps(pastData); - } - - if (!editor || !filePath) { - vscode.window.showErrorMessage( - 'Eco: Unable to proceed as no active editor or file path found.', - ); - return; - } - - const selectedLine = editor.selection.start.line + 1; // Update to VS Code editor indexing - - const smellsData = smellsCacheManager.getCachedSmells(filePath); - - if (!smellsData || smellsData.length === 0) { - vscode.window.showErrorMessage( - 'Eco: No smells detected in the file for refactoring.', - ); - return; - } - - // Find the smell to refactor - let smellToRefactor: Smell | undefined; - if (smellGiven?.messageId) { - smellToRefactor = smellsData.find( - (smell: Smell) => - smell.messageId === smellGiven.messageId && - smellGiven.occurences[0].line === smell.occurences[0].line, - ); - } else { - smellToRefactor = smellsData.find( - (smell: Smell) => selectedLine === smell.occurences[0].line, - ); - } - - if (!smellToRefactor) { - vscode.window.showErrorMessage('Eco: No matching smell found for refactoring.'); +import * as path from 'path'; +import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; + +// 📌 Refactor Code Smells for a File +export async function refactorSmellsByType( + treeDataProvider: SmellsDisplayProvider, + fileUri: vscode.Uri | string, +) { + if (!fileUri) { + vscode.window.showErrorMessage('Error: No file selected for refactoring.'); return; } - await vscode.workspace.save(editor.document.uri); - - const refactorResult = await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: `Fetching refactoring for ${smellToRefactor.symbol} on line ${smellToRefactor.occurences[0].line}`, - }, - async (_progress, _token) => { - const result = await refactorLine(smellToRefactor, filePath); - - if (result && result.refactoredData) { - vscode.window.showInformationMessage( - 'Refactoring report available in sidebar.', - ); - } - - return result; - }, - ); - - if (!refactorResult || !refactorResult.refactoredData) { - vscode.window.showErrorMessage( - 'Eco: Refactoring failed. See console for details.', - ); - return; - } - - const { refactoredData } = refactorResult; - - await startRefactoringSession(context, editor, refactoredData); - - if (refactorResult.updatedSmells.length) { - const fileHighlighter = FileHighlighter.getInstance(context, smellsCacheManager); - fileHighlighter.highlightSmells(editor, refactorResult.updatedSmells); - } else { - vscode.window.showWarningMessage( - 'Eco: No updated smells detected after refactoring.', - ); - } -} - -export async function refactorAllSmellsOfType( - // eslint-disable-next-line unused-imports/no-unused-vars - context: vscode.ExtensionContext, - // eslint-disable-next-line unused-imports/no-unused-vars - smellsCacheManager: SmellsCacheManager, - // eslint-disable-next-line unused-imports/no-unused-vars - smellId: string, -): Promise { - // const { editor, filePath } = getEditorAndFilePath(); - // const pastData = contextManager.getWorkspaceData( - // envConfig.CURRENT_REFACTOR_DATA_KEY!, - // ); - // // Clean up temp directory if not removed - // if (pastData) { - // cleanTemps(pastData); - // } - // if (!editor) { - // vscode.window.showErrorMessage( - // 'Eco: Unable to proceed as no active editor found.', - // ); - // console.log('No active editor found to refactor smell. Returning back.'); - // return; - // } - // if (!filePath) { - // vscode.window.showErrorMessage( - // 'Eco: Unable to proceed as active editor does not have a valid file path.', - // ); - // console.log('No valid file path found to refactor smell. Returning back.'); - // return; - // } - // // only account for one selection to be refactored for now - // // const selectedLine = editor.selection.start.line + 1; // update to VS code editor indexing - // const smellsData: Smell[] = contextManager.getWorkspaceData( - // envConfig.SMELL_MAP_KEY!, - // )[filePath].smells; - // if (!smellsData || smellsData.length === 0) { - // vscode.window.showErrorMessage( - // 'Eco: No smells detected in the file for refactoring.', - // ); - // console.log('No smells found in the file for refactoring.'); - // return; - // } - // // Filter smells by the given type ID - // const smellsOfType = smellsData.filter( - // (smell: Smell) => smell.messageId === smellId, - // ); - // if (smellsOfType.length === 0) { - // vscode.window.showWarningMessage( - // `Eco: No smells of type ${smellId} found in the file.`, - // ); - // return; - // } - // let combinedRefactoredData = ''; - // let totalEnergySaved = 0; - // let allUpdatedSmells: Smell[] = []; - // // Refactor each smell of the given type - // for (const smell of smellsOfType) { - // const refactorResult = await refactorLine(smell, filePath); - // if (refactorResult && refactorResult.refactoredData) { - // // Add two newlines between each refactored result - // if (combinedRefactoredData) { - // combinedRefactoredData += '\n\n'; - // } - // fs.readFile( - // refactorResult.refactoredData.targetFile.refactored, - // (err, data) => { - // if (!err) { - // combinedRefactoredData += data.toString('utf8'); - // } - // }, - // ); - // totalEnergySaved += refactorResult.refactoredData.energySaved; - // if (refactorResult.updatedSmells) { - // allUpdatedSmells = [...allUpdatedSmells, ...refactorResult.updatedSmells]; - // } - // } - // } - // /* - // Once all refactorings are merge, need to write to a file so that it has a path that - // will be the new `targetFile`. Also need to reconstruct the `RefactoredData` object - // by combining all `affectedFiles` merge to new paths if applicable. Once implemented, - // just uncomment lines below and pass in the refactoredData. - // */ - // // Tentative data structure to be built below, change inputs as needed but needs - // // to implement the `MultiRefactoredData` interface - // // For any temp files that need to be written due to merging, I'd suggest writing them all - // // to one temp directory and add that directory to allTempDirs, that way they will be removed - // // UNCOMMENT ME WHEN READY - // // const combinedRefactoredData: MultiRefactoredData = { - // // targetFile: combinedTargetFile, - // // affectedFiles: allAffectedFiles, - // // energySaved: totalEnergySaved, - // // tempDirs: allTempDirs - // // } - // // UNCOMMENT ME WHEN READY - // // startRefactoringSession(contextManager,editor,combinedRefactoredData); - // if (combinedRefactoredData) { - // // await RefactorManager.previewRefactor(editor, combinedRefactoredData); - // vscode.window.showInformationMessage( - // `Eco: Refactoring completed. Total energy difference: ${totalEnergySaved.toFixed( - // 4, - // )}`, - // ); - // } else { - // vscode.window.showErrorMessage( - // 'Eco: Refactoring failed. See console for details.', - // ); - // return; - // } - // if (allUpdatedSmells.length) { - // const fileHighlighter = FileHighlighter.getInstance(contextManager); - // fileHighlighter.highlightSmells(editor, allUpdatedSmells); - // } else { - // vscode.window.showWarningMessage( - // 'Eco: No updated smells detected after refactoring.', - // ); - // } -} - -/* istanbul ignore next */ -async function startRefactoringSession( - context: vscode.ExtensionContext, - editor: vscode.TextEditor, - refactoredData: RefactoredData | MultiRefactoredData, -): Promise { - // Store only the diff editor state - await context.workspaceState.update( - envConfig.CURRENT_REFACTOR_DATA_KEY!, - refactoredData, - ); - + const filePath = typeof fileUri === 'string' ? fileUri : fileUri.fsPath; vscode.window.showInformationMessage( - 'Hey Niv, this needs to be connected to the new refactor sidebar :)', + `Refactoring code smells in: ${path.basename(filePath)}`, ); - // await vscode.commands.executeCommand('extension.refactorSidebar.focus'); - - // //Read the refactored code - // const refactoredCode = vscode.Uri.file(refactoredData.targetFile.refactored); - - // //Get the original code from the editor - // const originalCode = editor.document.uri; - - // const allFiles: ChangedFile[] = [ - // refactoredData.targetFile, - // ...refactoredData.affectedFiles, - // ].map((file) => { - // return { - // original: vscode.Uri.file(file.original).toString(), - // refactored: vscode.Uri.file(file.refactored).toString(), - // }; - // }); - - // await contextManager.setWorkspaceData(envConfig.ACTIVE_DIFF_KEY!, { - // files: allFiles, - // firstOpen: true, - // isOpen: true, - // }); - - // await setTimeout(500); - - // const doc = await vscode.workspace.openTextDocument(originalCode); - // await vscode.window.showTextDocument(doc, { preview: false }); - - // //Show the diff viewer - // sidebarState.isOpening = true; - // vscode.commands.executeCommand( - // 'vscode.diff', - // originalCode, - // refactoredCode, - // 'Refactoring Comparison', - // ); - // vscode.commands.executeCommand('ecooptimizer.showRefactorSidebar'); - // sidebarState.isOpening = false; -} - -export async function cleanTemps(pastData: any): Promise { - console.log('Cleaning up stale artifacts'); - const tempDirs = - (pastData!.tempDir! as string) || (pastData!.tempDirs! as string[]); - - if (Array.isArray(tempDirs)) { - for (const dir in tempDirs) { - await fs.promises.rm(dir, { recursive: true, force: true }); - } - } else { - await fs.promises.rm(tempDirs, { recursive: true, force: true }); - } + // Simulate backend request + setTimeout(() => { + vscode.window.showInformationMessage( + `Code smells refactored for: ${path.basename(filePath)}`, + ); + }, 3000); } diff --git a/src/commands/showLogs.ts b/src/commands/showLogs.ts deleted file mode 100644 index 93b5481..0000000 --- a/src/commands/showLogs.ts +++ /dev/null @@ -1,190 +0,0 @@ -import * as vscode from 'vscode'; -import WebSocket from 'ws'; - -import { initLogs } from '../api/backend'; -import { envConfig } from '../utils/envConfig'; -import { serverStatus, ServerStatusType } from '../utils/serverStatus'; - -const WEBSOCKET_BASE_URL = `ws://${envConfig.SERVER_URL}/logs`; - -class LogInitializationError extends Error { - constructor(message: string) { - super(message); - this.name = 'LogInitializationError'; - } -} - -export class LogManager { - private websockets: { [key: string]: WebSocket | undefined }; - private channels: { - [key: string]: { name: string; channel: vscode.LogOutputChannel | undefined }; - }; - private channelsCreated: boolean; - private context: vscode.ExtensionContext; - - constructor(context: vscode.ExtensionContext) { - this.context = context; - this.websockets = { - main: undefined, - detect: undefined, - refactor: undefined, - }; - this.channels = { - main: { name: 'EcoOptimizer: Main', channel: undefined }, - detect: { name: 'EcoOptimizer: Detect', channel: undefined }, - refactor: { name: 'EcoOptimizer: Refactor', channel: undefined }, - }; - this.channelsCreated = false; - - // Listen for server status changes - serverStatus.on('change', async (newStatus: ServerStatusType) => { - console.log('Server status changed:', newStatus); - if (newStatus === ServerStatusType.DOWN) { - this.channels.main.channel?.appendLine('Server connection lost'); - } else { - this.channels.main.channel?.appendLine('Server connection re-established.'); - await this.startLogging(); - } - }); - } - - /** - * Starts the logging process, including initializing logs and WebSockets. - * @param retries - Number of retry attempts. - * @param delay - Initial delay between retries (in milliseconds). - */ - public async startLogging(retries = 3, delay = 1000): Promise { - let logInitialized = false; - const logPath = this.context.logUri?.fsPath; - - if (!logPath) { - throw new LogInitializationError( - 'Missing contextManager or logUri. Cannot initialize logging.', - ); - } - - for (let attempt = 1; attempt <= retries; attempt++) { - try { - if (!logInitialized) { - logInitialized = await initLogs(logPath); - - if (!logInitialized) { - throw new LogInitializationError( - `Failed to initialize logs at path: ${logPath}`, - ); - } - console.log('Log initialization successful.'); - } - - this.initializeWebSockets(); - console.log('Successfully initialized WebSockets. Logging is now active.'); - return; - } catch (error) { - const err = error as Error; - console.error(`[Attempt ${attempt}/${retries}] ${err.name}: ${err.message}`); - - if (attempt < retries) { - console.log(`Retrying in ${delay}ms...`); - await new Promise((resolve) => setTimeout(resolve, delay)); - delay *= 2; // Exponential backoff - } else { - throw new Error('Max retries reached. Logging process failed.'); - } - } - } - } - - /** - * Initializes WebSocket connections for logging. - */ - private initializeWebSockets(): void { - if (!this.channelsCreated) { - this.createOutputChannels(); - this.channelsCreated = true; - } - this.startWebSocket('main'); - this.startWebSocket('detect'); - this.startWebSocket('refactor'); - } - - /** - * Creates output channels for logging. - */ - private createOutputChannels(): void { - console.log('Creating output channels'); - for (const channel of Object.keys(this.channels)) { - this.channels[channel].channel = vscode.window.createOutputChannel( - this.channels[channel].name, - { log: true }, - ); - } - } - - /** - * Starts a WebSocket connection for a specific log type. - * @param logType - The type of log (e.g., 'main', 'detect', 'refactor'). - */ - private startWebSocket(logType: string): void { - const url = `${WEBSOCKET_BASE_URL}/${logType}`; - const ws = new WebSocket(url); - this.websockets[logType] = ws; - - ws.on('message', (data) => { - const logEvent = data.toString('utf8'); - const level = - logEvent.match(/\b(ERROR|DEBUG|INFO|WARNING|TRACE)\b/i)?.[0].trim() || - 'UNKNOWN'; - const msg = logEvent.split(`[${level}]`, 2)[1].trim(); - - console.log(logEvent); - console.log('Level:', level); - - switch (level) { - case 'ERROR': { - this.channels[logType].channel!.error(msg); - break; - } - case 'DEBUG': { - this.channels[logType].channel!.debug(msg); - break; - } - case 'WARNING': { - this.channels[logType].channel!.warn(msg); - break; - } - case 'CRITICAL': { - this.channels[logType].channel!.error(msg); - break; - } - default: { - this.channels[logType].channel!.info(msg); - break; - } - } - }); - - ws.on('error', (err) => { - this.channels[logType].channel!.error(`WebSocket error: ${err.message}`); - }); - - ws.on('close', () => { - this.channels[logType].channel!.appendLine( - `WebSocket connection closed for ${this.channels[logType].name}`, - ); - }); - - ws.on('open', () => { - this.channels[logType].channel!.appendLine( - `Connected to ${logType} via WebSocket`, - ); - }); - } - - /** - * Stops watching logs and cleans up resources. - */ - public stopWatchingLogs(): void { - Object.values(this.websockets).forEach((ws) => ws?.close()); - Object.values(this.channels).forEach((channel) => channel.channel?.dispose()); - } -} diff --git a/src/extension.ts b/src/extension.ts index 940c227..9aaddc7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,15 +15,6 @@ import { checkServerStatus } from './api/backend'; import { FilterSmellsProvider } from './providers/FilterSmellsProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; -import { serverStatus } from './utils/serverStatus'; -import { - refactorAllSmellsOfType, - refactorSelectedSmell, -} from './commands/refactorSmell'; -import { LogManager } from './commands/showLogs'; -import { LineSelectionManager } from './ui/lineSelectionManager'; - -let logManager: LogManager; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -32,9 +23,6 @@ let logManager: LogManager; export function activate(context: vscode.ExtensionContext): void { console.log('Activating Eco-Optimizer extension...'); - // Iniiialize the log manager - logManager = new LogManager(context); - // Initialize the SmellsCacheManager for managing caching of smells and file hashes. const smellsCacheManager = new SmellsCacheManager(context); @@ -104,44 +92,6 @@ export function activate(context: vscode.ExtensionContext): void { ), ); - // Refactor Selected Smell Command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.refactorSmell', () => { - if (serverStatus.getStatus() === 'up') { - console.log('Eco: Refactor Selected Smell Command Triggered'); - refactorSelectedSmell(context, smellsCacheManager); - } else { - vscode.window.showWarningMessage('Action blocked: Server is down.'); - } - }), - ); - - // Refactor All Smells of Type Command - context.subscriptions.push( - vscode.commands.registerCommand( - 'ecooptimizer.refactorAllSmellsOfType', - async (smellId: string) => { - if (serverStatus.getStatus() === 'up') { - console.log( - `Eco: Refactor All Smells of Type Command Triggered for ${smellId}`, - ); - refactorAllSmellsOfType(context, smellsCacheManager, smellId); - } else { - vscode.window.showWarningMessage('Action blocked: Server is down.'); - } - }, - ), - ); - - // Register the "Toggle Smell Auto Lint" command. - // TODO: Uncomment this block after implementing smell linting - // context.subscriptions.push( - // vscode.commands.registerCommand('ecooptimizer.toggleSmellLinting', () => { - // console.log('Eco: Toggle Smell Linting Command Triggered'); - // toggleSmellLinting(contextManager); - // }), - // ); - // Register the "Jump to Smell" command. context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.jumpToSmell', jumpToSmell), @@ -154,112 +104,12 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // Adds comments to lines describing the smell - const lineSelectManager = new LineSelectionManager(smellsCacheManager); - context.subscriptions.push( - vscode.window.onDidChangeTextEditorSelection((event) => { - console.log('Eco: Detected line selection event'); - lineSelectManager.commentLine(event.textEditor); - }), - ); - - // Register a listener for configuration changes - context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration((event) => { - handleConfigurationChange(event); - }), - ); - - // Listen for editor changes - // TODO: Uncomment this block after implementing smell linting - // context.subscriptions.push( - // vscode.window.onDidChangeActiveTextEditor(async (editor) => { - // if (editor) { - // console.log('Eco: Detected editor change event'); - - // // Check if the file is a Python file - // if (editor.document.languageId === 'python') { - // console.log('Eco: Active file is a Python file.'); - - // // Check if smell linting is enabled - // const isEnabled = context.workspaceState.get( - // envConfig.SMELL_LINTING_ENABLED_KEY, - // false, - // ); - // if (isEnabled) { - // console.log('Eco: Smell linting is enabled. Detecting smells...'); - // await detectSmells(contextManager); - // } - // } - // } - // }), - // ); - // Register the file save listener to detect outdated files. const fileSaveListener = registerFileSaveListener( smellsCacheManager, smellsDisplayProvider, ); context.subscriptions.push(fileSaveListener); - - // TODO: Setting to re-enable popup if disabled - const settingsPopupChoice = context.globalState.get('showSettingsPopup'); - - if (settingsPopupChoice === undefined || settingsPopupChoice) { - showSettingsPopup(context); - } -} - -function showSettingsPopup(context: vscode.ExtensionContext): void { - // Check if the required settings are already configured - const config = vscode.workspace.getConfiguration('ecooptimizer'); - const workspacePath = config.get('projectWorkspacePath', ''); - const logsOutputPath = config.get('logsOutputPath', ''); - const unitTestPath = config.get('unitTestPath', ''); - - // If settings are not configured, prompt the user to configure them - if (!workspacePath || !logsOutputPath || !unitTestPath) { - vscode.window - .showInformationMessage( - 'Please configure the paths for your workspace and logs.', - { modal: true }, - 'Continue', // Button to open settings - 'Skip', // Button to dismiss - 'Never show this again', - ) - .then((selection) => { - if (selection === 'Continue') { - // Open the settings page filtered to extension's settings - vscode.commands.executeCommand( - 'workbench.action.openSettings', - 'ecooptimizer', - ); - } else if (selection === 'Skip') { - // Inform user they can configure later - vscode.window.showInformationMessage( - 'You can configure the paths later in the settings.', - ); - } else if (selection === 'Never show this again') { - context.globalState.update('showSettingsPopup', false); - vscode.window.showInformationMessage( - 'You can re-enable this popup again in the settings.', - ); - } - }); - } -} - -function handleConfigurationChange(event: vscode.ConfigurationChangeEvent): void { - // Check if any relevant setting was changed - if ( - event.affectsConfiguration('ecooptimizer.projectWorkspacePath') || - event.affectsConfiguration('ecooptimizer.logsOutputPath') - ) { - // Display a warning message about changing critical settings - vscode.window.showWarningMessage( - 'You have changed a critical setting for the EcoOptimizer plugin. Ensure the new value is valid and correct for optimal functionality.', - ); - } } /** @@ -267,5 +117,4 @@ function handleConfigurationChange(event: vscode.ConfigurationChangeEvent): void */ export function deactivate(): void { console.log('Deactivating Eco-Optimizer extension...'); - logManager.stopWatchingLogs(); } diff --git a/src/global.d.ts b/src/global.d.ts index cf5f436..ae62978 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -33,33 +33,4 @@ declare global { occurences: Occurrence[]; // Optional: List of occurrences for repeated calls additionalInfo: AdditionalInfo; } - - export interface ChangedFile { - original: string; - refactored: string; - } - - export interface RefactoredData { - tempDir: string; - targetFile: ChangedFile; - energySaved: number; - affectedFiles: ChangedFile[]; - } - - export interface RefactorOutput { - refactoredData?: RefactoredData; // Refactored code as a string - updatedSmells: Smell[]; // - } - - export interface ActiveDiff { - files: ChangedFile[]; - isOpen: boolean; - firstOpen: boolean; - } - - export type SmellDetails = { - symbol: string; - message: string; - }; - } diff --git a/src/listeners/fileSaveListener.ts b/src/listeners/fileSaveListener.ts index ae07102..b38d2d0 100644 --- a/src/listeners/fileSaveListener.ts +++ b/src/listeners/fileSaveListener.ts @@ -30,17 +30,6 @@ export function registerFileSaveListener( )}" has been modified since the last analysis.`, ); - // Check if smell linting is enabled - // TODO: Uncomment this block after implementing smell linting - // const isEnabled = contextManager.getWorkspaceData( - // envConfig.SMELL_LINTING_ENABLED_KEY, - // false, - // ); - // if (isEnabled) { - // console.log('Eco: Smell linting is enabled. Detecting smells...'); - // await detectSmells(contextManager); - // } - // Mark file as outdated in the UI smellsDisplayProvider.markFileAsOutdated(filePath); } diff --git a/src/managers/SmellsViewStateManager.ts b/src/managers/SmellsViewStateManager.ts index 1e56c31..136751c 100644 --- a/src/managers/SmellsViewStateManager.ts +++ b/src/managers/SmellsViewStateManager.ts @@ -30,7 +30,6 @@ export class SmellsStateManager { ): void { this.fileStatusMap.set(filePath, 'passed'); - console.log('Smells:', smells, typeof smells); const formattedSmells: ProcessedSmell[] = smells.map((smell) => { const foundEntry = Object.values(smellMetadata).find( (smellData) => smellData.message_id === smell.messageId, diff --git a/src/types.d.ts b/src/types.d.ts deleted file mode 100644 index a52ddf3..0000000 --- a/src/types.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface Occurrence { - line: number; - endLine?: number; - column: number; - endColumn?: number; -} - -export interface AdditionalInfo { - // CRC - repetitions?: number; - callString?: string; - // SCL - concatTarget?: string; - innerLoopLine?: number; -} - -export interface Smell { - type: string; // Type of the smell (e.g., "performance", "convention") - symbol: string; // Symbolic identifier for the smell (e.g., "cached-repeated-calls") - message: string; // Detailed description of the smell - messageId: string; // Unique ID for the smell - confidence: string; // Confidence level (e.g., "HIGH", "MEDIUM") - path: string; // Optional: absolute file path - module: string; // Optional: Module name - obj?: string; // Optional: Object name associated with the smell (if applicable) - occurences: Occurrence[]; // Optional: List of occurrences for repeated calls - additionalInfo: AdditionalInfo; -} - -export interface ChangedFile { - original: string; - refactored: string; -} - -export interface RefactoredData { - tempDir: string; - targetFile: ChangedFile; - energySaved: number; - affectedFiles: ChangedFile[]; -} - -export interface RefactorOutput { - refactoredData?: RefactoredData; // Refactored code as a string - updatedSmells: Smell[]; // -} - -export interface ActiveDiff { - files: ChangedFile[]; - isOpen: boolean; - firstOpen: boolean; -} - -export type SmellDetails = { - symbol: string; - message: string; - colour: string; // RGB colour as a string -}; diff --git a/src/ui/fileHighlighter.ts b/src/ui/fileHighlighter.ts deleted file mode 100644 index c620507..0000000 --- a/src/ui/fileHighlighter.ts +++ /dev/null @@ -1,137 +0,0 @@ -import * as vscode from 'vscode'; -import { getEditor } from '../utils/editorUtils'; -import { HoverManager } from './hoverManager'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; - -export class FileHighlighter { - private static instance: FileHighlighter; - private decorations: vscode.TextEditorDecorationType[] = []; - - constructor( - private context: vscode.ExtensionContext, - private smellsCacheManager: SmellsCacheManager, - ) {} - - public static getInstance( - context: vscode.ExtensionContext, - smellsCacheManager: SmellsCacheManager, - ): FileHighlighter { - if (!FileHighlighter.instance) { - FileHighlighter.instance = new FileHighlighter(context, smellsCacheManager); - } - return FileHighlighter.instance; - } - - public resetHighlights(): void { - this.decorations.forEach((decoration) => decoration.dispose()); - this.decorations = []; - } - - public highlightSmells(editor: vscode.TextEditor, smells: Smell[]): void { - this.resetHighlights(); - - const config = vscode.workspace.getConfiguration('ecooptimizer.detection'); - const smellsConfig = config.get<{ - [key: string]: { enabled: boolean; colour: string }; - }>('smells', {}); - const useSingleColour = config.get('useSingleColour', false); - const singleHighlightColour = config.get( - 'singleHighlightColour', - 'rgba(255, 204, 0, 0.5)', - ); - const highlightStyle = config.get('highlightStyle', 'underline'); - - const activeSmells = new Set(smells.map((smell) => smell.symbol)); - - activeSmells.forEach((smellType) => { - const smellConfig = smellsConfig[smellType]; - if (smellConfig?.enabled) { - const colour = useSingleColour ? singleHighlightColour : smellConfig.colour; - this.highlightSmell(editor, smells, smellType, colour, highlightStyle); - } - }); - } - - private highlightSmell( - editor: vscode.TextEditor, - smells: Smell[], - targetSmell: string, - colour: string, - style: string, - ): void { - const smellLines: vscode.DecorationOptions[] = smells - .filter((smell: Smell) => { - const valid = smell.occurences.every((occurrence: { line: number }) => - isValidLine(occurrence.line), - ); - const isCorrectType = smell.symbol === targetSmell; - return valid && isCorrectType; - }) - .map((smell: Smell) => { - const line = smell.occurences[0].line - 1; // convert to zero-based line index for VS editor - const lineText = editor.document.lineAt(line).text; - const indexStart = lineText.length - lineText.trimStart().length; - const indexEnd = lineText.trimEnd().length + 2; - const range = new vscode.Range(line, indexStart, line, indexEnd); - - const hoverManager = HoverManager.getInstance( - this.context, - this.smellsCacheManager, - smells, - ); - return { range, hoverMessage: hoverManager.hoverContent || undefined }; - }); - - console.log('Highlighting smell:', targetSmell, colour, style, smellLines); - const decoration = this.getDecoration(colour, style); - editor.setDecorations(decoration, smellLines); - this.decorations.push(decoration); - } - - private getDecoration( - colour: string, - style: string, - ): vscode.TextEditorDecorationType { - switch (style) { - case 'underline': - return vscode.window.createTextEditorDecorationType({ - textDecoration: `wavy ${colour} underline 1px`, - }); - case 'flashlight': - return vscode.window.createTextEditorDecorationType({ - isWholeLine: true, - backgroundColor: colour, - }); - case 'border-arrow': - return vscode.window.createTextEditorDecorationType({ - borderWidth: '1px 2px 1px 0', - borderStyle: 'solid', - borderColor: colour, - after: { - contentText: '▶', - margin: '0 0 0 5px', - color: colour, - fontWeight: 'bold', - }, - overviewRulerColor: colour, - overviewRulerLane: vscode.OverviewRulerLane.Right, - }); - default: - return vscode.window.createTextEditorDecorationType({ - textDecoration: `wavy ${colour} underline 1px`, - }); - } - } -} - -function isValidLine(line: any): boolean { - return ( - line !== undefined && - line !== null && - typeof line === 'number' && - Number.isFinite(line) && - line > 0 && - Number.isInteger(line) && - line <= getEditor()!.document.lineCount - ); -} diff --git a/src/ui/hoverManager.ts b/src/ui/hoverManager.ts deleted file mode 100644 index 621b539..0000000 --- a/src/ui/hoverManager.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as vscode from 'vscode'; -import { - refactorSelectedSmell, - refactorAllSmellsOfType, -} from '../commands/refactorSmell'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; - -export class HoverManager { - private static instance: HoverManager; - private smells: Smell[]; - public hoverContent: vscode.MarkdownString; - - static getInstance( - context: vscode.ExtensionContext, - smellsCacheManager: SmellsCacheManager, - smells: Smell[], - ): HoverManager { - if (!HoverManager.instance) { - HoverManager.instance = new HoverManager(context, smellsCacheManager, smells); - } else { - HoverManager.instance.updateSmells(smells); - } - return HoverManager.instance; - } - - public constructor( - private context: vscode.ExtensionContext, - private smellsCacheManager: SmellsCacheManager, - smells: Smell[], - ) { - this.smells = smells || []; - this.hoverContent = this.registerHoverProvider() ?? new vscode.MarkdownString(); - this.registerCommands(); - } - - public updateSmells(smells: Smell[]): void { - this.smells = smells || []; - } - - // Register hover provider for Python files - public registerHoverProvider(): void { - this.context.subscriptions.push( - vscode.languages.registerHoverProvider( - { scheme: 'file', language: 'python' }, - { - provideHover: (document, position, _token) => { - const hoverContent = this.getHoverContent(document, position); - return hoverContent ? new vscode.Hover(hoverContent) : null; - }, - }, - ), - ); - } - - // hover content for detected smells - getHoverContent( - document: vscode.TextDocument, - position: vscode.Position, - ): vscode.MarkdownString | null { - const lineNumber = position.line + 1; // convert to 1-based index - console.log('line number: ' + position.line); - // filter to find the smells on current line - const smellsOnLine = this.smells.filter((smell) => - smell.occurences.some( - (occurrence) => - occurrence.line === lineNumber || - (occurrence.endLine && - lineNumber >= occurrence.line && - lineNumber <= occurrence.endLine), - ), - ); - - console.log('smells: ' + smellsOnLine); - - if (smellsOnLine.length === 0) { - return null; - } - - const hoverContent = new vscode.MarkdownString(); - hoverContent.isTrusted = true; // Allow command links - - smellsOnLine.forEach((smell) => { - hoverContent.appendMarkdown( - `**${smell.symbol}:** ${smell.message}\t\t` + - `[Refactor](command:extension.refactorThisSmell?${encodeURIComponent( - JSON.stringify(smell), - )})\t\t` + - `---[Refactor all smells of this type...](command:extension.refactorAllSmellsOfType?${encodeURIComponent( - JSON.stringify(smell), - )})\n\n`, - ); - console.log(hoverContent); - }); - - return hoverContent; - } - - // Register commands for refactor actions - public registerCommands(): void { - this.context.subscriptions.push( - vscode.commands.registerCommand( - 'extension.refactorThisSmell', - async (smell: Smell) => { - await refactorSelectedSmell(this.context, this.smellsCacheManager, smell); - }, - ), - // clicking "Refactor All Smells of this Type..." - vscode.commands.registerCommand( - 'extension.refactorAllSmellsOfType', - async (smell: Smell) => { - await refactorAllSmellsOfType( - this.context, - this.smellsCacheManager, - smell.messageId, - ); - }, - ), - ); - } -} diff --git a/src/ui/lineSelectionManager.ts b/src/ui/lineSelectionManager.ts deleted file mode 100644 index a34ec0b..0000000 --- a/src/ui/lineSelectionManager.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as vscode from 'vscode'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; - -export class LineSelectionManager { - private decoration: vscode.TextEditorDecorationType | null = null; - - public constructor(private smellsCacheManager: SmellsCacheManager) {} - - public removeLastComment(): void { - if (this.decoration) { - console.log('Removing decoration'); - this.decoration.dispose(); - } - } - - public commentLine(editor: vscode.TextEditor): void { - this.removeLastComment(); - - if (!editor) { - return; - } - - const filePath = editor.document.fileName; - const smells = this.smellsCacheManager.getCachedSmells(filePath); - - if (!smells || smells.length === 0) { - return; - } - - const { selection } = editor; - - if (!selection.isSingleLine) { - return; - } - - const selectedLine = selection.start.line; - console.log(`selection: ${selectedLine}`); - - const smellsAtLine = smells.filter((smell) => { - return smell.occurences[0].line === selectedLine + 1; - }); - - if (smellsAtLine.length === 0) { - return; - } - - let comment; - if (smellsAtLine.length > 1) { - comment = `🍂 Smell: ${smellsAtLine[0].symbol} | (+${ - smellsAtLine.length - 1 - })`; - } else { - comment = `🍂 Smell: ${smellsAtLine[0].symbol}`; - } - - this.decoration = vscode.window.createTextEditorDecorationType({ - isWholeLine: true, - after: { - contentText: comment, - color: 'rgb(153, 211, 212)', - margin: '0 0 0 10px', - textDecoration: 'none', - }, - }); - - const selectionLine: vscode.Range[] = []; - - const line_text = editor.document.lineAt(selectedLine).text; - const line_length = line_text.length; - const indexStart = line_length - line_text.trimStart().length; - const indexEnd = line_text.trimEnd().length + 1; - - selectionLine.push( - new vscode.Range(selectedLine, indexStart, selectedLine, indexEnd), - ); - - editor.setDecorations(this.decoration, selectionLine); - } -} diff --git a/src/utils/configManager.ts b/src/utils/configManager.ts deleted file mode 100644 index 45e49f5..0000000 --- a/src/utils/configManager.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as vscode from 'vscode'; - -export class ConfigManager { - // resolve ${workspaceFolder} placeholder - private static resolvePath(path: string): string { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; - return path.replace('${workspaceFolder}', workspaceFolder); - } - - // get workspace path - static getWorkspacePath(): string { - const rawPath = vscode.workspace - .getConfiguration('ecooptimizer') - .get('projectWorkspacePath', ''); - const resolvedPath = - rawPath || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; - - // write to both User and Workspace settings if not already set - this.writeSetting('projectWorkspacePath', resolvedPath); - - return resolvedPath; - } - - // get logs output path - static getLogsOutputPath(): string { - const rawPath = vscode.workspace - .getConfiguration('ecooptimizer') - .get('logsOutputPath', ''); - const workspacePath = this.getWorkspacePath(); - const resolvedPath = rawPath || `${workspacePath}/logs`; - - // write to both User and Workspace settings if not already set - this.writeSetting('logsOutputPath', resolvedPath); - - return resolvedPath; - } - - // listen for configuration changes - static onConfigChange(callback: () => void): void { - vscode.workspace.onDidChangeConfiguration((event) => { - if ( - event.affectsConfiguration('ecooptimizer.projectWorkspacePath') || - event.affectsConfiguration('ecooptimizer.logsOutputPath') - ) { - callback(); - } - }); - } - - // write settings to both User and Workspace if necessary - private static writeSetting(setting: string, value: string): void { - const config = vscode.workspace.getConfiguration('ecooptimizer'); - - // inspect current values in both User and Workspace settings - const currentValueGlobal = config.inspect(setting)?.globalValue; - const currentValueWorkspace = config.inspect(setting)?.workspaceValue; - - // update User Settings (Global) if empty - if (!currentValueGlobal || currentValueGlobal.trim() === '') { - config.update(setting, value, vscode.ConfigurationTarget.Global); - } - - // update Workspace Settings if empty - if (!currentValueWorkspace || currentValueWorkspace.trim() === '') { - config.update(setting, value, vscode.ConfigurationTarget.Workspace); - } - } -} diff --git a/src/utils/editorUtils.ts b/src/utils/editorUtils.ts deleted file mode 100644 index e15d1ca..0000000 --- a/src/utils/editorUtils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as vscode from 'vscode'; - -/** - * Gets the active editor and its file path if an editor is open. - * @returns {{ editor: vscode.TextEditor | undefined, filePath: string | undefined }} - * An object containing the active editor and the file path, or undefined for both if no editor is open. - */ -export function getEditorAndFilePath(): { - editor: vscode.TextEditor | undefined; - filePath: string | undefined; -} { - const activeEditor = vscode.window.activeTextEditor; - const filePath = activeEditor?.document.uri.fsPath; - return { editor: activeEditor, filePath }; -} - -/** - * Gets the active editor if an editor is open. - */ -export function getEditor(): vscode.TextEditor | undefined { - return vscode.window.activeTextEditor; -} diff --git a/src/utils/envConfig.ts b/src/utils/envConfig.ts index d55ccdc..7b98c34 100644 --- a/src/utils/envConfig.ts +++ b/src/utils/envConfig.ts @@ -4,22 +4,12 @@ dotenv.config(); export interface EnvConfig { SERVER_URL?: string; - SMELL_MAP_KEY?: string; - FILE_CHANGES_KEY?: string; - LAST_USED_SMELLS_KEY?: string; - CURRENT_REFACTOR_DATA_KEY?: string; SMELL_CACHE_KEY?: string; FILE_HASH_CACHE_KEY?: string; - ACTIVE_DIFF_KEY?: string; - SMELL_LINTING_ENABLED_KEY?: string; } export const envConfig: EnvConfig = { SERVER_URL: process.env.SERVER_URL, - SMELL_MAP_KEY: process.env.SMELL_MAP_KEY, - FILE_CHANGES_KEY: process.env.FILE_CHANGES_KEY, - LAST_USED_SMELLS_KEY: process.env.LAST_USED_SMELLS_KEY, - CURRENT_REFACTOR_DATA_KEY: process.env.CURRENT_REFACTOR_DATA_KEY, - ACTIVE_DIFF_KEY: process.env.ACTIVE_DIFF_KEY, - SMELL_LINTING_ENABLED_KEY: process.env.SMELL_LINTING_ENABLED_KEY, + SMELL_CACHE_KEY: process.env.SMELL_CACHE_KEY, + FILE_HASH_CACHE_KEY: process.env.FILE_HASH_CACHE_KEY, }; From ccf01d714f6be152d22ce133e2924cdc24998566 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 02:13:27 -0400 Subject: [PATCH 004/121] renamed to FilterViewProvider.ts --- src/commands/filterSmells.ts | 2 +- src/extension.ts | 2 +- .../{FilterSmellsProvider.ts => FilterViewProvider.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/providers/{FilterSmellsProvider.ts => FilterViewProvider.ts} (100%) diff --git a/src/commands/filterSmells.ts b/src/commands/filterSmells.ts index 590ca6e..febfa92 100644 --- a/src/commands/filterSmells.ts +++ b/src/commands/filterSmells.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { FilterSmellsProvider } from '../providers/FilterSmellsProvider'; +import { FilterSmellsProvider } from '../providers/FilterViewProvider'; /** * Registers VS Code commands for managing smell filters. diff --git a/src/extension.ts b/src/extension.ts index 9aaddc7..bdeb6f1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,7 +12,7 @@ import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; import { SmellsDisplayProvider } from './providers/SmellsViewProvider'; import { checkServerStatus } from './api/backend'; -import { FilterSmellsProvider } from './providers/FilterSmellsProvider'; +import { FilterSmellsProvider } from './providers/FilterViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; diff --git a/src/providers/FilterSmellsProvider.ts b/src/providers/FilterViewProvider.ts similarity index 100% rename from src/providers/FilterSmellsProvider.ts rename to src/providers/FilterViewProvider.ts From 89e9320437d493cd94b124fe54084f91c495fb0e Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 02:16:04 -0400 Subject: [PATCH 005/121] renamed to SmellsViewProvider class --- src/commands/detectSmells.ts | 8 ++++---- src/commands/refactorSmell.ts | 4 ++-- src/commands/resetConfiguration.ts | 4 ++-- src/commands/wipeWorkCache.ts | 4 ++-- src/context/SmellsCacheManager.ts | 4 ++-- src/extension.ts | 4 ++-- src/listeners/fileSaveListener.ts | 4 ++-- src/providers/SmellsViewProvider.ts | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 3646d7a..edde8e6 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import { fetchSmells } from '../api/backend'; -import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { getEnabledSmells } from '../utils/smellsData'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { serverStatus, ServerStatusType } from '../utils/serverStatus'; @@ -17,7 +17,7 @@ import { serverStatus, ServerStatusType } from '../utils/serverStatus'; */ export async function detectSmellsFile( smellsCacheManager: SmellsCacheManager, - treeDataProvider: SmellsDisplayProvider, + treeDataProvider: SmellsViewProvider, fileUri: vscode.Uri | string, ) { // Validate the file URI or path @@ -128,7 +128,7 @@ export async function detectSmellsFile( */ export async function detectSmellsFolder( smellsCacheManager: SmellsCacheManager, - treeDataProvider: SmellsDisplayProvider, + treeDataProvider: SmellsViewProvider, folderPath: string, ) { // Notify the user that folder analysis has started @@ -181,7 +181,7 @@ export async function detectSmellsFolder( async function handleOutdatedFile( filePath: string, smellsCacheManager: SmellsCacheManager, - smellsDisplayProvider: SmellsDisplayProvider, + smellsDisplayProvider: SmellsViewProvider, ) { // Check if the file is marked as outdated if (smellsDisplayProvider.isFileOutdated(filePath)) { diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 7919b7a..7a0cbfe 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,10 +1,10 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; // 📌 Refactor Code Smells for a File export async function refactorSmellsByType( - treeDataProvider: SmellsDisplayProvider, + treeDataProvider: SmellsViewProvider, fileUri: vscode.Uri | string, ) { if (!fileUri) { diff --git a/src/commands/resetConfiguration.ts b/src/commands/resetConfiguration.ts index fdd2305..945b7bc 100644 --- a/src/commands/resetConfiguration.ts +++ b/src/commands/resetConfiguration.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; // Updated import -import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; /** * Resets the workspace configuration by clearing the stored path and wiping cached smells. @@ -13,7 +13,7 @@ import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; export async function resetConfiguration( context: vscode.ExtensionContext, smellsCacheManager: SmellsCacheManager, - treeDataProvider: SmellsDisplayProvider, + treeDataProvider: SmellsViewProvider, ) { const confirm = await vscode.window.showWarningMessage( 'Are you sure you want to reset the workspace configuration? This will remove the currently selected folder and wipe cached smells.', diff --git a/src/commands/wipeWorkCache.ts b/src/commands/wipeWorkCache.ts index 768c780..e8c6aa8 100644 --- a/src/commands/wipeWorkCache.ts +++ b/src/commands/wipeWorkCache.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; /** * Clears the smells cache and refreshes the UI. @@ -9,7 +9,7 @@ import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; */ export async function wipeWorkCache( smellsCacheManager: SmellsCacheManager, - smellsDisplayProvider: SmellsDisplayProvider, + smellsDisplayProvider: SmellsViewProvider, ) { const userResponse = await vscode.window.showWarningMessage( 'Are you sure you want to clear the smells cache? This action cannot be undone.', diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index b015ea9..e425db9 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { createHash } from 'crypto'; -import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { envConfig } from '../utils/envConfig'; /** @@ -122,7 +122,7 @@ export class SmellsCacheManager { * @param smellsDisplayProvider - The tree view provider responsible for the UI. */ public async clearCacheAndRefreshUI( - smellsDisplayProvider: SmellsDisplayProvider, + smellsDisplayProvider: SmellsViewProvider, ): Promise { // Remove all cached smells from the workspace state await this.clearSmellsCache(); diff --git a/src/extension.ts b/src/extension.ts index bdeb6f1..291efaa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,7 @@ import { openFile } from './commands/openFile'; import { registerFilterSmellCommands } from './commands/filterSmells'; import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; -import { SmellsDisplayProvider } from './providers/SmellsViewProvider'; +import { SmellsViewProvider } from './providers/SmellsViewProvider'; import { checkServerStatus } from './api/backend'; import { FilterSmellsProvider } from './providers/FilterViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; @@ -27,7 +27,7 @@ export function activate(context: vscode.ExtensionContext): void { const smellsCacheManager = new SmellsCacheManager(context); // Initialize the Code Smells View. - const smellsDisplayProvider = new SmellsDisplayProvider(context); + const smellsDisplayProvider = new SmellsViewProvider(context); const codeSmellsView = vscode.window.createTreeView('ecooptimizer.view', { treeDataProvider: smellsDisplayProvider, }); diff --git a/src/listeners/fileSaveListener.ts b/src/listeners/fileSaveListener.ts index b38d2d0..63b3199 100644 --- a/src/listeners/fileSaveListener.ts +++ b/src/listeners/fileSaveListener.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { SmellsDisplayProvider } from '../providers/SmellsViewProvider'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import path from 'path'; /** @@ -10,7 +10,7 @@ import path from 'path'; */ export function registerFileSaveListener( smellsCacheManager: SmellsCacheManager, - smellsDisplayProvider: SmellsDisplayProvider, + smellsDisplayProvider: SmellsViewProvider, ): vscode.Disposable { return vscode.workspace.onDidSaveTextDocument(async (document) => { const filePath = document.fileName; diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 95d48b3..866ef36 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { SmellsStateManager } from '../managers/SmellsViewStateManager'; import { SmellsUIManager } from '../managers/SmellsViewUIManager'; -export class SmellsDisplayProvider implements vscode.TreeDataProvider { +export class SmellsViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; From 2495e3324c14be566c9b871537f8757be84acbb1 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 02:16:50 -0400 Subject: [PATCH 006/121] renamed to FilterViewProvider class --- src/commands/filterSmells.ts | 4 ++-- src/extension.ts | 4 ++-- src/providers/FilterViewProvider.ts | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/commands/filterSmells.ts b/src/commands/filterSmells.ts index febfa92..a3dd7d0 100644 --- a/src/commands/filterSmells.ts +++ b/src/commands/filterSmells.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { FilterSmellsProvider } from '../providers/FilterViewProvider'; +import { FilterViewProvider } from '../providers/FilterViewProvider'; /** * Registers VS Code commands for managing smell filters. @@ -8,7 +8,7 @@ import { FilterSmellsProvider } from '../providers/FilterViewProvider'; */ export function registerFilterSmellCommands( context: vscode.ExtensionContext, - filterSmellsProvider: FilterSmellsProvider, + filterSmellsProvider: FilterViewProvider, ) { /** * Toggles the state of a specific smell filter. diff --git a/src/extension.ts b/src/extension.ts index 291efaa..da67ee9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,7 +12,7 @@ import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; import { SmellsViewProvider } from './providers/SmellsViewProvider'; import { checkServerStatus } from './api/backend'; -import { FilterSmellsProvider } from './providers/FilterViewProvider'; +import { FilterViewProvider } from './providers/FilterViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; @@ -61,7 +61,7 @@ export function activate(context: vscode.ExtensionContext): void { ); // Initialize the Filter Smells View. - const filterSmellsProvider = new FilterSmellsProvider(context); + const filterSmellsProvider = new FilterViewProvider(context); const filterSmellsView = vscode.window.createTreeView('ecooptimizer.filterView', { treeDataProvider: filterSmellsProvider, showCollapseAll: true, diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index f5e975f..d8a416a 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -4,9 +4,7 @@ import { FilterSmellConfig, loadSmells, saveSmells } from '../utils/smellsData'; /** * Provides a tree view for filtering code smells within the VS Code extension. */ -export class FilterSmellsProvider - implements vscode.TreeDataProvider -{ +export class FilterViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter< vscode.TreeItem | undefined | void > = new vscode.EventEmitter(); From 160d55d428bdcb13d9ce2664fb067c18444b261b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 02:17:31 -0400 Subject: [PATCH 007/121] cleaning --- src/commands/toggleSmellLinting.ts | 49 ------------------------------ 1 file changed, 49 deletions(-) delete mode 100644 src/commands/toggleSmellLinting.ts diff --git a/src/commands/toggleSmellLinting.ts b/src/commands/toggleSmellLinting.ts deleted file mode 100644 index c605645..0000000 --- a/src/commands/toggleSmellLinting.ts +++ /dev/null @@ -1,49 +0,0 @@ -// import * as vscode from 'vscode'; -// import { ContextManager } from '../context/contextManager'; -// import { FileHighlighter } from '../ui/fileHighlighter'; // Import the class -// import { envConfig } from '../utils/envConfig'; - -// export async function toggleSmellLinting( -// contextManager: ContextManager, -// ): Promise { -// const isEnabled = contextManager.getWorkspaceData( -// envConfig.SMELL_LINTING_ENABLED_KEY, -// false, -// ); -// const newState = !isEnabled; - -// // Update state immediately for UI responsiveness -// vscode.commands.executeCommand('setContext', 'eco.smellLintingEnabled', newState); - -// // Use the singleton instance of FileHighlighter -// const fileHighlighter = FileHighlighter.getInstance(contextManager); - -// try { -// if (newState) { -// // Run detection and update state on success -// await detectSmells(contextManager); // in the future recieve a true/false - -// await contextManager.setWorkspaceData( -// envConfig.SMELL_LINTING_ENABLED_KEY, -// newState, -// ); -// } else { -// // Clear highlights and update state -// fileHighlighter.resetHighlights(); // Call resetHighlights on the singleton instance -// await contextManager.setWorkspaceData( -// envConfig.SMELL_LINTING_ENABLED_KEY, -// newState, -// ); -// vscode.window.showInformationMessage('Eco: Smell linting turned off.'); -// } -// } catch (error) { -// console.error('Eco: Error toggling smell linting:', error); -// vscode.window.showErrorMessage('Eco: Failed to toggle smell linting.'); -// // Ensure UI state matches actual on error -// vscode.commands.executeCommand( -// 'setContext', -// 'eco.smellLintingEnabled', -// isEnabled, -// ); -// } -// } From 7085de046054d5c94d4f10c1b580846ee9f92a5b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 02:48:32 -0400 Subject: [PATCH 008/121] some cleaning in SmellsCacheManager.ts --- src/context/SmellsCacheManager.ts | 50 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index e425db9..f1239e8 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -21,12 +21,8 @@ export class SmellsCacheManager { * @returns An array of detected smells or `undefined` if the file has not been analyzed. */ public getCachedSmells(filePath: string): Smell[] | undefined { - const cache = this.context.workspaceState.get>( - envConfig.SMELL_CACHE_KEY!, - {}, - ); - - return cache[filePath]; // Returns an array of smells or `undefined` if not cached + const cache = this.getFullSmellCache(); + return cache[filePath]; // May be undefined } /** @@ -37,13 +33,8 @@ export class SmellsCacheManager { * @param smells - The detected smells to store (empty array if no smells found). */ public async setCachedSmells(filePath: string, smells: Smell[]): Promise { - const cache = this.context.workspaceState.get>( - envConfig.SMELL_CACHE_KEY!, - {}, - ); - - cache[filePath] = smells; // Store detected smells or an empty array if clean - + const cache = this.getFullSmellCache(); + cache[filePath] = smells; await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); } @@ -61,14 +52,20 @@ export class SmellsCacheManager { * @param filePath - The path of the file to clear. */ public async clearCachedSmellsForFile(filePath: string): Promise { - const cache = this.context.workspaceState.get>( + const cache = this.getFullSmellCache(); + delete cache[filePath]; + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + } + + /** + * Retrieves the entire smell cache. + * @returns A record of file paths to cached smells. + */ + public getFullSmellCache(): Record { + return this.context.workspaceState.get>( envConfig.SMELL_CACHE_KEY!, {}, ); - - delete cache[filePath]; // Remove the file's cached smells - - await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); } // ============================ @@ -90,10 +87,7 @@ export class SmellsCacheManager { * @param hash - The computed file hash. */ public async storeFileHash(filePath: string, hash: string): Promise { - const hashes = this.context.workspaceState.get>( - envConfig.FILE_HASH_CACHE_KEY!, - {}, - ); + const hashes = this.getFullFileHashCache(); hashes[filePath] = hash; await this.context.workspaceState.update(envConfig.FILE_HASH_CACHE_KEY!, hashes); } @@ -104,11 +98,19 @@ export class SmellsCacheManager { * @returns The stored hash or undefined if not found. */ public getStoredFileHash(filePath: string): string | undefined { - const hashes = this.context.workspaceState.get>( + const hashes = this.getFullFileHashCache(); + return hashes[filePath]; + } + + /** + * Retrieves the entire file hash cache. + * @returns A record of file paths to SHA256 hashes. + */ + private getFullFileHashCache(): Record { + return this.context.workspaceState.get>( envConfig.FILE_HASH_CACHE_KEY!, {}, ); - return hashes[filePath]; } // ============================ From c52f13fb9379f1b7da74b40f5623cadb4bd699ee Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 02:49:12 -0400 Subject: [PATCH 009/121] Removed cache for smells when smells filters was modified --- src/extension.ts | 22 ++++++----- src/providers/FilterViewProvider.ts | 59 +++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index da67ee9..364452a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,9 +27,9 @@ export function activate(context: vscode.ExtensionContext): void { const smellsCacheManager = new SmellsCacheManager(context); // Initialize the Code Smells View. - const smellsDisplayProvider = new SmellsViewProvider(context); + const smellsViewProvider = new SmellsViewProvider(context); const codeSmellsView = vscode.window.createTreeView('ecooptimizer.view', { - treeDataProvider: smellsDisplayProvider, + treeDataProvider: smellsViewProvider, }); context.subscriptions.push(codeSmellsView); @@ -50,18 +50,22 @@ export function activate(context: vscode.ExtensionContext): void { // Register workspace-related commands. context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.configureWorkspace', () => - configureWorkspace(context, smellsDisplayProvider), + configureWorkspace(context, smellsViewProvider), ), ); context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.resetConfiguration', () => - resetConfiguration(context, smellsCacheManager, smellsDisplayProvider), + resetConfiguration(context, smellsCacheManager, smellsViewProvider), ), ); // Initialize the Filter Smells View. - const filterSmellsProvider = new FilterViewProvider(context); + const filterSmellsProvider = new FilterViewProvider( + context, + smellsCacheManager, + smellsViewProvider, + ); const filterSmellsView = vscode.window.createTreeView('ecooptimizer.filterView', { treeDataProvider: filterSmellsProvider, showCollapseAll: true, @@ -82,13 +86,13 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand( 'ecooptimizer.detectSmellsFolder', (folderPath) => - detectSmellsFolder(smellsCacheManager, smellsDisplayProvider, folderPath), + detectSmellsFolder(smellsCacheManager, smellsViewProvider, folderPath), ), ); context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileUri) => - detectSmellsFile(smellsCacheManager, smellsDisplayProvider, fileUri), + detectSmellsFile(smellsCacheManager, smellsViewProvider, fileUri), ), ); @@ -100,14 +104,14 @@ export function activate(context: vscode.ExtensionContext): void { // Register the "Clear Smells Cache" command. context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.wipeWorkCache', async () => { - await wipeWorkCache(smellsCacheManager, smellsDisplayProvider); + await wipeWorkCache(smellsCacheManager, smellsViewProvider); }), ); // Register the file save listener to detect outdated files. const fileSaveListener = registerFileSaveListener( smellsCacheManager, - smellsDisplayProvider, + smellsViewProvider, ); context.subscriptions.push(fileSaveListener); } diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index d8a416a..de9d7c5 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -1,5 +1,7 @@ import * as vscode from 'vscode'; import { FilterSmellConfig, loadSmells, saveSmells } from '../utils/smellsData'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { SmellsViewProvider } from './SmellsViewProvider'; /** * Provides a tree view for filtering code smells within the VS Code extension. @@ -14,7 +16,11 @@ export class FilterViewProvider implements vscode.TreeDataProvider; private smells: Record = {}; - constructor(private context: vscode.ExtensionContext) { + constructor( + private context: vscode.ExtensionContext, + private cacheManager: SmellsCacheManager, + private smellsViewProvider: SmellsViewProvider, + ) { this.smells = loadSmells(); } @@ -50,17 +56,20 @@ export class FilterViewProvider implements vscode.TreeDataProvider { if (!element) { return Promise.resolve( - Object.keys(this.smells).map((smellKey) => { - const smell = this.smells[smellKey]; - return new SmellItem( - smellKey, - smell.name, - smell.enabled, - smell.analyzer_options && Object.keys(smell.analyzer_options).length > 0 - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.None, - ); - }), + Object.keys(this.smells) + .sort((a, b) => this.smells[a].name.localeCompare(this.smells[b].name)) + .map((smellKey) => { + const smell = this.smells[smellKey]; + return new SmellItem( + smellKey, + smell.name, + smell.enabled, + smell.analyzer_options && + Object.keys(smell.analyzer_options).length > 0 + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None, + ); + }), ); } @@ -83,30 +92,34 @@ export class FilterViewProvider implements vscode.TreeDataProvider { if (this.smells[smellKey]) { this.smells[smellKey].enabled = !this.smells[smellKey].enabled; saveSmells(this.smells); + await this.invalidateCachedSmellsForAffectedFiles(); this._onDidChangeTreeData.fire(); } } /** * Updates the value of a specific smell option and saves the configuration. + * Also clears the smell cache for affected files. * @param smellKey - The key of the smell. * @param optionKey - The key of the option. * @param newValue - The new value to set. */ - updateOption( + async updateOption( smellKey: string, optionKey: string, newValue: number | string, - ): void { + ): Promise { if (this.smells[smellKey]?.analyzer_options?.[optionKey]) { this.smells[smellKey].analyzer_options[optionKey].value = newValue; saveSmells(this.smells); + await this.invalidateCachedSmellsForAffectedFiles(); this._onDidChangeTreeData.fire(); } else { vscode.window.showErrorMessage( @@ -124,15 +137,29 @@ export class FilterViewProvider implements vscode.TreeDataProvider { Object.keys(this.smells).forEach((key) => { this.smells[key].enabled = enabled; }); saveSmells(this.smells); + await this.invalidateCachedSmellsForAffectedFiles(); this._onDidChangeTreeData.fire(); } + + /** + * Clears smell cache and marks all cached file results as outdated. + */ + async invalidateCachedSmellsForAffectedFiles(): Promise { + const cache = this.cacheManager.getFullSmellCache(); + + for (const [filePath, smells] of Object.entries(cache)) { + await this.cacheManager.clearCachedSmellsForFile(filePath); + this.smellsViewProvider.markFileAsOutdated(filePath); + } + } } /** From 300b5838208eeb5be14cf8b26254b934a01e7eb5 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 02:59:51 -0400 Subject: [PATCH 010/121] added warning pop-up if users changed filter --- src/providers/FilterViewProvider.ts | 31 ++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index de9d7c5..b1e5944 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -31,12 +31,18 @@ export class FilterViewProvider implements vscode.TreeDataProvider): void { this.treeView = treeView; - this.treeView.onDidChangeCheckboxState((event) => { - event.items.forEach((item) => { - if (item[0] instanceof SmellItem) { - this.toggleSmell(item[0].key); + this.treeView.onDidChangeCheckboxState(async (event) => { + for (const [item] of event.items) { + if (item instanceof SmellItem) { + const confirmed = await this.confirmFilterChange(); + if (confirmed) { + await this.toggleSmell(item.key); + } else { + // Cancelled — refresh the tree to revert the checkbox UI + this._onDidChangeTreeData.fire(); + } } - }); + } }); } @@ -116,6 +122,9 @@ export class FilterViewProvider implements vscode.TreeDataProvider { + const confirmed = await this.confirmFilterChange(); + if (!confirmed) return; + if (this.smells[smellKey]?.analyzer_options?.[optionKey]) { this.smells[smellKey].analyzer_options[optionKey].value = newValue; saveSmells(this.smells); @@ -141,6 +150,9 @@ export class FilterViewProvider implements vscode.TreeDataProvider { + const confirmed = await this.confirmFilterChange(); + if (!confirmed) return; + Object.keys(this.smells).forEach((key) => { this.smells[key].enabled = enabled; }); @@ -160,6 +172,15 @@ export class FilterViewProvider implements vscode.TreeDataProvider { + const result = await vscode.window.showWarningMessage( + 'Changing smell filters will invalidate existing analysis results. Do you want to continue?', + { modal: true }, + 'Yes', + ); + return result === 'Yes'; + } } /** From 6521b104c383e762dd973c9452917b1ed9cbc734 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 03:01:17 -0400 Subject: [PATCH 011/121] comment --- src/providers/FilterViewProvider.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index b1e5944..5c16c80 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -173,6 +173,13 @@ export class FilterViewProvider implements vscode.TreeDataProvider { const result = await vscode.window.showWarningMessage( 'Changing smell filters will invalidate existing analysis results. Do you want to continue?', From 878d5cab44c5dd5558dd47b72216ae49ccb6efc5 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 14:38:22 -0400 Subject: [PATCH 012/121] fixed when refactor smell by type icon is shown --- package.json | 9 ++------- src/managers/SmellsViewUIManager.ts | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 56110de..bd1fd41 100644 --- a/package.json +++ b/package.json @@ -196,11 +196,6 @@ "icon": "$(search)", "category": "Eco Optimizer" }, - { - "command": "ecooptimizer.refactorSmell", - "title": "Refactor Smell", - "category": "Eco" - }, { "command": "ecooptimizer.refactorAllSmellsOfType", "title": "Refactor Smells By Type", @@ -254,12 +249,12 @@ }, { "command": "ecooptimizer.detectSmellsFile", - "when": "viewItem == ecoOptimizerFile", + "when": "viewItem == ecoOptimizerFile || viewItem == ecoOptimizerFile-hasSmells", "group": "inline" }, { "command": "ecooptimizer.refactorAllSmellsOfType", - "when": "viewItem == ecoOptimizerFile", + "when": "viewItem == ecoOptimizerFile-hasSmells", "group": "inline" } ] diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts index d164dca..e3f6651 100644 --- a/src/managers/SmellsViewUIManager.ts +++ b/src/managers/SmellsViewUIManager.ts @@ -46,6 +46,11 @@ export class SmellsUIManager { item.contextValue = 'ecoOptimizerFile'; this.assignOpenFileCommand(item, element); this.updateFileItem(item, status, isOutdated); + + // Add a context value for files with smells + if (hasSmells && status === 'passed') { + item.contextValue = 'ecoOptimizerFile-hasSmells'; // Append 'hasSmells' to the context value + } } else { item.contextValue = 'ecoOptimizerSmell'; const parentFile = this.stateManager.getFileForSmell(element); From e574404cf3ae9a4c2eba36004f9a147bfd23f8ac Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 14:53:18 -0400 Subject: [PATCH 013/121] added fix for outdated files showing refactor button --- package.json | 11 +++++++++++ src/managers/SmellsViewUIManager.ts | 10 +++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bd1fd41..cdbd86d 100644 --- a/package.json +++ b/package.json @@ -202,6 +202,12 @@ "icon": "$(tools)", "category": "Eco Optimizer" }, + { + "command": "ecooptimizer.refactorSmell", + "title": "Refactor Smell", + "icon": "$(tools)", + "category": "Eco Optimizer" + }, { "command": "ecooptimizer.jumpToSmell", "title": "Jump to Smell in File", @@ -256,6 +262,11 @@ "command": "ecooptimizer.refactorAllSmellsOfType", "when": "viewItem == ecoOptimizerFile-hasSmells", "group": "inline" + }, + { + "command": "ecooptimizer.refactorSmell", + "when": "viewItem == ecoOptimizerSmell", + "group": "inline" } ] } diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts index e3f6651..0f002af 100644 --- a/src/managers/SmellsViewUIManager.ts +++ b/src/managers/SmellsViewUIManager.ts @@ -48,7 +48,7 @@ export class SmellsUIManager { this.updateFileItem(item, status, isOutdated); // Add a context value for files with smells - if (hasSmells && status === 'passed') { + if (hasSmells && status === 'passed' && !isOutdated) { item.contextValue = 'ecoOptimizerFile-hasSmells'; // Append 'hasSmells' to the context value } } else { @@ -62,7 +62,6 @@ export class SmellsUIManager { } this.setSmellTooltip(item, element); } - return item; } @@ -96,12 +95,13 @@ export class SmellsUIManager { 'warning', new vscode.ThemeColor('charts.orange'), ); + item.tooltip = `${path.basename(this.getStatusMessage('outdated'))}`; } else { item.iconPath = this.getStatusIcon(status); + item.tooltip = `${path.basename( + item.label as string, + )} (${this.getStatusMessage(status)})`; } - item.tooltip = `${path.basename( - item.label as string, - )} (${this.getStatusMessage(status)})`; } /** From c69d477230606a9969f5b612ed7e5b292d44b640 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 17:46:28 -0400 Subject: [PATCH 014/121] minor change how we process smells in smells view --- src/commands/detectSmells.ts | 4 +-- src/managers/SmellsViewStateManager.ts | 42 +++++--------------------- src/providers/SmellsViewProvider.ts | 11 ++----- 3 files changed, 12 insertions(+), 45 deletions(-) diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index edde8e6..71087e9 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -59,7 +59,7 @@ export async function detectSmellsFile( ); if (cachedSmells.length > 0) { - treeDataProvider.updateSmells(filePath, cachedSmells, enabledSmells); + treeDataProvider.updateSmells(filePath, cachedSmells); } else { treeDataProvider.updateStatus(filePath, 'no_issues'); } @@ -96,7 +96,7 @@ export async function detectSmellsFile( treeDataProvider.clearOutdatedStatus(filePath); if (smells.length > 0) { - treeDataProvider.updateSmells(filePath, smells, enabledSmells); + treeDataProvider.updateSmells(filePath, smells); vscode.window.showInformationMessage( `Analysis complete: Detected ${ smells.length diff --git a/src/managers/SmellsViewStateManager.ts b/src/managers/SmellsViewStateManager.ts index 136751c..8f2c231 100644 --- a/src/managers/SmellsViewStateManager.ts +++ b/src/managers/SmellsViewStateManager.ts @@ -1,19 +1,8 @@ import * as path from 'path'; -interface DetectedSmell { - messageId: string; - symbol: string; - occurences: { line: number; endLine?: number }[]; -} - -interface ProcessedSmell { - acronym: string; - occurrences: { line: number; endLine?: number }[]; -} - export class SmellsStateManager { private fileStatusMap: Map = new Map(); - private detectedSmells: Map = new Map(); + private detectedSmells: Map = new Map(); // Use Smell[] instead of ProcessedSmell[] private smellToFileMap: Map = new Map(); private modifiedFiles: Map = new Map(); @@ -21,36 +10,19 @@ export class SmellsStateManager { * Updates the detected smells for a file. * @param filePath - The analyzed file path. * @param smells - The detected smells in the file. - * @param smellMetadata - Metadata containing message ID and acronym for each smell. */ - updateSmells( - filePath: string, - smells: DetectedSmell[], - smellMetadata: Record, - ): void { + updateSmells(filePath: string, smells: Smell[]): void { this.fileStatusMap.set(filePath, 'passed'); - const formattedSmells: ProcessedSmell[] = smells.map((smell) => { - const foundEntry = Object.values(smellMetadata).find( - (smellData) => smellData.message_id === smell.messageId, - ) as { message_id: string; acronym: string }; - - return { - acronym: foundEntry ? foundEntry.acronym : smell.messageId, - occurrences: smell.occurences.map((occ) => ({ - line: occ.line, - endLine: occ.endLine, - })), - }; - }); - - this.detectedSmells.set(filePath, formattedSmells); + // Update the detected smells for the file + this.detectedSmells.set(filePath, smells); + // Update the detected smells for the folder const folderPath = path.dirname(filePath); if (!this.detectedSmells.has(folderPath)) { this.detectedSmells.set(folderPath, []); } - this.detectedSmells.get(folderPath)?.push(...formattedSmells); + this.detectedSmells.get(folderPath)?.push(...smells); } /** @@ -110,7 +82,7 @@ export class SmellsStateManager { * @param filePath - The path of the file. * @returns An array of smell entries. */ - getSmellsForFile(filePath: string): ProcessedSmell[] { + getSmellsForFile(filePath: string): Smell[] { return this.detectedSmells.get(filePath) || []; } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 866ef36..5be0e1d 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -60,7 +60,7 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { // If the file is not outdated, return the detected smells const smells = this.stateManager.getSmellsForFile(element); return smells.map((smell) => { - const smellItem = `${smell.acronym}: Line ${smell.occurrences + const smellItem = `${smell.messageId}: Line ${smell.occurences .map((o) => o.line) .join(', ')}`; this.stateManager.mapSmellToFile(smellItem, element); @@ -72,14 +72,9 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { * Updates the detected smells for a file and refreshes the tree view. * @param filePath - The analyzed file path. * @param smells - The detected smells in the file. - * @param smellMetadata - Metadata containing message ID and acronym for each smell. */ - updateSmells( - filePath: string, - smells: Smell[], - smellMetadata: Record, - ): void { - this.stateManager.updateSmells(filePath, smells, smellMetadata); + updateSmells(filePath: string, smells: Smell[]): void { + this.stateManager.updateSmells(filePath, smells); this.refresh(); } From 31b00f60cffd9f2752f80ad1ca1f79c20ddfd4c9 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 22:43:51 -0400 Subject: [PATCH 015/121] made smells have a unique identifier to help make refactoring --- src/api/backend.ts | 72 +++++++++++++++-- src/commands/detectSmells.ts | 3 +- src/context/SmellsCacheManager.ts | 98 +++++++++++++++++------ src/extension.ts | 35 +++++++++ src/global.d.ts | 17 ++++ src/managers/SmellsViewUIManager.ts | 116 ++++++++++++++++++---------- src/providers/SmellsViewProvider.ts | 2 +- 7 files changed, 273 insertions(+), 70 deletions(-) diff --git a/src/api/backend.ts b/src/api/backend.ts index 544ddea..64d8ee5 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -1,13 +1,16 @@ - import { envConfig } from '../utils/envConfig'; import { serverStatus } from '../utils/serverStatus'; import { ServerStatusType } from '../utils/serverStatus'; +import * as vscode from 'vscode'; const BASE_URL = `http://${envConfig.SERVER_URL}`; // API URL for Python backend +/** + * Checks the status of the backend server. + */ export async function checkServerStatus(): Promise { try { - const response = await fetch('http://localhost:8000/health'); + const response = await fetch(`${BASE_URL}/health`); if (response.ok) { serverStatus.setStatus(ServerStatusType.UP); } else { @@ -20,7 +23,7 @@ export async function checkServerStatus(): Promise { /** * Sends a request to the backend to detect code smells in the specified file. - * + * * @param filePath - The absolute path to the file being analyzed. * @param enabledSmells - A dictionary containing enabled smells and their configured options. * @returns A promise resolving to the backend response or throwing an error if unsuccessful. @@ -33,9 +36,9 @@ export async function fetchSmells( try { const response = await fetch(url, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ file_path: filePath, @@ -52,7 +55,7 @@ export async function fetchSmells( const smellsList = await response.json(); if (!Array.isArray(smellsList)) { - throw new Error("Unexpected response format from backend."); + throw new Error('Unexpected response format from backend.'); } return { smells: smellsList, status: response.status }; @@ -61,4 +64,61 @@ export async function fetchSmells( `Failed to connect to the backend: ${error.message}. Please check your network and try again.` ); } +} + +/** + * Sends a request to the backend to refactor a specific smell. + * + * @param filePath - The absolute path to the file containing the smell. + * @param smell - The smell to refactor. + * @returns A promise resolving to the refactored data or throwing an error if unsuccessful. + */ +export async function refactorSmell( + filePath: string, + smell: Smell +): Promise { + const url = `${BASE_URL}/refactor`; + + const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => + filePath.includes(folder.uri.fsPath) + ); + + if (!workspaceFolder) { + console.error('Eco: Error - Unable to determine workspace folder for', filePath); + throw new Error( + `Eco: Unable to find a matching workspace folder for file: ${filePath}` + ); + } + + const workspaceFolderPath = workspaceFolder.uri.fsPath; + + console.log( + `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspaceFolderPath}"` + ); + + const payload = { + source_dir: workspaceFolderPath, + smell, + }; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Refactoring failed'); + } + + const refactorResult = (await response.json()) as RefactoredData; + return refactorResult; + } catch (error: any) { + console.error('Eco: Unexpected error in refactorSmell:', error); + throw new Error(`Refactoring failed: ${error.message}`); + } } \ No newline at end of file diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 71087e9..bee79e8 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -91,12 +91,13 @@ export async function detectSmellsFile( if (status === 200) { // Cache detected smells, even if no smells are found await smellsCacheManager.setCachedSmells(filePath, smells); + const smellsWithID = smellsCacheManager.getCachedSmells(filePath) || []; // Remove the file from modifiedFiles after re-analysis treeDataProvider.clearOutdatedStatus(filePath); if (smells.length > 0) { - treeDataProvider.updateSmells(filePath, smells); + treeDataProvider.updateSmells(filePath, smellsWithID); vscode.window.showInformationMessage( `Analysis complete: Detected ${ smells.length diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index f1239e8..a73e132 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -5,6 +5,8 @@ import { envConfig } from '../utils/envConfig'; /** * Manages caching of detected smells and file hashes to avoid redundant backend calls. + * This class handles storing, retrieving, and clearing cached smells and file hashes, + * as well as refreshing the UI when the cache is updated. */ export class SmellsCacheManager { constructor(private context: vscode.ExtensionContext) {} @@ -13,33 +15,78 @@ export class SmellsCacheManager { // Smell Caching Methods // ============================ + /** + * Generates a unique string ID for a smell based on its content. + * The ID is derived from a SHA256 hash of the smell object. + * + * @param smell - The smell object to generate an ID for. + * @returns A unique string ID for the smell. + */ + private generateSmellId(smell: Smell): string { + // Generate a SHA256 hash of the smell object + const smellHash = createHash('sha256') + .update(JSON.stringify(smell)) + .digest('hex'); + + // Use the first 8 characters of the hash as the ID + return smellHash.substring(0, 3); + } + + /** + * Caches detected smells for a given file and assigns unique string IDs to each smell. + * The smells are stored in the workspace state for persistence. + * + * @param filePath - The absolute path of the file. + * @param smells - The detected smells to store. + */ + public async setCachedSmells(filePath: string, smells: Smell[]): Promise { + const cache = this.getFullSmellCache(); + + // Assign unique string IDs to each smell + const smellsWithIds = smells.map((smell) => { + const id = this.generateSmellId(smell); + return { + ...smell, + id, // Add the unique string ID to the smell object + }; + }); + + // Update the cache with the new smells + cache[filePath] = smellsWithIds; + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + } + /** * Retrieves cached smells for a given file. - * If the file has been analyzed and found clean, this will return an empty array. * * @param filePath - The absolute path of the file. - * @returns An array of detected smells or `undefined` if the file has not been analyzed. + * @returns An array of detected smells with unique IDs, or undefined if no smells are cached. */ public getCachedSmells(filePath: string): Smell[] | undefined { const cache = this.getFullSmellCache(); - return cache[filePath]; // May be undefined + return cache[filePath]; } /** - * Caches detected smells for a given file. - * If no smells are detected, caches an empty array to avoid redundant backend calls. + * Retrieves a smell by its unique string ID. * - * @param filePath - The absolute path of the file. - * @param smells - The detected smells to store (empty array if no smells found). + * @param id - The unique string ID of the smell. + * @returns The smell object, or undefined if no smell matches the ID. */ - public async setCachedSmells(filePath: string, smells: Smell[]): Promise { + public getSmellById(id: string): Smell | undefined { const cache = this.getFullSmellCache(); - cache[filePath] = smells; - await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + for (const filePath in cache) { + const smells = cache[filePath]; + const smell = smells.find((s) => s.id === id); + if (smell) { + return smell; + } + } + return undefined; } /** - * Clears all cached smells from the workspace. + * Clears all cached smells from the workspace state. * This forces a fresh analysis of all files when `detectSmellsFile` is called. */ public async clearSmellsCache(): Promise { @@ -58,8 +105,9 @@ export class SmellsCacheManager { } /** - * Retrieves the entire smell cache. - * @returns A record of file paths to cached smells. + * Retrieves the entire smell cache from the workspace state. + * + * @returns A record of file paths to their corresponding cached smells. */ public getFullSmellCache(): Record { return this.context.workspaceState.get>( @@ -73,18 +121,20 @@ export class SmellsCacheManager { // ============================ /** - * Computes a SHA256 hash of a file's contents. + * Computes a SHA256 hash of a file's contents and returns it as a string. + * * @param content - The file content as a string. - * @returns A SHA256 hash string. + * @returns A SHA256 hash string derived from the file content. */ public computeFileHash(content: string): string { return createHash('sha256').update(content).digest('hex'); } /** - * Stores a hash of a file's contents in workspaceState. - * @param filePath - Absolute path of the file. - * @param hash - The computed file hash. + * Stores a hash of a file's contents in the workspace state. + * + * @param filePath - The absolute path of the file. + * @param hash - The computed file hash as a string. */ public async storeFileHash(filePath: string, hash: string): Promise { const hashes = this.getFullFileHashCache(); @@ -94,8 +144,9 @@ export class SmellsCacheManager { /** * Retrieves the stored hash for a given file. - * @param filePath - Absolute path of the file. - * @returns The stored hash or undefined if not found. + * + * @param filePath - The absolute path of the file. + * @returns The stored hash as a string, or undefined if no hash is found. */ public getStoredFileHash(filePath: string): string | undefined { const hashes = this.getFullFileHashCache(); @@ -103,8 +154,9 @@ export class SmellsCacheManager { } /** - * Retrieves the entire file hash cache. - * @returns A record of file paths to SHA256 hashes. + * Retrieves the entire file hash cache from the workspace state. + * + * @returns A record of file paths to their corresponding SHA256 hashes. */ private getFullFileHashCache(): Record { return this.context.workspaceState.get>( @@ -119,7 +171,7 @@ export class SmellsCacheManager { /** * Clears all cached smells and refreshes the UI. - * Used by both "Clear Smells Cache" and "Reset Configuration". + * This method is used by both "Clear Smells Cache" and "Reset Configuration" commands. * * @param smellsDisplayProvider - The tree view provider responsible for the UI. */ diff --git a/src/extension.ts b/src/extension.ts index 364452a..653a125 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,6 +96,41 @@ export function activate(context: vscode.ExtensionContext): void { ), ); + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.refactorSmell', (fileUri) => { + // Ensure the fileUri is valid + if (!fileUri) { + console.error('No file URI provided.'); + return; + } + + // Extract the smell ID from the fileUri string (e.g., "(aa7) R0913: Line 15") + const smellIdMatch = fileUri.match(/\(([^)]+)\)/); + const smellId = smellIdMatch ? smellIdMatch[1] : null; + + if (!smellId) { + console.error('No smell ID found in the file URI:', fileUri); + return; + } + + // Retrieve the smell object by ID using the cache manager + const smell = smellsCacheManager.getSmellById(smellId); + if (!smell) { + console.error('No smell found with ID:', smellId); + return; + } + + // Get the file path from the smell object + const filePath = smell.path; + + // Print the file path and smell object to the console + console.log('File Path:', filePath); + console.log('Smell Object:', smell); + + // Add additional logic here to handle refactoring + }), + ); + // Register the "Jump to Smell" command. context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.jumpToSmell', jumpToSmell), diff --git a/src/global.d.ts b/src/global.d.ts index ae62978..8ae153e 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -32,5 +32,22 @@ declare global { obj?: string; // Optional: Object name associated with the smell (if applicable) occurences: Occurrence[]; // Optional: List of occurrences for repeated calls additionalInfo: AdditionalInfo; + id?: string; // Add this line to include the unique ID + } + + /** + * Represents the response from the `/refactor` endpoint. + */ + export interface RefactoredData { + tempDir: string; + targetFile: { + original: string; + refactored: string; + }; + energySaved: number | null; + affectedFiles: { + original: string; + refactored: string; + }[]; } } diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts index 0f002af..15623a4 100644 --- a/src/managers/SmellsViewUIManager.ts +++ b/src/managers/SmellsViewUIManager.ts @@ -1,72 +1,103 @@ import * as vscode from 'vscode'; -import * as path from 'path'; import * as fs from 'fs'; +import * as path from 'path'; import { SmellsStateManager } from './SmellsViewStateManager'; +/** + * Manages the UI representation of files, folders, and detected smells in the VS Code tree view. + * This class handles creating tree items, assigning commands, and updating item states based on + * the analysis status and file state (e.g., outdated, queued, passed, failed). + */ export class SmellsUIManager { constructor(private stateManager: SmellsStateManager) {} /** * Creates a tree item for a given element (folder, file, or smell). + * The tree item's appearance and behavior depend on the type of element and its current state. + * * @param element - The file or folder path, or a detected smell. + * @returns A `vscode.TreeItem` representing the element. */ createTreeItem(element: string): vscode.TreeItem { + // Retrieve the current status and smell information for the element const status = this.stateManager.getFileStatus(element); const hasSmells = this.stateManager.getSmellsForFile(element).length > 0; const isDirectory = fs.existsSync(element) && fs.statSync(element).isDirectory(); const isSmellItem = !fs.existsSync(element) && !isDirectory; - // Check if the file is outdated + // Check if the file is outdated (needs reanalysis) const isOutdated = !isDirectory && !isSmellItem && this.stateManager.isFileOutdated(element); - // Set the collapsible state + // Determine the collapsible state of the tree item let collapsibleState: vscode.TreeItemCollapsibleState; if (isDirectory) { - // Directories are always collapsible - collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; // Folders are collapsible } else if (isSmellItem) { - // Smell items are never collapsible - collapsibleState = vscode.TreeItemCollapsibleState.None; + collapsibleState = vscode.TreeItemCollapsibleState.None; // Smells are not collapsible } else if (isOutdated) { - // Outdated files are not collapsible - collapsibleState = vscode.TreeItemCollapsibleState.None; + collapsibleState = vscode.TreeItemCollapsibleState.None; // Outdated files are not collapsible } else { - // Files with smells are collapsible collapsibleState = hasSmells - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.None; + ? vscode.TreeItemCollapsibleState.Collapsed // Files with smells are collapsible + : vscode.TreeItemCollapsibleState.None; // Files without smells are not collapsible } + // Create the tree item with the element's basename and collapsible state const item = new vscode.TreeItem(path.basename(element), collapsibleState); + // Customize the tree item based on its type (folder, file, or smell) if (isDirectory) { + // Folders have a specific context value for styling and behavior item.contextValue = 'ecoOptimizerFolder'; } else if (!isSmellItem) { + // Files have a specific context value and can be opened item.contextValue = 'ecoOptimizerFile'; - this.assignOpenFileCommand(item, element); - this.updateFileItem(item, status, isOutdated); + this.assignOpenFileCommand(item, element); // Assign a command to open the file + this.updateFileItem(item, status, isOutdated); // Update the item's appearance based on status - // Add a context value for files with smells + // Add a context value for files with detected smells if (hasSmells && status === 'passed' && !isOutdated) { - item.contextValue = 'ecoOptimizerFile-hasSmells'; // Append 'hasSmells' to the context value + item.contextValue = 'ecoOptimizerFile-hasSmells'; } } else { + // Smells have a specific context value and display detailed information item.contextValue = 'ecoOptimizerSmell'; + + // Retrieve the parent file and smell object for the smell item const parentFile = this.stateManager.getFileForSmell(element); if (parentFile) { - const [, lineStr] = element.split(': Line '); - const lines = lineStr.split(',').map((line) => parseInt(line.trim(), 10)); - const firstLine = lines.length > 0 ? lines[0] - 1 : 0; - this.assignJumpToSmellCommand(item, parentFile, firstLine); + const smells = this.stateManager.getSmellsForFile(parentFile); + + // Extract the smell ID from the element's label + const idMatch = element.match(/\(([^)]+)\)/); + const id = idMatch ? idMatch[1] : null; + + // Find the smell by its ID + const smell = smells.find((s) => s.id === id); + + if (smell) { + // Set the label and description for the smell item + item.label = `${smell.messageId}: Line ${smell.occurences + .map((o) => o.line) + .join(', ')} (ID: ${smell.id}) `; + + // Assign a command to jump to the first occurrence of the smell in the file + const firstLine = smell.occurences[0]?.line - 1 || 0; // Default to line 0 if no occurrences + this.assignJumpToSmellCommand(item, parentFile, firstLine); + } } + + // Set the tooltip for the smell item this.setSmellTooltip(item, element); } + return item; } /** * Assigns a command to open a file when the tree item is clicked. + * * @param item - The tree item to update. * @param filePath - The path of the file to open. */ @@ -79,7 +110,27 @@ export class SmellsUIManager { } /** - * Updates the file item's status, including icon, message, and description. + * Assigns a command to jump to a specific line in a file when the tree item is clicked. + * + * @param item - The tree item to update. + * @param filePath - The path of the file containing the smell. + * @param line - The line number to jump to. + */ + private assignJumpToSmellCommand( + item: vscode.TreeItem, + filePath: string, + line: number, + ): void { + item.command = { + command: 'ecooptimizer.jumpToSmell', + title: 'Jump to Smell', + arguments: [filePath, line], + }; + } + + /** + * Updates the file item's appearance based on its analysis status and whether it is outdated. + * * @param item - The tree item to update. * @param status - The analysis status (e.g., "queued", "passed", "failed", "outdated"). * @param isOutdated - Whether the file is outdated. @@ -90,6 +141,7 @@ export class SmellsUIManager { isOutdated: boolean, ): void { if (isOutdated) { + // Mark the file as outdated with a warning icon and description item.description = 'outdated'; item.iconPath = new vscode.ThemeIcon( 'warning', @@ -97,6 +149,7 @@ export class SmellsUIManager { ); item.tooltip = `${path.basename(this.getStatusMessage('outdated'))}`; } else { + // Set the icon and tooltip based on the analysis status item.iconPath = this.getStatusIcon(status); item.tooltip = `${path.basename( item.label as string, @@ -104,26 +157,9 @@ export class SmellsUIManager { } } - /** - * Assigns a command to jump to a specific line in a file when the tree item is clicked. - * @param item - The tree item to update. - * @param filePath - The path of the file containing the smell. - * @param line - The line number to jump to. - */ - private assignJumpToSmellCommand( - item: vscode.TreeItem, - filePath: string, - line: number, - ): void { - item.command = { - command: 'ecooptimizer.jumpToSmell', - title: 'Jump to Smell', - arguments: [filePath, line], - }; - } - /** * Sets the tooltip for a smell item. + * * @param item - The tree item to update. * @param smellDescription - The description of the smell. */ @@ -133,6 +169,7 @@ export class SmellsUIManager { /** * Retrieves the appropriate VS Code icon based on the smell analysis status. + * * @param status - The analysis status. * @returns The corresponding VS Code theme icon. */ @@ -166,6 +203,7 @@ export class SmellsUIManager { /** * Retrieves the status message corresponding to the smell analysis state. + * * @param status - The analysis status. * @returns A descriptive status message. */ diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 5be0e1d..f61082e 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -62,7 +62,7 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { return smells.map((smell) => { const smellItem = `${smell.messageId}: Line ${smell.occurences .map((o) => o.line) - .join(', ')}`; + .join(', ')} (ID: ${smell.id})`; this.stateManager.mapSmellToFile(smellItem, element); return smellItem; }); From 26c6a9fcdb912400b59f12dec1d68d9f2c8a6d4c Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Fri, 21 Mar 2025 22:58:49 -0400 Subject: [PATCH 016/121] Added error catching for smell detection commands + fixed hashing logic --- src/commands/detectSmells.ts | 14 +++++++------- src/context/SmellsCacheManager.ts | 6 +++--- src/extension.ts | 19 ++++++++++++++----- src/providers/SmellsViewProvider.ts | 1 + 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index edde8e6..575c9d0 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -32,13 +32,6 @@ export async function detectSmellsFile( // Handle outdated files before proceeding await handleOutdatedFile(filePath, smellsCacheManager, treeDataProvider); - // Open the file and compute its hash - const document = await vscode.workspace.openTextDocument(filePath); - const fileContent = document.getText(); - - // Store the file hash after analyzing - await smellsCacheManager.storeFileHash(filePath, fileContent); - // Retrieve enabled smells from configuration const enabledSmells = getEnabledSmells(); @@ -52,6 +45,7 @@ export async function detectSmellsFile( // Check if smells are already cached const cachedSmells = smellsCacheManager.getCachedSmells(filePath); + console.log('Cached smells:', cachedSmells); if (cachedSmells !== undefined) { // Use cached smells if available vscode.window.showInformationMessage( @@ -188,6 +182,12 @@ async function handleOutdatedFile( // Delete cached smells for the outdated file await smellsCacheManager.clearCachedSmellsForFile(filePath); + const document = await vscode.workspace.openTextDocument(filePath); + const fileContent = document.getText(); + + console.log('Storing file hash for:', filePath); + await smellsCacheManager.storeFileHash(filePath, fileContent); + // Remove the outdated status from the UI smellsDisplayProvider.updateStatus(filePath, 'queued'); // Reset to "queued" or another default status } diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index f1239e8..5e734f4 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -84,11 +84,11 @@ export class SmellsCacheManager { /** * Stores a hash of a file's contents in workspaceState. * @param filePath - Absolute path of the file. - * @param hash - The computed file hash. + * @param content - The file content to hash. */ - public async storeFileHash(filePath: string, hash: string): Promise { + public async storeFileHash(filePath: string, content: string): Promise { const hashes = this.getFullFileHashCache(); - hashes[filePath] = hash; + hashes[filePath] = this.computeFileHash(content); await this.context.workspaceState.update(envConfig.FILE_HASH_CACHE_KEY!, hashes); } diff --git a/src/extension.ts b/src/extension.ts index 364452a..448ee14 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -85,15 +85,24 @@ export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push( vscode.commands.registerCommand( 'ecooptimizer.detectSmellsFolder', - (folderPath) => - detectSmellsFolder(smellsCacheManager, smellsViewProvider, folderPath), + (folderPath) => { + try { + detectSmellsFolder(smellsCacheManager, smellsViewProvider, folderPath); + } catch (error: any) { + vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); + } + }, ), ); context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileUri) => - detectSmellsFile(smellsCacheManager, smellsViewProvider, fileUri), - ), + vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileUri) => { + try { + detectSmellsFile(smellsCacheManager, smellsViewProvider, fileUri); + } catch (error: any) { + vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); + } + }), ); // Register the "Jump to Smell" command. diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 866ef36..ac9a30e 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -79,6 +79,7 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { smells: Smell[], smellMetadata: Record, ): void { + console.log('Updating UI with detected smells...', smells); this.stateManager.updateSmells(filePath, smells, smellMetadata); this.refresh(); } From 4fcd83e19d8e1f8e3fb6b768cce0dfc3d45eb651 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 23:15:05 -0400 Subject: [PATCH 017/121] fix bug when jumping to smell --- src/managers/SmellsViewUIManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts index 15623a4..1899d7e 100644 --- a/src/managers/SmellsViewUIManager.ts +++ b/src/managers/SmellsViewUIManager.ts @@ -70,7 +70,7 @@ export class SmellsUIManager { const smells = this.stateManager.getSmellsForFile(parentFile); // Extract the smell ID from the element's label - const idMatch = element.match(/\(([^)]+)\)/); + const idMatch = element.match(/\(ID:\s*([^)]+)\)/); const id = idMatch ? idMatch[1] : null; // Find the smell by its ID From 437b9eb69577bbf6ea9ea646eaa67f11eabb0a1c Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Fri, 21 Mar 2025 23:31:22 -0400 Subject: [PATCH 018/121] added inital refactoring stage --- src/commands/refactorSmell.ts | 41 ++++++++++++++++++++++++++--------- src/extension.ts | 7 ++++-- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 7a0cbfe..f835153 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,26 +1,47 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { refactorSmell as backendRefactorSmell } from '../api/backend'; // Import the backend function -// 📌 Refactor Code Smells for a File -export async function refactorSmellsByType( +/** + * Handles the refactoring of a specific smell in a file. + * + * @param treeDataProvider - The tree data provider for updating the UI. + * @param filePath - The path of the file to refactor. + * @param smell - The smell to refactor. + */ +export async function refactorSmell( treeDataProvider: SmellsViewProvider, - fileUri: vscode.Uri | string, + filePath: string, + smell: Smell, ) { - if (!fileUri) { - vscode.window.showErrorMessage('Error: No file selected for refactoring.'); + if (!filePath || !smell) { + vscode.window.showErrorMessage('Error: Invalid file path or smell.'); return; } - const filePath = typeof fileUri === 'string' ? fileUri : fileUri.fsPath; vscode.window.showInformationMessage( `Refactoring code smells in: ${path.basename(filePath)}`, ); - // Simulate backend request - setTimeout(() => { + try { + // Call the backend to refactor the smell + const refactoredData = await backendRefactorSmell(filePath, smell); + + // Log the response from the backend + console.log('Refactoring response:', refactoredData); + + // Notify the user vscode.window.showInformationMessage( - `Code smells refactored for: ${path.basename(filePath)}`, + `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); - }, 3000); + + // Optionally, open the refactored file + const refactoredFilePath = refactoredData.targetFile.refactored; + const document = await vscode.workspace.openTextDocument(refactoredFilePath); + await vscode.window.showTextDocument(document); + } catch (error: any) { + console.error('Refactoring failed:', error.message); + vscode.window.showErrorMessage(`Refactoring failed: ${error.message}`); + } } diff --git a/src/extension.ts b/src/extension.ts index 653a125..a73a3dc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { checkServerStatus } from './api/backend'; import { FilterViewProvider } from './providers/FilterViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; +import { refactorSmell } from './commands/refactorSmell'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -105,7 +106,8 @@ export function activate(context: vscode.ExtensionContext): void { } // Extract the smell ID from the fileUri string (e.g., "(aa7) R0913: Line 15") - const smellIdMatch = fileUri.match(/\(([^)]+)\)/); + console.log('File URi:', fileUri); + const smellIdMatch = fileUri.match(/\(ID:\s*([^)]+)\)/); const smellId = smellIdMatch ? smellIdMatch[1] : null; if (!smellId) { @@ -127,7 +129,8 @@ export function activate(context: vscode.ExtensionContext): void { console.log('File Path:', filePath); console.log('Smell Object:', smell); - // Add additional logic here to handle refactoring + // Call the refactorSmell function + refactorSmell(smellsViewProvider, filePath, smell); }), ); From 40d2e8cc3420e688b5f5744debccbf642fc48310 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 00:08:01 -0400 Subject: [PATCH 019/121] added refactoring view --- package.json | 5 ++ src/commands/refactorSmell.ts | 11 ++++ src/extension.ts | 22 +++++++- .../RefactoringDetailsViewProvider.ts | 53 +++++++++++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 src/providers/RefactoringDetailsViewProvider.ts diff --git a/package.json b/package.json index cdbd86d..0fa5dd9 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,11 @@ "id": "ecooptimizer.filterView", "name": "Filter Smells", "icon": "assets/eco-icon.png" + }, + { + "id": "ecooptimizer.refactoringDetails", + "name": "Refactoring Details", + "icon": "assets/eco-icon.png" } ] }, diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index f835153..bc30576 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,17 +1,20 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { refactorSmell as backendRefactorSmell } from '../api/backend'; // Import the backend function /** * Handles the refactoring of a specific smell in a file. * * @param treeDataProvider - The tree data provider for updating the UI. + * @param refactoringDetailsViewProvider - The refactoring details view provider. * @param filePath - The path of the file to refactor. * @param smell - The smell to refactor. */ export async function refactorSmell( treeDataProvider: SmellsViewProvider, + refactoringDetailsViewProvider: RefactoringDetailsViewProvider, filePath: string, smell: Smell, ) { @@ -31,6 +34,11 @@ export async function refactorSmell( // Log the response from the backend console.log('Refactoring response:', refactoredData); + // Update the refactoring details view with the refactored file name + refactoringDetailsViewProvider.updateRefactoringDetails( + refactoredData.targetFile.refactored, + ); + // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, @@ -43,5 +51,8 @@ export async function refactorSmell( } catch (error: any) { console.error('Refactoring failed:', error.message); vscode.window.showErrorMessage(`Refactoring failed: ${error.message}`); + + // Reset the refactoring details view on failure + refactoringDetailsViewProvider.resetRefactoringDetails(); } } diff --git a/src/extension.ts b/src/extension.ts index a73a3dc..9cd5f49 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import { FilterViewProvider } from './providers/FilterViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; import { refactorSmell } from './commands/refactorSmell'; +import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -97,6 +98,19 @@ export function activate(context: vscode.ExtensionContext): void { ), ); + // Initialize the RefactoringDetailsViewProvider + const refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); + const refactoringDetailsView = vscode.window.createTreeView( + 'ecooptimizer.refactoringDetails', + { + treeDataProvider: refactoringDetailsViewProvider, + }, + ); + + // Reset the refactoring details view initially + refactoringDetailsViewProvider.resetRefactoringDetails(); + + // Register the refactorSmell command context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.refactorSmell', (fileUri) => { // Ensure the fileUri is valid @@ -106,7 +120,6 @@ export function activate(context: vscode.ExtensionContext): void { } // Extract the smell ID from the fileUri string (e.g., "(aa7) R0913: Line 15") - console.log('File URi:', fileUri); const smellIdMatch = fileUri.match(/\(ID:\s*([^)]+)\)/); const smellId = smellIdMatch ? smellIdMatch[1] : null; @@ -130,7 +143,12 @@ export function activate(context: vscode.ExtensionContext): void { console.log('Smell Object:', smell); // Call the refactorSmell function - refactorSmell(smellsViewProvider, filePath, smell); + refactorSmell( + smellsViewProvider, + refactoringDetailsViewProvider, + filePath, + smell, + ); }), ); diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts new file mode 100644 index 0000000..88800f1 --- /dev/null +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -0,0 +1,53 @@ +import * as vscode from 'vscode'; + +export class RefactoringDetailsViewProvider + implements vscode.TreeDataProvider +{ + private _onDidChangeTreeData = new vscode.EventEmitter< + RefactoringDetailItem | undefined + >(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private refactoringDetails: RefactoringDetailItem[] = []; + + constructor() {} + + /** + * Updates the refactoring details with the given file name. + * @param fileName - The name of the refactored file. + */ + updateRefactoringDetails(fileName: string): void { + this.refactoringDetails = [ + new RefactoringDetailItem('Refactored File', fileName), + ]; + this._onDidChangeTreeData.fire(undefined); // Refresh the view + } + + /** + * Resets the refactoring details to indicate no refactoring is in progress. + */ + resetRefactoringDetails(): void { + this.refactoringDetails = [ + new RefactoringDetailItem('Status', 'Refactoring not in progress'), + ]; + this._onDidChangeTreeData.fire(undefined); // Refresh the view + } + + getTreeItem(element: RefactoringDetailItem): vscode.TreeItem { + return element; + } + + getChildren(element?: RefactoringDetailItem): RefactoringDetailItem[] { + if (element) { + return []; // No nested items + } + return this.refactoringDetails; + } +} + +class RefactoringDetailItem extends vscode.TreeItem { + constructor(label: string, description: string) { + super(label, vscode.TreeItemCollapsibleState.None); + this.description = description; + } +} From 269ff46b5e71e3e069759eed511674c3f52a0883 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 00:16:15 -0400 Subject: [PATCH 020/121] added diff editor + accept and reject (not working) --- package.json | 20 ++++++ src/commands/refactorSmell.ts | 16 +++-- src/extension.ts | 14 +++++ .../RefactoringDetailsViewProvider.ts | 62 +++++++++++++++++-- 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0fa5dd9..44c78df 100644 --- a/package.json +++ b/package.json @@ -213,6 +213,16 @@ "icon": "$(tools)", "category": "Eco Optimizer" }, + { + "command": "ecooptimizer.acceptRefactoring", + "title": "Accept Refactoring", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.rejectRefactoring", + "title": "Reject Refactoring", + "category": "Eco Optimizer" + }, { "command": "ecooptimizer.jumpToSmell", "title": "Jump to Smell in File", @@ -245,6 +255,16 @@ "command": "ecooptimizer.deselectAllFilterSmells", "when": "view == ecooptimizer.filterView", "group": "resource" + }, + { + "command": "ecooptimizer.acceptRefactoring", + "when": "view == ecooptimizer.refactoringDetails", + "group": "navigation" + }, + { + "command": "ecooptimizer.rejectRefactoring", + "when": "view == ecooptimizer.refactoringDetails", + "group": "navigation" } ], "view/item/context": [ diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index bc30576..ccfd673 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -37,17 +37,23 @@ export async function refactorSmell( // Update the refactoring details view with the refactored file name refactoringDetailsViewProvider.updateRefactoringDetails( refactoredData.targetFile.refactored, + refactoredData.targetFile.original, + ); + + // Show a diff view between the original and refactored files + const originalUri = vscode.Uri.file(refactoredData.targetFile.original); + const refactoredUri = vscode.Uri.file(refactoredData.targetFile.refactored); + await vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + refactoredUri, + 'Original ↔ Refactored', ); // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); - - // Optionally, open the refactored file - const refactoredFilePath = refactoredData.targetFile.refactored; - const document = await vscode.workspace.openTextDocument(refactoredFilePath); - await vscode.window.showTextDocument(document); } catch (error: any) { console.error('Refactoring failed:', error.message); vscode.window.showErrorMessage(`Refactoring failed: ${error.message}`); diff --git a/src/extension.ts b/src/extension.ts index 9cd5f49..cea6503 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -152,6 +152,20 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + // Register the acceptRefactoring command + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => { + refactoringDetailsViewProvider.acceptRefactoring(); + }), + ); + + // Register the rejectRefactoring command + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', () => { + refactoringDetailsViewProvider.rejectRefactoring(); + }), + ); + // Register the "Jump to Smell" command. context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.jumpToSmell', jumpToSmell), diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 88800f1..18156a1 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; export class RefactoringDetailsViewProvider implements vscode.TreeDataProvider @@ -9,16 +10,26 @@ export class RefactoringDetailsViewProvider readonly onDidChangeTreeData = this._onDidChangeTreeData.event; private refactoringDetails: RefactoringDetailItem[] = []; + private originalFilePath: string | undefined; + private refactoredFilePath: string | undefined; constructor() {} /** - * Updates the refactoring details with the given file name. - * @param fileName - The name of the refactored file. + * Updates the refactoring details with the given file names. + * @param refactoredFilePath - The path of the refactored file. + * @param originalFilePath - The path of the original file. */ - updateRefactoringDetails(fileName: string): void { + updateRefactoringDetails( + refactoredFilePath: string, + originalFilePath: string, + ): void { + this.refactoredFilePath = refactoredFilePath; + this.originalFilePath = originalFilePath; + this.refactoringDetails = [ - new RefactoringDetailItem('Refactored File', fileName), + new RefactoringDetailItem('Refactored File', refactoredFilePath, 'accept'), + new RefactoringDetailItem('Original File', originalFilePath, 'reject'), ]; this._onDidChangeTreeData.fire(undefined); // Refresh the view } @@ -27,6 +38,9 @@ export class RefactoringDetailsViewProvider * Resets the refactoring details to indicate no refactoring is in progress. */ resetRefactoringDetails(): void { + this.refactoredFilePath = undefined; + this.originalFilePath = undefined; + this.refactoringDetails = [ new RefactoringDetailItem('Status', 'Refactoring not in progress'), ]; @@ -43,11 +57,49 @@ export class RefactoringDetailsViewProvider } return this.refactoringDetails; } + + /** + * Handles the accept action. + */ + acceptRefactoring(): void { + if (this.refactoredFilePath && this.originalFilePath) { + // Replace the original file with the refactored file + fs.copyFileSync(this.refactoredFilePath, this.originalFilePath); + vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); + } else { + vscode.window.showErrorMessage('No refactoring data available.'); + } + } + + /** + * Handles the reject action. + */ + rejectRefactoring(): void { + vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); + } } class RefactoringDetailItem extends vscode.TreeItem { - constructor(label: string, description: string) { + constructor( + label: string, + description: string, + public readonly action?: 'accept' | 'reject', + ) { super(label, vscode.TreeItemCollapsibleState.None); this.description = description; + + if (action === 'accept') { + this.iconPath = new vscode.ThemeIcon('check'); + this.command = { + command: 'ecooptimizer.acceptRefactoring', + title: 'Accept Refactoring', + }; + } else if (action === 'reject') { + this.iconPath = new vscode.ThemeIcon('close'); + this.command = { + command: 'ecooptimizer.rejectRefactoring', + title: 'Reject Refactoring', + }; + } } } From e6c4d0200bccff33cd1c9473e5a2b2cb12177247 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 01:21:52 -0400 Subject: [PATCH 021/121] refactoring: removed file path param --- src/api/backend.ts | 23 +++++----- src/commands/refactorSmell.ts | 17 +++---- src/extension.ts | 45 ++++++++++--------- .../RefactoringDetailsViewProvider.ts | 25 +---------- 4 files changed, 46 insertions(+), 64 deletions(-) diff --git a/src/api/backend.ts b/src/api/backend.ts index 64d8ee5..a8b533d 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -30,7 +30,7 @@ export async function checkServerStatus(): Promise { */ export async function fetchSmells( filePath: string, - enabledSmells: Record> + enabledSmells: Record>, ): Promise<{ smells: Smell[]; status: number }> { const url = `${BASE_URL}/smells`; @@ -48,7 +48,7 @@ export async function fetchSmells( if (!response.ok) { throw new Error( - `Backend request failed with status ${response.status}: ${response.statusText}` + `Backend request failed with status ${response.status}: ${response.statusText}`, ); } @@ -61,7 +61,7 @@ export async function fetchSmells( return { smells: smellsList, status: response.status }; } catch (error: any) { throw new Error( - `Failed to connect to the backend: ${error.message}. Please check your network and try again.` + `Failed to connect to the backend: ${error.message}. Please check your network and try again.`, ); } } @@ -69,33 +69,36 @@ export async function fetchSmells( /** * Sends a request to the backend to refactor a specific smell. * - * @param filePath - The absolute path to the file containing the smell. * @param smell - The smell to refactor. * @returns A promise resolving to the refactored data or throwing an error if unsuccessful. */ -export async function refactorSmell( - filePath: string, - smell: Smell +export async function backendRefactorSmell( + smell: Smell, ): Promise { const url = `${BASE_URL}/refactor`; + // Extract the file path from the smell object + const filePath = smell.path; + + // Find the workspace folder containing the file const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => - filePath.includes(folder.uri.fsPath) + filePath.includes(folder.uri.fsPath), ); if (!workspaceFolder) { console.error('Eco: Error - Unable to determine workspace folder for', filePath); throw new Error( - `Eco: Unable to find a matching workspace folder for file: ${filePath}` + `Eco: Unable to find a matching workspace folder for file: ${filePath}`, ); } const workspaceFolderPath = workspaceFolder.uri.fsPath; console.log( - `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspaceFolderPath}"` + `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspaceFolderPath}"`, ); + // Prepare the payload for the backend const payload = { source_dir: workspaceFolderPath, smell, diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index ccfd673..6f40475 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,35 +1,30 @@ import * as vscode from 'vscode'; -import * as path from 'path'; +import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; -import { refactorSmell as backendRefactorSmell } from '../api/backend'; // Import the backend function /** - * Handles the refactoring of a specific smell in a file. + * Handles the refactoring of a specific smell. * * @param treeDataProvider - The tree data provider for updating the UI. * @param refactoringDetailsViewProvider - The refactoring details view provider. - * @param filePath - The path of the file to refactor. * @param smell - The smell to refactor. */ export async function refactorSmell( treeDataProvider: SmellsViewProvider, refactoringDetailsViewProvider: RefactoringDetailsViewProvider, - filePath: string, smell: Smell, ) { - if (!filePath || !smell) { - vscode.window.showErrorMessage('Error: Invalid file path or smell.'); + if (!smell) { + vscode.window.showErrorMessage('Error: Invalid smell.'); return; } - vscode.window.showInformationMessage( - `Refactoring code smells in: ${path.basename(filePath)}`, - ); + vscode.window.showInformationMessage(`Refactoring code smell: ${smell.symbol}`); try { // Call the backend to refactor the smell - const refactoredData = await backendRefactorSmell(filePath, smell); + const refactoredData = await backendRefactorSmell(smell); // Log the response from the backend console.log('Refactoring response:', refactoredData); diff --git a/src/extension.ts b/src/extension.ts index cea6503..e1e7777 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,7 @@ import { envConfig } from './utils/envConfig'; import * as vscode from 'vscode'; import { configureWorkspace } from './commands/configureWorkspace'; +import * as fs from 'fs'; import { resetConfiguration } from './commands/resetConfiguration'; import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; import { openFile } from './commands/openFile'; @@ -113,12 +114,6 @@ export function activate(context: vscode.ExtensionContext): void { // Register the refactorSmell command context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.refactorSmell', (fileUri) => { - // Ensure the fileUri is valid - if (!fileUri) { - console.error('No file URI provided.'); - return; - } - // Extract the smell ID from the fileUri string (e.g., "(aa7) R0913: Line 15") const smellIdMatch = fileUri.match(/\(ID:\s*([^)]+)\)/); const smellId = smellIdMatch ? smellIdMatch[1] : null; @@ -135,34 +130,44 @@ export function activate(context: vscode.ExtensionContext): void { return; } - // Get the file path from the smell object - const filePath = smell.path; - - // Print the file path and smell object to the console - console.log('File Path:', filePath); + // Print the smell object to the console console.log('Smell Object:', smell); - // Call the refactorSmell function - refactorSmell( - smellsViewProvider, - refactoringDetailsViewProvider, - filePath, - smell, - ); + // Call the refactorSmell function with only the smell object + refactorSmell(smellsViewProvider, refactoringDetailsViewProvider, smell); }), ); // Register the acceptRefactoring command context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => { - refactoringDetailsViewProvider.acceptRefactoring(); + const refactoredFilePath = refactoringDetailsViewProvider.refactoredFilePath; + const originalFilePath = refactoringDetailsViewProvider.originalFilePath; + + if (refactoredFilePath && originalFilePath) { + // Replace the original file with the refactored file + fs.copyFileSync(refactoredFilePath, originalFilePath); + vscode.window.showInformationMessage( + 'Refactoring accepted! Changes applied.', + ); + + // Reset the refactoring details view + refactoringDetailsViewProvider.resetRefactoringDetails(); + } else { + vscode.window.showErrorMessage('No refactoring data available.'); + } }), ); // Register the rejectRefactoring command context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', () => { - refactoringDetailsViewProvider.rejectRefactoring(); + vscode.window.showInformationMessage( + 'Refactoring rejected! Changes discarded.', + ); + + // Reset the refactoring details view + refactoringDetailsViewProvider.resetRefactoringDetails(); }), ); diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 18156a1..2b774ca 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; export class RefactoringDetailsViewProvider implements vscode.TreeDataProvider @@ -10,8 +9,8 @@ export class RefactoringDetailsViewProvider readonly onDidChangeTreeData = this._onDidChangeTreeData.event; private refactoringDetails: RefactoringDetailItem[] = []; - private originalFilePath: string | undefined; - private refactoredFilePath: string | undefined; + public originalFilePath: string | undefined; + public refactoredFilePath: string | undefined; constructor() {} @@ -57,26 +56,6 @@ export class RefactoringDetailsViewProvider } return this.refactoringDetails; } - - /** - * Handles the accept action. - */ - acceptRefactoring(): void { - if (this.refactoredFilePath && this.originalFilePath) { - // Replace the original file with the refactored file - fs.copyFileSync(this.refactoredFilePath, this.originalFilePath); - vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); - } else { - vscode.window.showErrorMessage('No refactoring data available.'); - } - } - - /** - * Handles the reject action. - */ - rejectRefactoring(): void { - vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); - } } class RefactoringDetailItem extends vscode.TreeItem { From 1927f8a516f3b53d779908482dd8171925414132 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 01:47:50 -0400 Subject: [PATCH 022/121] added accept reject icons --- package.json | 16 ++++--- src/commands/refactorSmell.ts | 45 +++++++++++++++++++ src/extension.ts | 9 +++- .../RefactoringDetailsViewProvider.ts | 41 ++++++++--------- 4 files changed, 80 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 44c78df..d293dcf 100644 --- a/package.json +++ b/package.json @@ -147,9 +147,9 @@ }, "viewsWelcome": [ { - "view": "ecooptimizer.view", - "contents": "No code smells detected yet. Configure your workspace to start analysis.\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", - "when": "!workspaceState.workspaceConfigured" + "view": "ecooptimizer.refactoringDetails", + "contents": "Refactoring is currently not in progress.", + "when": "!refactoringInProgress" } ], "commands": [ @@ -216,12 +216,14 @@ { "command": "ecooptimizer.acceptRefactoring", "title": "Accept Refactoring", - "category": "Eco Optimizer" + "category": "Eco Optimizer", + "icon": "$(check)" }, { "command": "ecooptimizer.rejectRefactoring", "title": "Reject Refactoring", - "category": "Eco Optimizer" + "category": "Eco Optimizer", + "icon": "$(close)" }, { "command": "ecooptimizer.jumpToSmell", @@ -258,12 +260,12 @@ }, { "command": "ecooptimizer.acceptRefactoring", - "when": "view == ecooptimizer.refactoringDetails", + "when": "view == ecooptimizer.refactoringDetails && refactoringInProgress", "group": "navigation" }, { "command": "ecooptimizer.rejectRefactoring", - "when": "view == ecooptimizer.refactoringDetails", + "when": "view == ecooptimizer.refactoringDetails && refactoringInProgress", "group": "navigation" } ], diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 6f40475..f4905a3 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -45,15 +45,60 @@ export async function refactorSmell( 'Original ↔ Refactored', ); + // Set a context key to track that refactoring is in progress + vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); + + // Listen for the diff editor being closed manually + const closeListener = vscode.window.onDidChangeVisibleTextEditors((editors) => { + const diffEditorStillOpen = editors.some( + (editor) => + editor.document.uri.toString() === originalUri.toString() || + editor.document.uri.toString() === refactoredUri.toString(), + ); + + if (!diffEditorStillOpen) { + // Show a confirmation popup if the diff editor is closed manually + vscode.window + .showWarningMessage( + 'You need to accept or reject the refactoring. Do you want to stop refactoring?', + { modal: true }, + 'Stop Refactoring', + ) + .then((choice) => { + if (choice === 'Stop Refactoring') { + // Reset the refactoring state + refactoringDetailsViewProvider.resetRefactoringDetails(); + vscode.commands.executeCommand( + 'setContext', + 'refactoringInProgress', + false, + ); + } else { + // Reopen the diff editor + vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + refactoredUri, + 'Original ↔ Refactored', + ); + } + }); + } + }); + // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); + + // Return the close listener so it can be disposed later + return closeListener; } catch (error: any) { console.error('Refactoring failed:', error.message); vscode.window.showErrorMessage(`Refactoring failed: ${error.message}`); // Reset the refactoring details view on failure refactoringDetailsViewProvider.resetRefactoringDetails(); + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } diff --git a/src/extension.ts b/src/extension.ts index e1e7777..14aa93e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -137,7 +137,6 @@ export function activate(context: vscode.ExtensionContext): void { refactorSmell(smellsViewProvider, refactoringDetailsViewProvider, smell); }), ); - // Register the acceptRefactoring command context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => { @@ -151,8 +150,12 @@ export function activate(context: vscode.ExtensionContext): void { 'Refactoring accepted! Changes applied.', ); + // Close the diff editor + vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + // Reset the refactoring details view refactoringDetailsViewProvider.resetRefactoringDetails(); + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } else { vscode.window.showErrorMessage('No refactoring data available.'); } @@ -166,8 +169,12 @@ export function activate(context: vscode.ExtensionContext): void { 'Refactoring rejected! Changes discarded.', ); + // Close the diff editor + vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + // Reset the refactoring details view refactoringDetailsViewProvider.resetRefactoringDetails(); + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); }), ); diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 2b774ca..1f39fff 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -12,7 +12,10 @@ export class RefactoringDetailsViewProvider public originalFilePath: string | undefined; public refactoredFilePath: string | undefined; - constructor() {} + constructor() { + // Initialize with the welcome view + this.resetRefactoringDetails(); + } /** * Updates the refactoring details with the given file names. @@ -26,9 +29,15 @@ export class RefactoringDetailsViewProvider this.refactoredFilePath = refactoredFilePath; this.originalFilePath = originalFilePath; + // Convert the absolute path of the original file to a relative path for display + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const relativeOriginalPath = workspaceFolder + ? vscode.workspace.asRelativePath(originalFilePath) + : originalFilePath; + + // Update the tree view with only the original file's relative path this.refactoringDetails = [ - new RefactoringDetailItem('Refactored File', refactoredFilePath, 'accept'), - new RefactoringDetailItem('Original File', originalFilePath, 'reject'), + new RefactoringDetailItem('Original File', relativeOriginalPath), ]; this._onDidChangeTreeData.fire(undefined); // Refresh the view } @@ -40,8 +49,12 @@ export class RefactoringDetailsViewProvider this.refactoredFilePath = undefined; this.originalFilePath = undefined; + // Set the welcome view this.refactoringDetails = [ - new RefactoringDetailItem('Status', 'Refactoring not in progress'), + new RefactoringDetailItem( + 'Status', + 'Refactoring is currently not in progress.', + ), ]; this._onDidChangeTreeData.fire(undefined); // Refresh the view } @@ -59,26 +72,8 @@ export class RefactoringDetailsViewProvider } class RefactoringDetailItem extends vscode.TreeItem { - constructor( - label: string, - description: string, - public readonly action?: 'accept' | 'reject', - ) { + constructor(label: string, description: string) { super(label, vscode.TreeItemCollapsibleState.None); this.description = description; - - if (action === 'accept') { - this.iconPath = new vscode.ThemeIcon('check'); - this.command = { - command: 'ecooptimizer.acceptRefactoring', - title: 'Accept Refactoring', - }; - } else if (action === 'reject') { - this.iconPath = new vscode.ThemeIcon('close'); - this.command = { - command: 'ecooptimizer.rejectRefactoring', - title: 'Reject Refactoring', - }; - } } } From 727a2997f7e151faada1321f7c813691372e5bbe Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 02:19:31 -0400 Subject: [PATCH 023/121] more refactoring --- package.json | 17 ++++--- src/extension.ts | 48 ++++++++++++++----- .../RefactoringDetailsViewProvider.ts | 17 +++---- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index d293dcf..bd4d82a 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,11 @@ }, "views": { "ecooptimizer": [ + { + "id": "ecooptimizer.refactoringDetails", + "name": "Refactoring Details", + "icon": "assets/eco-icon.png" + }, { "id": "ecooptimizer.view", "name": "Code Smells", @@ -137,19 +142,19 @@ "id": "ecooptimizer.filterView", "name": "Filter Smells", "icon": "assets/eco-icon.png" - }, - { - "id": "ecooptimizer.refactoringDetails", - "name": "Refactoring Details", - "icon": "assets/eco-icon.png" } ] }, "viewsWelcome": [ { "view": "ecooptimizer.refactoringDetails", - "contents": "Refactoring is currently not in progress.", + "contents": "Refactoring is currently not in progress. Try selecting a smell in the Code Smells view to start refactoring.", "when": "!refactoringInProgress" + }, + { + "view": "ecooptimizer.view", + "contents": "No code smells detected yet. Configure your workspace to start analysis.\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", + "when": "!workspaceState.workspaceConfigured" } ], "commands": [ diff --git a/src/extension.ts b/src/extension.ts index 14aa93e..03e3fd0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -137,6 +137,7 @@ export function activate(context: vscode.ExtensionContext): void { refactorSmell(smellsViewProvider, refactoringDetailsViewProvider, smell); }), ); + // Register the acceptRefactoring command context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => { @@ -144,18 +145,35 @@ export function activate(context: vscode.ExtensionContext): void { const originalFilePath = refactoringDetailsViewProvider.originalFilePath; if (refactoredFilePath && originalFilePath) { - // Replace the original file with the refactored file - fs.copyFileSync(refactoredFilePath, originalFilePath); - vscode.window.showInformationMessage( - 'Refactoring accepted! Changes applied.', - ); - - // Close the diff editor - vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - - // Reset the refactoring details view - refactoringDetailsViewProvider.resetRefactoringDetails(); - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + try { + // Replace the original file with the refactored file + fs.copyFileSync(refactoredFilePath, originalFilePath); + vscode.window.showInformationMessage( + 'Refactoring accepted! Changes applied.', + ); + + // Close the diff editor + vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + + // Reset the refactoring details view + refactoringDetailsViewProvider.resetRefactoringDetails(); + vscode.commands.executeCommand( + 'setContext', + 'refactoringInProgress', + false, + ); + + // Close the refactoring details view + vscode.commands.executeCommand( + 'workbench.action.closeView', + 'ecooptimizer.refactoringDetails', + ); + } catch (error) { + console.error('Failed to accept refactoring:', error); + vscode.window.showErrorMessage( + 'Failed to accept refactoring. Please try again.', + ); + } } else { vscode.window.showErrorMessage('No refactoring data available.'); } @@ -175,6 +193,12 @@ export function activate(context: vscode.ExtensionContext): void { // Reset the refactoring details view refactoringDetailsViewProvider.resetRefactoringDetails(); vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + + // Close the refactoring details view + vscode.commands.executeCommand( + 'workbench.action.closeView', + 'ecooptimizer.refactoringDetails', + ); }), ); diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 1f39fff..5d8c2e4 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -10,10 +10,10 @@ export class RefactoringDetailsViewProvider private refactoringDetails: RefactoringDetailItem[] = []; public originalFilePath: string | undefined; - public refactoredFilePath: string | undefined; + public refactoredFilePath: string | undefined; // Add this property constructor() { - // Initialize with the welcome view + // Initialize with an empty state this.resetRefactoringDetails(); } @@ -26,7 +26,7 @@ export class RefactoringDetailsViewProvider refactoredFilePath: string, originalFilePath: string, ): void { - this.refactoredFilePath = refactoredFilePath; + this.refactoredFilePath = refactoredFilePath; // Set the refactored file path this.originalFilePath = originalFilePath; // Convert the absolute path of the original file to a relative path for display @@ -46,16 +46,11 @@ export class RefactoringDetailsViewProvider * Resets the refactoring details to indicate no refactoring is in progress. */ resetRefactoringDetails(): void { - this.refactoredFilePath = undefined; + this.refactoredFilePath = undefined; // Reset the refactored file path this.originalFilePath = undefined; - // Set the welcome view - this.refactoringDetails = [ - new RefactoringDetailItem( - 'Status', - 'Refactoring is currently not in progress.', - ), - ]; + // Clear the tree view + this.refactoringDetails = []; this._onDidChangeTreeData.fire(undefined); // Refresh the view } From 3d9fd28075c5556a066052ec5ca4b90732edba75 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 02:56:19 -0400 Subject: [PATCH 024/121] more refactoring - open diff editor command --- package.json | 11 +++++ src/commands/refactorSmell.ts | 43 +------------------ src/extension.ts | 35 ++++++++++----- .../RefactoringDetailsViewProvider.ts | 7 +++ 4 files changed, 43 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index bd4d82a..e935595 100644 --- a/package.json +++ b/package.json @@ -239,6 +239,11 @@ "command": "ecooptimizer.wipeWorkCache", "title": "Clear Smells Cache", "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.openDiffEditor", + "title": "Open Diff Editor", + "category": "Eco Optimizer" } ], "menus": { @@ -299,6 +304,12 @@ "command": "ecooptimizer.refactorSmell", "when": "viewItem == ecoOptimizerSmell", "group": "inline" + }, + { + "command": "ecooptimizer.openDiffEditor", + "when": "viewItem == ecoOptimizerFile", + "group": "inline", + "icon": "$(diff)" } ] } diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index f4905a3..63f1f1f 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -42,57 +42,16 @@ export async function refactorSmell( 'vscode.diff', originalUri, refactoredUri, - 'Original ↔ Refactored', + 'Refactoring Comparison', ); // Set a context key to track that refactoring is in progress vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); - // Listen for the diff editor being closed manually - const closeListener = vscode.window.onDidChangeVisibleTextEditors((editors) => { - const diffEditorStillOpen = editors.some( - (editor) => - editor.document.uri.toString() === originalUri.toString() || - editor.document.uri.toString() === refactoredUri.toString(), - ); - - if (!diffEditorStillOpen) { - // Show a confirmation popup if the diff editor is closed manually - vscode.window - .showWarningMessage( - 'You need to accept or reject the refactoring. Do you want to stop refactoring?', - { modal: true }, - 'Stop Refactoring', - ) - .then((choice) => { - if (choice === 'Stop Refactoring') { - // Reset the refactoring state - refactoringDetailsViewProvider.resetRefactoringDetails(); - vscode.commands.executeCommand( - 'setContext', - 'refactoringInProgress', - false, - ); - } else { - // Reopen the diff editor - vscode.commands.executeCommand( - 'vscode.diff', - originalUri, - refactoredUri, - 'Original ↔ Refactored', - ); - } - }); - } - }); - // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); - - // Return the close listener so it can be disposed later - return closeListener; } catch (error: any) { console.error('Refactoring failed:', error.message); vscode.window.showErrorMessage(`Refactoring failed: ${error.message}`); diff --git a/src/extension.ts b/src/extension.ts index 03e3fd0..50e7aaf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -162,12 +162,6 @@ export function activate(context: vscode.ExtensionContext): void { 'refactoringInProgress', false, ); - - // Close the refactoring details view - vscode.commands.executeCommand( - 'workbench.action.closeView', - 'ecooptimizer.refactoringDetails', - ); } catch (error) { console.error('Failed to accept refactoring:', error); vscode.window.showErrorMessage( @@ -193,12 +187,31 @@ export function activate(context: vscode.ExtensionContext): void { // Reset the refactoring details view refactoringDetailsViewProvider.resetRefactoringDetails(); vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + }), + ); - // Close the refactoring details view - vscode.commands.executeCommand( - 'workbench.action.closeView', - 'ecooptimizer.refactoringDetails', - ); + // Register the command to open the diff editor + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.openDiffEditor', (fileUri) => { + const refactoredFilePath = refactoringDetailsViewProvider.refactoredFilePath; + const originalFilePath = refactoringDetailsViewProvider.originalFilePath; + + if (refactoredFilePath && originalFilePath) { + // Get the file name from the original file path + const fileName = originalFilePath.split('/').pop() || 'file'; + + // Show the diff editor + const originalUri = vscode.Uri.file(originalFilePath); + const refactoredUri = vscode.Uri.file(refactoredFilePath); + vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + refactoredUri, + `${fileName} (original) ↔ ${fileName} (refactored)`, + ); + } else { + vscode.window.showErrorMessage('No refactoring data available.'); + } }), ); diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 5d8c2e4..e7e39f3 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -70,5 +70,12 @@ class RefactoringDetailItem extends vscode.TreeItem { constructor(label: string, description: string) { super(label, vscode.TreeItemCollapsibleState.None); this.description = description; + + // Add a command to open the diff editor when the item is clicked + this.command = { + command: 'ecooptimizer.openDiffEditor', + title: 'Open Diff Editor', + arguments: [description], // Pass the file path as an argument + }; } } From c26df542e5bb824e25d0e793a33652214f6ca59e Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 03:22:57 -0400 Subject: [PATCH 025/121] refactor - fixed sidebar dropdown with affected files --- src/commands/refactorSmell.ts | 16 ++- src/extension.ts | 120 +++++++++--------- .../RefactoringDetailsViewProvider.ts | 103 +++++++++++---- 3 files changed, 146 insertions(+), 93 deletions(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 63f1f1f..44d7b40 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -29,20 +29,22 @@ export async function refactorSmell( // Log the response from the backend console.log('Refactoring response:', refactoredData); - // Update the refactoring details view with the refactored file name + // Update the refactoring details view with the target file and affected files refactoringDetailsViewProvider.updateRefactoringDetails( - refactoredData.targetFile.refactored, - refactoredData.targetFile.original, + refactoredData.targetFile, + refactoredData.affectedFiles, ); - // Show a diff view between the original and refactored files - const originalUri = vscode.Uri.file(refactoredData.targetFile.original); - const refactoredUri = vscode.Uri.file(refactoredData.targetFile.refactored); + // Show a diff view for the target file + const targetFile = refactoredData.targetFile; + const fileName = targetFile.original.split('/').pop() || 'file'; + const originalUri = vscode.Uri.file(targetFile.original); + const refactoredUri = vscode.Uri.file(targetFile.refactored); await vscode.commands.executeCommand( 'vscode.diff', originalUri, refactoredUri, - 'Refactoring Comparison', + `${fileName} (original) ↔ ${fileName} (refactored)`, ); // Set a context key to track that refactoring is in progress diff --git a/src/extension.ts b/src/extension.ts index 50e7aaf..f46780a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,7 +4,6 @@ import { envConfig } from './utils/envConfig'; import * as vscode from 'vscode'; import { configureWorkspace } from './commands/configureWorkspace'; -import * as fs from 'fs'; import { resetConfiguration } from './commands/resetConfiguration'; import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; import { openFile } from './commands/openFile'; @@ -138,66 +137,65 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // Register the acceptRefactoring command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => { - const refactoredFilePath = refactoringDetailsViewProvider.refactoredFilePath; - const originalFilePath = refactoringDetailsViewProvider.originalFilePath; - - if (refactoredFilePath && originalFilePath) { - try { - // Replace the original file with the refactored file - fs.copyFileSync(refactoredFilePath, originalFilePath); - vscode.window.showInformationMessage( - 'Refactoring accepted! Changes applied.', - ); - - // Close the diff editor - vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - - // Reset the refactoring details view - refactoringDetailsViewProvider.resetRefactoringDetails(); - vscode.commands.executeCommand( - 'setContext', - 'refactoringInProgress', - false, - ); - } catch (error) { - console.error('Failed to accept refactoring:', error); - vscode.window.showErrorMessage( - 'Failed to accept refactoring. Please try again.', - ); - } - } else { - vscode.window.showErrorMessage('No refactoring data available.'); - } - }), - ); - - // Register the rejectRefactoring command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', () => { - vscode.window.showInformationMessage( - 'Refactoring rejected! Changes discarded.', - ); - - // Close the diff editor - vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - - // Reset the refactoring details view - refactoringDetailsViewProvider.resetRefactoringDetails(); - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); - }), - ); + // // Register the acceptRefactoring command + // context.subscriptions.push( + // vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => { + // const refactoredFilePath = refactoringDetailsViewProvider.refactoredFilePath; + // const originalFilePath = refactoringDetailsViewProvider.originalFilePath; + + // if (refactoredFilePath && originalFilePath) { + // try { + // // Replace the original file with the refactored file + // fs.copyFileSync(refactoredFilePath, originalFilePath); + // vscode.window.showInformationMessage( + // 'Refactoring accepted! Changes applied.', + // ); + + // // Close the diff editor + // vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + + // // Reset the refactoring details view + // refactoringDetailsViewProvider.resetRefactoringDetails(); + // vscode.commands.executeCommand( + // 'setContext', + // 'refactoringInProgress', + // false, + // ); + // } catch (error) { + // console.error('Failed to accept refactoring:', error); + // vscode.window.showErrorMessage( + // 'Failed to accept refactoring. Please try again.', + // ); + // } + // } else { + // vscode.window.showErrorMessage('No refactoring data available.'); + // } + // }), + // ); + + // // Register the rejectRefactoring command + // context.subscriptions.push( + // vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', () => { + // vscode.window.showInformationMessage( + // 'Refactoring rejected! Changes discarded.', + // ); + + // // Close the diff editor + // vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + + // // Reset the refactoring details view + // refactoringDetailsViewProvider.resetRefactoringDetails(); + // vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + // }), + // ); + // Register the command to open the diff editor // Register the command to open the diff editor context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.openDiffEditor', (fileUri) => { - const refactoredFilePath = refactoringDetailsViewProvider.refactoredFilePath; - const originalFilePath = refactoringDetailsViewProvider.originalFilePath; - - if (refactoredFilePath && originalFilePath) { - // Get the file name from the original file path + vscode.commands.registerCommand( + 'ecooptimizer.openDiffEditor', + (originalFilePath: string, refactoredFilePath: string) => { + // Get the file name for the diff editor title const fileName = originalFilePath.split('/').pop() || 'file'; // Show the diff editor @@ -209,10 +207,8 @@ export function activate(context: vscode.ExtensionContext): void { refactoredUri, `${fileName} (original) ↔ ${fileName} (refactored)`, ); - } else { - vscode.window.showErrorMessage('No refactoring data available.'); - } - }), + }, + ), ); // Register the "Jump to Smell" command. diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index e7e39f3..432af54 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as path from 'path'; export class RefactoringDetailsViewProvider implements vscode.TreeDataProvider @@ -9,8 +10,8 @@ export class RefactoringDetailsViewProvider readonly onDidChangeTreeData = this._onDidChangeTreeData.event; private refactoringDetails: RefactoringDetailItem[] = []; - public originalFilePath: string | undefined; - public refactoredFilePath: string | undefined; // Add this property + public targetFile: { original: string; refactored: string } | undefined; + public affectedFiles: { original: string; refactored: string }[] = []; constructor() { // Initialize with an empty state @@ -18,27 +19,56 @@ export class RefactoringDetailsViewProvider } /** - * Updates the refactoring details with the given file names. - * @param refactoredFilePath - The path of the refactored file. - * @param originalFilePath - The path of the original file. + * Updates the refactoring details with the given target file and affected files. + * @param targetFile - The target file (original and refactored paths). + * @param affectedFiles - The list of affected files (original and refactored paths). */ updateRefactoringDetails( - refactoredFilePath: string, - originalFilePath: string, + targetFile: { original: string; refactored: string }, + affectedFiles: { original: string; refactored: string }[], ): void { - this.refactoredFilePath = refactoredFilePath; // Set the refactored file path - this.originalFilePath = originalFilePath; + this.targetFile = targetFile; + this.affectedFiles = affectedFiles; - // Convert the absolute path of the original file to a relative path for display + // Convert the absolute paths to relative paths for display const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const relativeOriginalPath = workspaceFolder - ? vscode.workspace.asRelativePath(originalFilePath) - : originalFilePath; + const relativeTargetFile = workspaceFolder + ? vscode.workspace.asRelativePath(targetFile.original) + : targetFile.original; - // Update the tree view with only the original file's relative path + const relativeAffectedFiles = affectedFiles.map((file) => + workspaceFolder + ? vscode.workspace.asRelativePath(file.original) + : file.original, + ); + + // Create the tree view items this.refactoringDetails = [ - new RefactoringDetailItem('Original File', relativeOriginalPath), + new RefactoringDetailItem( + path.basename(targetFile.original), // File name as label + 'Target File', // Description + targetFile.original, + targetFile.refactored, + true, // This is a parent item (collapsible) + ), ]; + + // Add affected files as child items + if (affectedFiles.length > 0) { + this.refactoringDetails.push( + ...affectedFiles.map( + (file) => + new RefactoringDetailItem( + path.basename(file.original), // File name as label + 'Affected File', // Description + file.original, + file.refactored, + false, // This is a child item (not collapsible) + ), + ), + ); + } + this._onDidChangeTreeData.fire(undefined); // Refresh the view } @@ -46,8 +76,8 @@ export class RefactoringDetailsViewProvider * Resets the refactoring details to indicate no refactoring is in progress. */ resetRefactoringDetails(): void { - this.refactoredFilePath = undefined; // Reset the refactored file path - this.originalFilePath = undefined; + this.targetFile = undefined; + this.affectedFiles = []; // Clear the tree view this.refactoringDetails = []; @@ -60,22 +90,47 @@ export class RefactoringDetailsViewProvider getChildren(element?: RefactoringDetailItem): RefactoringDetailItem[] { if (element) { - return []; // No nested items + // If this is the parent item (Target File), return the affected files as children + if (element.isParent) { + return this.affectedFiles.map( + (file) => + new RefactoringDetailItem( + path.basename(file.original), // File name as label + 'Affected File', // Description + file.original, + file.refactored, + false, // This is a child item (not collapsible) + ), + ); + } + return []; // No nested items for child items } - return this.refactoringDetails; + // If no element is provided, return the parent item (Target File) + return this.refactoringDetails.filter((item) => item.isParent); } } class RefactoringDetailItem extends vscode.TreeItem { - constructor(label: string, description: string) { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; + constructor( + label: string, // File name + description: string, // "Target File" or "Affected File" + public readonly originalFilePath: string, + public readonly refactoredFilePath: string, + public readonly isParent: boolean = false, // Whether this is a parent item + ) { + super( + label, + isParent + ? vscode.TreeItemCollapsibleState.Collapsed // Parent item is collapsible + : vscode.TreeItemCollapsibleState.None, // Child item is not collapsible + ); + this.description = description; // Set the description - // Add a command to open the diff editor when the item is clicked + // Add a command to open the diff editor for both parent and child items this.command = { command: 'ecooptimizer.openDiffEditor', title: 'Open Diff Editor', - arguments: [description], // Pass the file path as an argument + arguments: [originalFilePath, refactoredFilePath], }; } } From 136b35cfacf61fe15c4ca1b8f36ae4166922f031 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 03:47:17 -0400 Subject: [PATCH 026/121] refactor - fixed diff editor to not open in preview --- package.json | 6 ------ src/commands/refactorSmell.ts | 8 ++++++-- src/extension.ts | 11 +++++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index e935595..58a4124 100644 --- a/package.json +++ b/package.json @@ -304,12 +304,6 @@ "command": "ecooptimizer.refactorSmell", "when": "viewItem == ecoOptimizerSmell", "group": "inline" - }, - { - "command": "ecooptimizer.openDiffEditor", - "when": "viewItem == ecoOptimizerFile", - "group": "inline", - "icon": "$(diff)" } ] } diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 44d7b40..1947227 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; +import path from 'path'; /** * Handles the refactoring of a specific smell. @@ -37,14 +38,17 @@ export async function refactorSmell( // Show a diff view for the target file const targetFile = refactoredData.targetFile; - const fileName = targetFile.original.split('/').pop() || 'file'; + const fileName = path.basename(targetFile.original); const originalUri = vscode.Uri.file(targetFile.original); const refactoredUri = vscode.Uri.file(targetFile.refactored); await vscode.commands.executeCommand( 'vscode.diff', originalUri, refactoredUri, - `${fileName} (original) ↔ ${fileName} (refactored)`, + `Refactoring Comparison (${fileName})`, + { + preview: false, + }, ); // Set a context key to track that refactoring is in progress diff --git a/src/extension.ts b/src/extension.ts index f46780a..86fbcc9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,6 +17,7 @@ import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; import { refactorSmell } from './commands/refactorSmell'; import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; +import path from 'path'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -189,23 +190,25 @@ export function activate(context: vscode.ExtensionContext): void { // }), // ); - // Register the command to open the diff editor // Register the command to open the diff editor context.subscriptions.push( vscode.commands.registerCommand( 'ecooptimizer.openDiffEditor', (originalFilePath: string, refactoredFilePath: string) => { // Get the file name for the diff editor title - const fileName = originalFilePath.split('/').pop() || 'file'; + const fileName = path.basename(originalFilePath); - // Show the diff editor + // Show the diff editor with the updated title const originalUri = vscode.Uri.file(originalFilePath); const refactoredUri = vscode.Uri.file(refactoredFilePath); vscode.commands.executeCommand( 'vscode.diff', originalUri, refactoredUri, - `${fileName} (original) ↔ ${fileName} (refactored)`, + `Refactoring Comparison (${fileName})`, + { + preview: false, + }, ); }, ), From ded38e75da88d9b4ffecac60843ecf5d48396eb3 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 04:06:42 -0400 Subject: [PATCH 027/121] added energy savings to tree items --- src/commands/refactorSmell.ts | 5 +- .../RefactoringDetailsViewProvider.ts | 98 ++++++++++--------- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 1947227..ffed663 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -30,10 +30,11 @@ export async function refactorSmell( // Log the response from the backend console.log('Refactoring response:', refactoredData); - // Update the refactoring details view with the target file and affected files + // Update the refactoring details view with the target file, affected files, and energy saved refactoringDetailsViewProvider.updateRefactoringDetails( refactoredData.targetFile, refactoredData.affectedFiles, + refactoredData.energySaved, // Pass the energy saved value ); // Show a diff view for the target file @@ -47,7 +48,7 @@ export async function refactorSmell( refactoredUri, `Refactoring Comparison (${fileName})`, { - preview: false, + preview: false, // Ensure the diff editor is not in preview mode }, ); diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 432af54..0a464df 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -12,38 +12,46 @@ export class RefactoringDetailsViewProvider private refactoringDetails: RefactoringDetailItem[] = []; public targetFile: { original: string; refactored: string } | undefined; public affectedFiles: { original: string; refactored: string }[] = []; + public energySaved: number | null = null; // Add energySaved as a class property constructor() { - // Initialize with an empty state this.resetRefactoringDetails(); } /** - * Updates the refactoring details with the given target file and affected files. + * Updates the refactoring details with the given target file, affected files, and energy saved. * @param targetFile - The target file (original and refactored paths). * @param affectedFiles - The list of affected files (original and refactored paths). + * @param energySaved - The amount of energy saved in kg CO2. */ updateRefactoringDetails( targetFile: { original: string; refactored: string }, affectedFiles: { original: string; refactored: string }[], + energySaved: number | null, ): void { this.targetFile = targetFile; this.affectedFiles = affectedFiles; + this.energySaved = energySaved; - // Convert the absolute paths to relative paths for display - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const relativeTargetFile = workspaceFolder - ? vscode.workspace.asRelativePath(targetFile.original) - : targetFile.original; + // Clear the existing refactoring details + this.refactoringDetails = []; - const relativeAffectedFiles = affectedFiles.map((file) => - workspaceFolder - ? vscode.workspace.asRelativePath(file.original) - : file.original, - ); + // Add energy saved as the first item + if (energySaved !== null) { + this.refactoringDetails.push( + new RefactoringDetailItem( + `Energy Saved: ${energySaved} kg CO2`, // Label + '', // No description + '', // No file path + '', // No file path + false, // Not collapsible + true, // Special item for energy saved + ), + ); + } - // Create the tree view items - this.refactoringDetails = [ + // Add the target file + this.refactoringDetails.push( new RefactoringDetailItem( path.basename(targetFile.original), // File name as label 'Target File', // Description @@ -51,23 +59,10 @@ export class RefactoringDetailsViewProvider targetFile.refactored, true, // This is a parent item (collapsible) ), - ]; + ); - // Add affected files as child items - if (affectedFiles.length > 0) { - this.refactoringDetails.push( - ...affectedFiles.map( - (file) => - new RefactoringDetailItem( - path.basename(file.original), // File name as label - 'Affected File', // Description - file.original, - file.refactored, - false, // This is a child item (not collapsible) - ), - ), - ); - } + // Do not add affected files to refactoringDetails here + // They will be added dynamically in getChildren when the parent item is expanded this._onDidChangeTreeData.fire(undefined); // Refresh the view } @@ -78,8 +73,7 @@ export class RefactoringDetailsViewProvider resetRefactoringDetails(): void { this.targetFile = undefined; this.affectedFiles = []; - - // Clear the tree view + this.energySaved = null; this.refactoringDetails = []; this._onDidChangeTreeData.fire(undefined); // Refresh the view } @@ -105,32 +99,44 @@ export class RefactoringDetailsViewProvider } return []; // No nested items for child items } - // If no element is provided, return the parent item (Target File) - return this.refactoringDetails.filter((item) => item.isParent); + // If no element is provided, return the top-level items (Energy Saved and Target File) + return this.refactoringDetails; } } class RefactoringDetailItem extends vscode.TreeItem { constructor( - label: string, // File name - description: string, // "Target File" or "Affected File" + label: string, + description: string, public readonly originalFilePath: string, public readonly refactoredFilePath: string, - public readonly isParent: boolean = false, // Whether this is a parent item + public readonly isParent: boolean = false, + public readonly isEnergySaved: boolean = false, ) { super( label, isParent - ? vscode.TreeItemCollapsibleState.Collapsed // Parent item is collapsible - : vscode.TreeItemCollapsibleState.None, // Child item is not collapsible + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None, ); - this.description = description; // Set the description + this.description = description; + + // Customize the icon for the Energy Saved item + if (isEnergySaved) { + this.iconPath = new vscode.ThemeIcon( + 'lightbulb', // Use a lightbulb icon for energy saved + new vscode.ThemeColor('charts.yellow'), + ); + this.tooltip = 'This is the amount of energy saved by refactoring.'; + } - // Add a command to open the diff editor for both parent and child items - this.command = { - command: 'ecooptimizer.openDiffEditor', - title: 'Open Diff Editor', - arguments: [originalFilePath, refactoredFilePath], - }; + // Add a command to open the diff editor for file items (not energy saved) + if (!isEnergySaved) { + this.command = { + command: 'ecooptimizer.openDiffEditor', + title: 'Open Diff Editor', + arguments: [originalFilePath, refactoredFilePath], + }; + } } } From 44a84880145947d96ce60e7db1c5cc66641023c4 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 04:59:38 -0400 Subject: [PATCH 028/121] add to focus on view --- src/commands/refactorSmell.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index ffed663..3487d25 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -55,6 +55,9 @@ export async function refactorSmell( // Set a context key to track that refactoring is in progress vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); + // Focus on the Refactoring Details view + await vscode.commands.executeCommand('ecooptimizer.refactoringDetails.focus'); + // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, From 7d12294ce4c0e97ab0d9d448d244daf46bc209bb Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 05:16:05 -0400 Subject: [PATCH 029/121] added accept and reject buttons --- src/commands/refactorSmell.ts | 62 +++++++++++++++++++++++++++++++ src/extension.ts | 70 +++++++++-------------------------- 2 files changed, 80 insertions(+), 52 deletions(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 3487d25..1d7d16d 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -3,6 +3,7 @@ import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import path from 'path'; +import * as fs from 'fs'; /** * Handles the refactoring of a specific smell. @@ -71,3 +72,64 @@ export async function refactorSmell( vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } + +/** + * Accepts the refactoring changes and saves the refactored files. + */ +export async function acceptRefactoring( + refactoringDetailsViewProvider: RefactoringDetailsViewProvider, +) { + const targetFile = refactoringDetailsViewProvider.targetFile; + const affectedFiles = refactoringDetailsViewProvider.affectedFiles; + + if (!targetFile || !affectedFiles) { + vscode.window.showErrorMessage('No refactoring data available.'); + return; + } + + try { + // Save the refactored target file + fs.copyFileSync(targetFile.refactored, targetFile.original); + + // Save the refactored affected files + for (const file of affectedFiles) { + fs.copyFileSync(file.refactored, file.original); + } + + // Notify the user + vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); + + // Reset the refactoring details view + refactoringDetailsViewProvider.resetRefactoringDetails(); + + // Close all diff editors + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + + // Set the context key to indicate refactoring is no longer in progress + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + } catch (error) { + console.error('Failed to accept refactoring:', error); + vscode.window.showErrorMessage( + 'Failed to accept refactoring. Please try again.', + ); + } +} + +/** + * Rejects the refactoring changes and keeps the original files. + */ +export async function rejectRefactoring( + refactoringDetailsViewProvider: RefactoringDetailsViewProvider, +) { + // Notify the user + vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); + + // Reset the refactoring details view + refactoringDetailsViewProvider.resetRefactoringDetails(); + + // Close all diff editors + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + + // Set the context key to indicate refactoring is no longer in progress + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); +} diff --git a/src/extension.ts b/src/extension.ts index 86fbcc9..9ee6690 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,11 @@ import { checkServerStatus } from './api/backend'; import { FilterViewProvider } from './providers/FilterViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; -import { refactorSmell } from './commands/refactorSmell'; +import { + acceptRefactoring, + refactorSmell, + rejectRefactoring, +} from './commands/refactorSmell'; import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; import path from 'path'; @@ -138,57 +142,19 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // // Register the acceptRefactoring command - // context.subscriptions.push( - // vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => { - // const refactoredFilePath = refactoringDetailsViewProvider.refactoredFilePath; - // const originalFilePath = refactoringDetailsViewProvider.originalFilePath; - - // if (refactoredFilePath && originalFilePath) { - // try { - // // Replace the original file with the refactored file - // fs.copyFileSync(refactoredFilePath, originalFilePath); - // vscode.window.showInformationMessage( - // 'Refactoring accepted! Changes applied.', - // ); - - // // Close the diff editor - // vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - - // // Reset the refactoring details view - // refactoringDetailsViewProvider.resetRefactoringDetails(); - // vscode.commands.executeCommand( - // 'setContext', - // 'refactoringInProgress', - // false, - // ); - // } catch (error) { - // console.error('Failed to accept refactoring:', error); - // vscode.window.showErrorMessage( - // 'Failed to accept refactoring. Please try again.', - // ); - // } - // } else { - // vscode.window.showErrorMessage('No refactoring data available.'); - // } - // }), - // ); - - // // Register the rejectRefactoring command - // context.subscriptions.push( - // vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', () => { - // vscode.window.showInformationMessage( - // 'Refactoring rejected! Changes discarded.', - // ); - - // // Close the diff editor - // vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - - // // Reset the refactoring details view - // refactoringDetailsViewProvider.resetRefactoringDetails(); - // vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); - // }), - // ); + // Register the acceptRefactoring command + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => + acceptRefactoring(refactoringDetailsViewProvider), + ), + ); + + // Register the rejectRefactoring command + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', () => + rejectRefactoring(refactoringDetailsViewProvider), + ), + ); // Register the command to open the diff editor context.subscriptions.push( From d18460251f2792052537df06329b21140e7fe538 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 12:20:25 -0400 Subject: [PATCH 030/121] make files outdated after refactoring --- src/commands/refactorSmell.ts | 28 ++++++++++++++++++++++++++++ src/extension.ts | 6 +++++- src/managers/SmellsViewUIManager.ts | 11 +++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 1d7d16d..b7c9506 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -2,9 +2,15 @@ import * as vscode from 'vscode'; import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; import path from 'path'; import * as fs from 'fs'; +function normalizePath(filePath: string): string { + const normalizedPath = filePath.toLowerCase(); // Normalize case for consistent Map keying + return normalizedPath; +} + /** * Handles the refactoring of a specific smell. * @@ -75,9 +81,12 @@ export async function refactorSmell( /** * Accepts the refactoring changes and saves the refactored files. + * Marks the modified files as outdated and clears their smell cache. */ export async function acceptRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, + smellsCacheManager: SmellsCacheManager, + smellsViewProvider: SmellsViewProvider, ) { const targetFile = refactoringDetailsViewProvider.targetFile; const affectedFiles = refactoringDetailsViewProvider.affectedFiles; @@ -99,6 +108,22 @@ export async function acceptRefactoring( // Notify the user vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); + // Clear the smell cache for the target file and affected files + await smellsCacheManager.clearCachedSmellsForFile( + normalizePath(targetFile.original), + ); + for (const file of affectedFiles) { + await smellsCacheManager.clearCachedSmellsForFile( + normalizePath(file.original), + ); + } + + // Mark the target file and affected files as outdated + smellsViewProvider.markFileAsOutdated(normalizePath(targetFile.original)); + for (const file of affectedFiles) { + smellsViewProvider.markFileAsOutdated(normalizePath(file.original)); + } + // Reset the refactoring details view refactoringDetailsViewProvider.resetRefactoringDetails(); @@ -107,6 +132,9 @@ export async function acceptRefactoring( // Set the context key to indicate refactoring is no longer in progress vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + + // Refresh the UI to reflect the outdated status of the modified files + smellsViewProvider.refresh(); } catch (error) { console.error('Failed to accept refactoring:', error); vscode.window.showErrorMessage( diff --git a/src/extension.ts b/src/extension.ts index 9ee6690..76104c4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -145,7 +145,11 @@ export function activate(context: vscode.ExtensionContext): void { // Register the acceptRefactoring command context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => - acceptRefactoring(refactoringDetailsViewProvider), + acceptRefactoring( + refactoringDetailsViewProvider, + smellsCacheManager, + smellsViewProvider, + ), ), ); diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts index 1899d7e..e0dbb20 100644 --- a/src/managers/SmellsViewUIManager.ts +++ b/src/managers/SmellsViewUIManager.ts @@ -196,6 +196,13 @@ export class SmellsUIManager { 'server-process', new vscode.ThemeColor('charts.red'), ); + case 'refactoring': + return new vscode.ThemeIcon('robot', new vscode.ThemeColor('charts.purple')); + case 'accept-refactoring': + return new vscode.ThemeIcon( + 'warning', + new vscode.ThemeColor('charts.yellow'), + ); default: return new vscode.ThemeIcon('circle-outline'); } @@ -221,6 +228,10 @@ export class SmellsUIManager { return 'File Outdated - Needs Reanalysis'; case 'server_down': return 'Server Unavailable'; + case 'refactoring': + return 'Refactoring Currently Ongoing'; + case 'accept-refactoring': + return 'Successfully Refactored - Needs Reanalysis'; default: return 'Smells Not Yet Detected'; } From c4e22986eff9558861bcc3fcf1d0199954f319f1 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:26:27 -0400 Subject: [PATCH 031/121] Add energy metrics dashboard fixes ssm-lab/capstone--source-code-optimizer#428 --- .env | 1 + package.json | 20 ++ src/commands/detectSmells.ts | 6 +- src/commands/exportMetricsData.ts | 58 +++++ src/commands/refactorSmell.ts | 14 +- src/extension.ts | 31 ++- src/providers/FilterViewProvider.ts | 8 +- src/providers/MetricsViewProvider.ts | 326 +++++++++++++++++++++++++++ src/utils/envConfig.ts | 2 + src/utils/smellsData.ts | 35 ++- 10 files changed, 486 insertions(+), 15 deletions(-) create mode 100644 src/commands/exportMetricsData.ts create mode 100644 src/providers/MetricsViewProvider.ts diff --git a/.env b/.env index f668a4c..779967a 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ SERVER_URL='127.0.0.1:8000' FILE_HASH_CACHE_KEY='fileHashCache' SMELL_CACHE_KEY='smellCache' +WORKSPACE_METRICS_DATA='metricsData' diff --git a/package.json b/package.json index 0fa5dd9..9cf246a 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,11 @@ "id": "ecooptimizer.refactoringDetails", "name": "Refactoring Details", "icon": "assets/eco-icon.png" + }, + { + "id": "ecooptimizer.metricsView", + "name": "Carbon Metrics", + "icon": "assets/eco-icon.png" } ] }, @@ -150,6 +155,11 @@ "view": "ecooptimizer.view", "contents": "No code smells detected yet. Configure your workspace to start analysis.\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", "when": "!workspaceState.workspaceConfigured" + }, + { + "view": "ecooptimizer.metricsView", + "contents": "No energy savings to declare. Configure your workspace to start saving energy!\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", + "when": "!workspaceState.workspaceConfigured" } ], "commands": [ @@ -189,6 +199,11 @@ "title": "Open File", "category": "Eco Optimizer" }, + { + "command": "ecooptimizer.exportMetricsData", + "title": "Export Metrics Data as JSON", + "category": "Eco Optimizer" + }, { "command": "ecooptimizer.detectSmellsFolder", "title": "Detect Smells for All Files", @@ -245,6 +260,11 @@ "command": "ecooptimizer.deselectAllFilterSmells", "when": "view == ecooptimizer.filterView", "group": "resource" + }, + { + "command": "ecooptimizer.exportMetricsData", + "when": "view == ecooptimizer.metricsView", + "group": "resource" } ], "view/item/context": [ diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index dbb4e0f..cc645d7 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -19,7 +19,7 @@ export async function detectSmellsFile( smellsCacheManager: SmellsCacheManager, treeDataProvider: SmellsViewProvider, fileUri: vscode.Uri | string, -) { +): Promise { // Validate the file URI or path if (!fileUri) { vscode.window.showErrorMessage('No file selected for analysis.'); @@ -125,7 +125,7 @@ export async function detectSmellsFolder( smellsCacheManager: SmellsCacheManager, treeDataProvider: SmellsViewProvider, folderPath: string, -) { +): Promise { // Notify the user that folder analysis has started vscode.window.showInformationMessage( `Detecting code smells for all Python files in: ${path.basename(folderPath)}`, @@ -177,7 +177,7 @@ async function handleOutdatedFile( filePath: string, smellsCacheManager: SmellsCacheManager, smellsDisplayProvider: SmellsViewProvider, -) { +): Promise { // Check if the file is marked as outdated if (smellsDisplayProvider.isFileOutdated(filePath)) { // Delete cached smells for the outdated file diff --git a/src/commands/exportMetricsData.ts b/src/commands/exportMetricsData.ts new file mode 100644 index 0000000..3a1b91f --- /dev/null +++ b/src/commands/exportMetricsData.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode'; +import { dirname } from 'path'; +import { writeFileSync } from 'fs'; + +import { MetricsDataItem } from '../providers/MetricsViewProvider'; +import { envConfig } from '../utils/envConfig'; + +export async function exportMetricsData( + context: vscode.ExtensionContext, +): Promise { + const metricsData = context.workspaceState.get<{ + [path: string]: MetricsDataItem; + }>(envConfig.WORKSPACE_METRICS_DATA!, {}); + + if (Object.keys(metricsData).length === 0) { + vscode.window.showInformationMessage('No metrics data available to export.'); + return; + } + + const configuredWorkspacePath = context.workspaceState.get( + 'workspaceConfiguredPath', + ); + + if (!configuredWorkspacePath) { + vscode.window.showErrorMessage('No configured workspace path found.'); + return; + } + + const workspaceUri = vscode.Uri.file(configuredWorkspacePath); + let fileUri: vscode.Uri; + + try { + const stat = await vscode.workspace.fs.stat(workspaceUri); + + if (stat.type === vscode.FileType.Directory) { + fileUri = vscode.Uri.joinPath(workspaceUri, 'metrics-data.json'); + } else if (stat.type === vscode.FileType.File) { + const parentDir = vscode.Uri.file(dirname(configuredWorkspacePath)); + fileUri = vscode.Uri.joinPath(parentDir, 'metrics-data.json'); + } else { + vscode.window.showErrorMessage('Invalid workspace path type.'); + return; + } + } catch (error) { + vscode.window.showErrorMessage(`Failed to access workspace path: ${error}`); + return; + } + + try { + const jsonData = JSON.stringify(metricsData, null, 2); + writeFileSync(fileUri.fsPath, jsonData, 'utf-8'); + vscode.window.showInformationMessage( + `Metrics data exported successfully to ${fileUri.fsPath}`, + ); + } catch (error) { + vscode.window.showErrorMessage(`Failed to export metrics data: ${error}`); + } +} diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index bc30576..e0e8236 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { refactorSmell as backendRefactorSmell } from '../api/backend'; // Import the backend function +import { MetricsViewProvider } from '../providers/MetricsViewProvider'; /** * Handles the refactoring of a specific smell in a file. @@ -13,11 +14,12 @@ import { refactorSmell as backendRefactorSmell } from '../api/backend'; // Impor * @param smell - The smell to refactor. */ export async function refactorSmell( - treeDataProvider: SmellsViewProvider, + smellsDataProvider: SmellsViewProvider, + metricsDataProvider: MetricsViewProvider, refactoringDetailsViewProvider: RefactoringDetailsViewProvider, filePath: string, smell: Smell, -) { +): Promise { if (!filePath || !smell) { vscode.window.showErrorMessage('Error: Invalid file path or smell.'); return; @@ -39,6 +41,14 @@ export async function refactorSmell( refactoredData.targetFile.refactored, ); + if (refactoredData.energySaved) { + metricsDataProvider.updateMetrics( + filePath, + refactoredData.energySaved, + smell.symbol, + ); + } + // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, diff --git a/src/extension.ts b/src/extension.ts index 94dacc2..c04759a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,16 +7,23 @@ import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; import { openFile } from './commands/openFile'; +import { exportMetricsData } from './commands/exportMetricsData'; import { registerFilterSmellCommands } from './commands/filterSmells'; import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; +import { refactorSmell } from './commands/refactorSmell'; + import { SmellsViewProvider } from './providers/SmellsViewProvider'; -import { checkServerStatus } from './api/backend'; import { FilterViewProvider } from './providers/FilterViewProvider'; +import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; +import { MetricsViewProvider } from './providers/MetricsViewProvider'; + import { SmellsCacheManager } from './context/SmellsCacheManager'; + import { registerFileSaveListener } from './listeners/fileSaveListener'; -import { refactorSmell } from './commands/refactorSmell'; -import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; + +import { checkServerStatus } from './api/backend'; +import { loadSmells } from './utils/smellsData'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -25,6 +32,8 @@ import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsVi export function activate(context: vscode.ExtensionContext): void { console.log('Activating Eco-Optimizer extension...'); + loadSmells(); + // Initialize the SmellsCacheManager for managing caching of smells and file hashes. const smellsCacheManager = new SmellsCacheManager(context); @@ -35,6 +44,12 @@ export function activate(context: vscode.ExtensionContext): void { }); context.subscriptions.push(codeSmellsView); + const metricsViewProvider = new MetricsViewProvider(context); + vscode.window.createTreeView('ecooptimizer.metricsView', { + treeDataProvider: metricsViewProvider, + showCollapseAll: true, + }); + // Start periodic backend status checks (every 10 seconds). checkServerStatus(); setInterval(checkServerStatus, 10000); @@ -67,6 +82,7 @@ export function activate(context: vscode.ExtensionContext): void { context, smellsCacheManager, smellsViewProvider, + metricsViewProvider, ); const filterSmellsView = vscode.window.createTreeView('ecooptimizer.filterView', { treeDataProvider: filterSmellsProvider, @@ -109,6 +125,7 @@ export function activate(context: vscode.ExtensionContext): void { // Initialize the RefactoringDetailsViewProvider const refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); + // eslint-disable-next-line unused-imports/no-unused-vars const refactoringDetailsView = vscode.window.createTreeView( 'ecooptimizer.refactoringDetails', { @@ -116,6 +133,13 @@ export function activate(context: vscode.ExtensionContext): void { }, ); + // Register the export command + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.exportMetricsData', () => + exportMetricsData(context), + ), + ); + // Reset the refactoring details view initially refactoringDetailsViewProvider.resetRefactoringDetails(); @@ -154,6 +178,7 @@ export function activate(context: vscode.ExtensionContext): void { // Call the refactorSmell function refactorSmell( smellsViewProvider, + metricsViewProvider, refactoringDetailsViewProvider, filePath, smell, diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index 5c16c80..7fee9f3 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; -import { FilterSmellConfig, loadSmells, saveSmells } from '../utils/smellsData'; +import { FilterSmellConfig, getFilterSmells, saveSmells } from '../utils/smellsData'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from './SmellsViewProvider'; +import { MetricsViewProvider } from './MetricsViewProvider'; /** * Provides a tree view for filtering code smells within the VS Code extension. @@ -20,8 +21,9 @@ export class FilterViewProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + constructor(private context: vscode.ExtensionContext) {} + + refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + getTreeItem(element: MetricItem): vscode.TreeItem { + return element; + } + + async getChildren(element?: MetricItem): Promise { + const metricsData = this.context.workspaceState.get<{ + [path: string]: MetricsDataItem; + }>(envConfig.WORKSPACE_METRICS_DATA!, {}); + + if (!element) { + // Root level: Show configured folder/file + const configuredPath = this.context.workspaceState.get( + 'workspaceConfiguredPath', + ); + if (!configuredPath) return []; + + const isDirectory = + fs.existsSync(configuredPath) && fs.statSync(configuredPath).isDirectory(); + if (isDirectory) { + return [this.createFolderItem(configuredPath)]; + } else { + return [this.createFileItem(configuredPath, metricsData)]; + } + } + + if (element.contextValue === 'folder') { + // Show folder stats and contents + const folderPath = element.resourceUri!.fsPath; + const folderContents = await this.getFolderContents(folderPath); + const folderMetrics = await this.calculateFolderMetrics( + folderContents, + metricsData, + ); + + // Folder stats + const folderStats = [ + new MetricItem( + `Total Carbon Saved: ${folderMetrics.totalCarbonSaved.toFixed(2)} kg`, + vscode.TreeItemCollapsibleState.None, + 'folder-stats', + ), + ...folderMetrics.smellData.map((data) => this.createSmellItem(data)), + ]; + + // Folder contents (subfolders and files) + const contents = await Promise.all( + folderContents.map((item) => { + if (item.type === 'folder') { + return this.createFolderItem(item.path); + } else { + return this.createFileItem(item.path, metricsData); + } + }), + ); + + return [...folderStats, ...contents]; + } + + if (element.contextValue === 'file') { + // Show smells in the file + const filePath = element.resourceUri!.fsPath; + const fileMetrics = this.calculateFileMetrics(filePath, metricsData); + return fileMetrics.smellData.map((data) => this.createSmellItem(data)); + } + + return []; + } + + private createFolderItem(folderPath: string): MetricItem { + return new MetricItem( + basename(folderPath), + vscode.TreeItemCollapsibleState.Collapsed, + 'folder', + undefined, + vscode.Uri.file(folderPath), // resourceUri + ); + } + + private createFileItem( + filePath: string, + metricsData: { [path: string]: MetricsDataItem }, + ): MetricItem { + const fileMetrics = this.calculateFileMetrics(filePath, metricsData); + return new MetricItem( + basename(filePath), + vscode.TreeItemCollapsibleState.Collapsed, + 'file', + fileMetrics.totalCarbonSaved, + vscode.Uri.file(filePath), + ); + } + + private createSmellItem(data: { + acronym: string; + name: string; + carbonSaved: number; + }): MetricItem { + return new MetricItem( + `${data.acronym}: ${data.carbonSaved.toFixed(2)} kg`, + vscode.TreeItemCollapsibleState.None, + 'smell', + undefined, + undefined, + data.name, + ); + } + + /** + * Retrieves the contents of a folder (subfolders and files). + * @param folderPath - The path of the folder. + */ + private async getFolderContents( + folderPath: string, + ): Promise> { + try { + const folderUri = vscode.Uri.file(folderPath); + const directoryEntries = await vscode.workspace.fs.readDirectory(folderUri); + + const contents: Array<{ + type: 'folder' | 'file'; + name: string; + path: string; + }> = []; + for (const [name, type] of directoryEntries) { + const fullPath = vscode.Uri.joinPath(folderUri, name).fsPath; + if (type === vscode.FileType.Directory) { + contents.push({ type: 'folder', name, path: fullPath }); + } else if (type === vscode.FileType.File && name.endsWith('.py')) { + contents.push({ type: 'file', name, path: fullPath }); + } + } + + return contents; + } catch (error) { + console.error(`Failed to read directory ${folderPath}:`, error); + return []; + } + } + + /** + * Calculates the carbon saved for a specific folder dynamically. + * @param folderContents - The contents of the folder. + * @param metricsData - The metrics data from the workspace state. + */ + private async calculateFolderMetrics( + folderContents: Array<{ type: 'folder' | 'file'; name: string; path: string }>, + metricsData: { [path: string]: MetricsDataItem }, + ): Promise<{ + totalCarbonSaved: number; + smellData: { acronym: string; name: string; carbonSaved: number }[]; + }> { + let totalCarbonSaved = 0; + const smellDistribution = new Map(); + + for (const item of folderContents) { + if (item.type === 'file') { + const fileMetrics = this.calculateFileMetrics(item.path, metricsData); + totalCarbonSaved += fileMetrics.totalCarbonSaved; + + for (const smellData of fileMetrics.smellData) { + const currentCarbonSaved = + smellDistribution.get(smellData.acronym)?.[1] || 0; + smellDistribution.set(smellData.acronym, [ + smellData.name, + currentCarbonSaved + smellData.carbonSaved, + ]); + } + } + } + + return { + totalCarbonSaved, + smellData: Array.from(smellDistribution.entries()).map( + ([acronym, [name, carbonSaved]]) => ({ + acronym, + name, + carbonSaved, + }), + ), + }; + } + + /** + * Calculates the carbon saved for a specific file. + * @param filePath - The path of the file. + * @param metricsData - The metrics data from the workspace state. + */ + private calculateFileMetrics( + filePath: string, + metricsData: { [path: string]: MetricsDataItem }, + ): { + totalCarbonSaved: number; + smellData: { acronym: string; name: string; carbonSaved: number }[]; + } { + const smellConfigData = getFilterSmells(); + const fileData = metricsData[filePath] || { + totalCarbonSaved: 0, + smellDistribution: {}, + }; + + // Initialize smell distribution with only enabled smells, defaulting to 0 + const smellDistribution = Object.keys(smellConfigData).reduce( + (acc, symbol) => { + if (smellConfigData[symbol]) { + acc[symbol] = fileData.smellDistribution[symbol] || 0; + } + return acc; + }, + {} as Record, + ); + + return { + totalCarbonSaved: fileData.totalCarbonSaved, + smellData: Object.entries(smellDistribution).map(([symbol, carbonSaved]) => ({ + acronym: smellConfigData[symbol].acronym, + name: smellConfigData[symbol].name, + carbonSaved, + })), + }; + } + + /** + * Updates the metrics view when a smell is refactored. + * @param filePath - The path of the refactored file. + * @param carbonSaved - The amount of carbon saved in kg. + */ + updateMetrics(filePath: string, carbonSaved: number, smellSymbol: string): void { + const metrics = this.context.workspaceState.get<{ + [path: string]: MetricsDataItem; + }>(envConfig.WORKSPACE_METRICS_DATA!, {}); + + // Get the acronym for the smell + const acronym = getEnabledSmells()[smellSymbol].acronym; + + if (!metrics[filePath]) { + metrics[filePath] = { + totalCarbonSaved: 0, + smellDistribution: {}, + }; + } + + metrics[filePath].totalCarbonSaved = + (metrics[filePath].totalCarbonSaved || 0) + carbonSaved; + + if (!metrics[filePath].smellDistribution[acronym]) { + metrics[filePath].smellDistribution[acronym] = 0; + } + metrics[filePath].smellDistribution[acronym] += carbonSaved; + + this.context.workspaceState.update(envConfig.WORKSPACE_METRICS_DATA!, metrics); + + this.refresh(); + } +} + +/** + * Represents a metric item in the tree view. + */ +class MetricItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly contextValue: string, + public readonly carbonSaved?: number, + public readonly resourceUri?: vscode.Uri, // For file/folder paths + public readonly smellName?: string, // For smell names + ) { + super(label, collapsibleState); + + // Set icon based on contextValue + switch (this.contextValue) { + case 'folder': + this.iconPath = new vscode.ThemeIcon('folder'); // Built-in folder icon + break; + case 'file': + this.iconPath = new vscode.ThemeIcon('file'); // Built-in file icon + break; + case 'smell': + this.iconPath = new vscode.ThemeIcon('tag'); // Built-in warning icon + break; + case 'folder-stats': + this.iconPath = new vscode.ThemeIcon('graph'); // Optional stats icon + break; + } + + // Set description for carbon saved + this.description = + carbonSaved !== undefined ? `Carbon Saved: ${carbonSaved.toFixed(2)} kg` : ''; + this.tooltip = this.description; + + this.tooltip = smellName !== undefined ? smellName : ''; + + if (resourceUri && contextValue === 'file') { + this.resourceUri = resourceUri; + this.command = { + title: 'Open File', + command: 'vscode.open', + arguments: [resourceUri], + }; + } + } +} + +export interface MetricsDataItem { + totalCarbonSaved: number; + smellDistribution: { + [smell: string]: number; + }; +} diff --git a/src/utils/envConfig.ts b/src/utils/envConfig.ts index 7b98c34..f3dc8ec 100644 --- a/src/utils/envConfig.ts +++ b/src/utils/envConfig.ts @@ -6,10 +6,12 @@ export interface EnvConfig { SERVER_URL?: string; SMELL_CACHE_KEY?: string; FILE_HASH_CACHE_KEY?: string; + WORKSPACE_METRICS_DATA?: string; } export const envConfig: EnvConfig = { SERVER_URL: process.env.SERVER_URL, SMELL_CACHE_KEY: process.env.SMELL_CACHE_KEY, FILE_HASH_CACHE_KEY: process.env.FILE_HASH_CACHE_KEY, + WORKSPACE_METRICS_DATA: process.env.WORKSPACE_METRICS_DATA, }; diff --git a/src/utils/smellsData.ts b/src/utils/smellsData.ts index 14b6e72..92102a1 100644 --- a/src/utils/smellsData.ts +++ b/src/utils/smellsData.ts @@ -26,28 +26,32 @@ interface DetectSmellConfig { options: Record; } +let filterSmells: Record; +let enabledSmells: Record; + /** * Loads the full smells configuration from smells.json. * @returns A dictionary of smells with their respective configuration. */ -export function loadSmells(): Record { +export function loadSmells(): void { const filePath = path.join(__dirname, '..', 'data', 'smells.json'); if (!fs.existsSync(filePath)) { vscode.window.showErrorMessage( 'Configuration file missing: smells.json could not be found.', ); - return {}; } try { - return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + filterSmells = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + enabledSmells = parseSmells(filterSmells); + + console.log('Smells loaded'); } catch (error) { vscode.window.showErrorMessage( 'Error loading smells.json. Please check the file format.', ); console.error('ERROR: Failed to parse smells.json', error); - return {}; } } @@ -56,22 +60,43 @@ export function loadSmells(): Record { * @param smells - The smells data to be saved. */ export function saveSmells(smells: Record): void { + filterSmells = smells; + const filePath = path.join(__dirname, '..', 'data', 'smells.json'); try { fs.writeFileSync(filePath, JSON.stringify(smells, null, 2)); + + enabledSmells = parseSmells(filterSmells); } catch (error) { vscode.window.showErrorMessage('Error saving smells.json.'); console.error('ERROR: Failed to write smells.json', error); } } +/** + * Extracts raw smells data from the loaded configuration. + * @returns A dictionary of smell config data for smell filtering. + */ +export function getFilterSmells(): Record { + return filterSmells; +} + /** * Extracts enabled smells from the loaded configuration. * @returns A dictionary of enabled smells formatted for backend processing. */ export function getEnabledSmells(): Record { - const smells = loadSmells(); + return enabledSmells; +} +/** + * Parses the raw smells into a formatted object. + * @param smells - The smells data to be saved. + * @returns A dictionary of enabled smells formatted for backend processing. + */ +function parseSmells( + smells: Record, +): Record { return Object.fromEntries( Object.entries(smells) .filter(([, smell]) => smell.enabled) From f93b106e827488721902b2e18c642523fcde9fa8 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:22:03 -0400 Subject: [PATCH 032/121] Update energy metrics dashboard - Added clear metrics command - Added manual refresh command - Added listeneer for workspace changes - Properly integrated dashboard with refactorSmell - Properly formatted carbon values to use scientific notation - Sorted tree items to show folders first then files fixes ssm-lab/capstone--source-code-optimizer#428 --- package.json | 26 ++- src/commands/refactorSmell.ts | 35 ++-- src/extension.ts | 48 +++++- src/global.d.ts | 2 +- src/listeners/workspaceModifiedListener.ts | 34 ++++ src/providers/MetricsViewProvider.ts | 152 +++++++++++------- .../RefactoringDetailsViewProvider.ts | 12 +- 7 files changed, 216 insertions(+), 93 deletions(-) create mode 100644 src/listeners/workspaceModifiedListener.ts diff --git a/package.json b/package.json index 9d69340..de28c8c 100644 --- a/package.json +++ b/package.json @@ -143,11 +143,6 @@ "name": "Filter Smells", "icon": "assets/eco-icon.png" }, - { - "id": "ecooptimizer.refactoringDetails", - "name": "Refactoring Details", - "icon": "assets/eco-icon.png" - }, { "id": "ecooptimizer.metricsView", "name": "Carbon Metrics", @@ -214,6 +209,17 @@ "title": "Export Metrics Data as JSON", "category": "Eco Optimizer" }, + { + "command": "ecooptimizer.clearMetricsData", + "title": "Clear Metrics Data", + "category": "Eco Optimizer" + }, + { + "command": "ecooptimizer.metricsView.refresh", + "title": "Refresh Metrics Data", + "icon": "$(sync)", + "category": "Eco Optimizer" + }, { "command": "ecooptimizer.detectSmellsFolder", "title": "Detect Smells for All Files", @@ -293,6 +299,16 @@ "when": "view == ecooptimizer.metricsView", "group": "resource" }, + { + "command": "ecooptimizer.clearMetricsData", + "when": "view == ecooptimizer.metricsView", + "group": "resource" + }, + { + "command": "ecooptimizer.metricsView.refresh", + "when": "view == ecooptimizer.metricsView", + "group": "navigation" + }, { "command": "ecooptimizer.acceptRefactoring", "when": "view == ecooptimizer.refactoringDetails && refactoringInProgress", diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 62a3c47..1a01c3e 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,11 +1,12 @@ import * as vscode from 'vscode'; +import { basename } from 'path'; +import * as fs from 'fs'; + import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; -import { MetricsViewProvider } from '../providers/MetricsViewProvider'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import path from 'path'; -import * as fs from 'fs'; +import { MetricsViewProvider } from '../providers/MetricsViewProvider'; function normalizePath(filePath: string): string { const normalizedPath = filePath.toLowerCase(); // Normalize case for consistent Map keying @@ -21,7 +22,6 @@ function normalizePath(filePath: string): string { */ export async function refactorSmell( smellsDataProvider: SmellsViewProvider, - metricsDataProvider: MetricsViewProvider, refactoringDetailsViewProvider: RefactoringDetailsViewProvider, smell: Smell, ): Promise { @@ -41,6 +41,7 @@ export async function refactorSmell( // Update the refactoring details view with the target file, affected files, and energy saved refactoringDetailsViewProvider.updateRefactoringDetails( + smell.symbol, refactoredData.targetFile, refactoredData.affectedFiles, refactoredData.energySaved, // Pass the energy saved value @@ -48,7 +49,7 @@ export async function refactorSmell( // Show a diff view for the target file const targetFile = refactoredData.targetFile; - const fileName = path.basename(targetFile.original); + const fileName = basename(targetFile.original); const originalUri = vscode.Uri.file(targetFile.original); const refactoredUri = vscode.Uri.file(targetFile.refactored); await vscode.commands.executeCommand( @@ -67,14 +68,6 @@ export async function refactorSmell( // Focus on the Refactoring Details view await vscode.commands.executeCommand('ecooptimizer.refactoringDetails.focus'); - if (refactoredData.energySaved) { - metricsDataProvider.updateMetrics( - filePath, - refactoredData.energySaved, - smell.symbol, - ); - } - // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, @@ -95,9 +88,10 @@ export async function refactorSmell( */ export async function acceptRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, + metricsDataProvider: MetricsViewProvider, smellsCacheManager: SmellsCacheManager, smellsViewProvider: SmellsViewProvider, -) { +): Promise { const targetFile = refactoringDetailsViewProvider.targetFile; const affectedFiles = refactoringDetailsViewProvider.affectedFiles; @@ -115,6 +109,17 @@ export async function acceptRefactoring( fs.copyFileSync(file.refactored, file.original); } + const energySaved = refactoringDetailsViewProvider.energySaved; + const targetSmell = refactoringDetailsViewProvider.targetSmell; + const file = vscode.Uri.file(targetFile.original).fsPath; + + console.log('Energy: %d, smell: %s', energySaved, targetSmell); + + if (energySaved && targetSmell) { + console.log('Updating metrics for', file); + metricsDataProvider.updateMetrics(file, energySaved, targetSmell); + } + // Notify the user vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); @@ -158,7 +163,7 @@ export async function acceptRefactoring( */ export async function rejectRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, -) { +): Promise { // Notify the user vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); diff --git a/src/extension.ts b/src/extension.ts index 70068e0..5c6c9be 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line unused-imports/no-unused-imports import { envConfig } from './utils/envConfig'; import * as vscode from 'vscode'; @@ -11,7 +10,11 @@ import { exportMetricsData } from './commands/exportMetricsData'; import { registerFilterSmellCommands } from './commands/filterSmells'; import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; -import { refactorSmell } from './commands/refactorSmell'; +import { + refactorSmell, + acceptRefactoring, + rejectRefactoring, +} from './commands/refactorSmell'; import { SmellsViewProvider } from './providers/SmellsViewProvider'; import { FilterViewProvider } from './providers/FilterViewProvider'; @@ -22,13 +25,10 @@ import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; -import { - acceptRefactoring, - checkServerStatus, - rejectRefactoring, -} from './api/backend'; +import { checkServerStatus } from './api/backend'; import { loadSmells } from './utils/smellsData'; import path from 'path'; +import { registerWorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -145,6 +145,34 @@ export function activate(context: vscode.ExtensionContext): void { ), ); + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.metricsView.refresh', () => { + metricsViewProvider.refresh(); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.clearMetricsData', () => { + vscode.window + .showWarningMessage( + 'Are you sure you want to clear the metrics data? This action is irreversible, and the data will be permanently lost unless exported.', + { modal: true }, + 'Yes', + 'No', + ) + .then((selection) => { + if (selection === 'Yes') { + context.workspaceState.update( + envConfig.WORKSPACE_METRICS_DATA!, + undefined, + ); + vscode.window.showInformationMessage('Metrics data has been cleared.'); + } + }); + metricsViewProvider.refresh(); + }), + ); + // Reset the refactoring details view initially refactoringDetailsViewProvider.resetRefactoringDetails(); @@ -180,6 +208,7 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => acceptRefactoring( refactoringDetailsViewProvider, + metricsViewProvider, smellsCacheManager, smellsViewProvider, ), @@ -235,6 +264,11 @@ export function activate(context: vscode.ExtensionContext): void { smellsViewProvider, ); context.subscriptions.push(fileSaveListener); + + // Register the workspace modified listener + const workspaceModifiedListener = + registerWorkspaceModifiedListener(metricsViewProvider); + context.subscriptions.push(workspaceModifiedListener); } /** diff --git a/src/global.d.ts b/src/global.d.ts index 8ae153e..10faf27 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -44,7 +44,7 @@ declare global { original: string; refactored: string; }; - energySaved: number | null; + energySaved?: number; affectedFiles: { original: string; refactored: string; diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts new file mode 100644 index 0000000..416c1d7 --- /dev/null +++ b/src/listeners/workspaceModifiedListener.ts @@ -0,0 +1,34 @@ +import * as vscode from 'vscode'; +import { MetricsViewProvider } from '../providers/MetricsViewProvider'; + +/** + * Registers a listener for workspace modifications (file creation, deletion, and changes) + * and refreshes the MetricsViewProvider when any of these events occur. + * + * @param metricsViewProvider - The MetricsViewProvider instance to refresh. + * @returns A disposable that can be used to unregister the listener. + */ +export function registerWorkspaceModifiedListener( + metricsViewProvider: MetricsViewProvider, +): vscode.Disposable { + const watcher = vscode.workspace.createFileSystemWatcher('**/*'); + + const onDidCreateDisposable = watcher.onDidCreate(() => { + metricsViewProvider.refresh(); + }); + + const onDidChangeDisposable = watcher.onDidChange(() => { + metricsViewProvider.refresh(); + }); + + const onDidDeleteDisposable = watcher.onDidDelete(() => { + metricsViewProvider.refresh(); + }); + + return vscode.Disposable.from( + watcher, + onDidCreateDisposable, + onDidChangeDisposable, + onDidDeleteDisposable, + ); +} diff --git a/src/providers/MetricsViewProvider.ts b/src/providers/MetricsViewProvider.ts index 96108c9..5291c0f 100644 --- a/src/providers/MetricsViewProvider.ts +++ b/src/providers/MetricsViewProvider.ts @@ -3,7 +3,64 @@ import * as fs from 'fs'; import { basename } from 'path'; import { envConfig } from '../utils/envConfig'; -import { getEnabledSmells, getFilterSmells } from '../utils/smellsData'; +import { getFilterSmells } from '../utils/smellsData'; + +/** + * Represents a metric item in the tree view. + */ +class MetricItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly contextValue: string, + public readonly carbonSaved?: number, + public readonly resourceUri?: vscode.Uri, // For file/folder paths + public readonly smellName?: string, // For smell names + ) { + super(label, collapsibleState); + + // Set icon based on contextValue + switch (this.contextValue) { + case 'folder': + this.iconPath = new vscode.ThemeIcon('folder'); // Built-in folder icon + break; + case 'file': + this.iconPath = new vscode.ThemeIcon('file'); // Built-in file icon + break; + case 'smell': + this.iconPath = new vscode.ThemeIcon('tag'); // Built-in warning icon + break; + case 'folder-stats': + this.iconPath = new vscode.ThemeIcon('graph'); // Optional stats icon + break; + } + + // Set description for carbon saved + this.description = + carbonSaved !== undefined + ? `Carbon Saved: ${formatNumber(carbonSaved)} kg` + : ''; + this.tooltip = this.description; + + this.tooltip = smellName !== undefined ? smellName : ''; + + if (resourceUri && contextValue === 'file') { + this.resourceUri = resourceUri; + this.command = { + title: 'Open File', + command: 'vscode.open', + arguments: [resourceUri], + }; + } + } +} + +export interface MetricsDataItem { + totalCarbonSaved: number; + smellDistribution: { + [smell: string]: number; + }; +} export class MetricsViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData = new vscode.EventEmitter(); @@ -52,7 +109,7 @@ export class MetricsViewProvider implements vscode.TreeDataProvider // Folder stats const folderStats = [ new MetricItem( - `Total Carbon Saved: ${folderMetrics.totalCarbonSaved.toFixed(2)} kg`, + `Total Carbon Saved: ${formatNumber(folderMetrics.totalCarbonSaved)} kg`, vscode.TreeItemCollapsibleState.None, 'folder-stats', ), @@ -70,7 +127,8 @@ export class MetricsViewProvider implements vscode.TreeDataProvider }), ); - return [...folderStats, ...contents]; + const children = [...folderStats, ...contents]; + return children.sort(compareTreeItems); } if (element.contextValue === 'file') { @@ -113,7 +171,7 @@ export class MetricsViewProvider implements vscode.TreeDataProvider carbonSaved: number; }): MetricItem { return new MetricItem( - `${data.acronym}: ${data.carbonSaved.toFixed(2)} kg`, + `${data.acronym}: ${formatNumber(data.carbonSaved)} kg`, vscode.TreeItemCollapsibleState.None, 'smell', undefined, @@ -246,9 +304,6 @@ export class MetricsViewProvider implements vscode.TreeDataProvider [path: string]: MetricsDataItem; }>(envConfig.WORKSPACE_METRICS_DATA!, {}); - // Get the acronym for the smell - const acronym = getEnabledSmells()[smellSymbol].acronym; - if (!metrics[filePath]) { metrics[filePath] = { totalCarbonSaved: 0, @@ -259,10 +314,10 @@ export class MetricsViewProvider implements vscode.TreeDataProvider metrics[filePath].totalCarbonSaved = (metrics[filePath].totalCarbonSaved || 0) + carbonSaved; - if (!metrics[filePath].smellDistribution[acronym]) { - metrics[filePath].smellDistribution[acronym] = 0; + if (!metrics[filePath].smellDistribution[smellSymbol]) { + metrics[filePath].smellDistribution[smellSymbol] = 0; } - metrics[filePath].smellDistribution[acronym] += carbonSaved; + metrics[filePath].smellDistribution[smellSymbol] += carbonSaved; this.context.workspaceState.update(envConfig.WORKSPACE_METRICS_DATA!, metrics); @@ -270,57 +325,32 @@ export class MetricsViewProvider implements vscode.TreeDataProvider } } -/** - * Represents a metric item in the tree view. - */ -class MetricItem extends vscode.TreeItem { - constructor( - public readonly label: string, - public readonly collapsibleState: vscode.TreeItemCollapsibleState, - public readonly contextValue: string, - public readonly carbonSaved?: number, - public readonly resourceUri?: vscode.Uri, // For file/folder paths - public readonly smellName?: string, // For smell names - ) { - super(label, collapsibleState); - - // Set icon based on contextValue - switch (this.contextValue) { - case 'folder': - this.iconPath = new vscode.ThemeIcon('folder'); // Built-in folder icon - break; - case 'file': - this.iconPath = new vscode.ThemeIcon('file'); // Built-in file icon - break; - case 'smell': - this.iconPath = new vscode.ThemeIcon('tag'); // Built-in warning icon - break; - case 'folder-stats': - this.iconPath = new vscode.ThemeIcon('graph'); // Optional stats icon - break; - } - - // Set description for carbon saved - this.description = - carbonSaved !== undefined ? `Carbon Saved: ${carbonSaved.toFixed(2)} kg` : ''; - this.tooltip = this.description; - - this.tooltip = smellName !== undefined ? smellName : ''; - - if (resourceUri && contextValue === 'file') { - this.resourceUri = resourceUri; - this.command = { - title: 'Open File', - command: 'vscode.open', - arguments: [resourceUri], - }; - } - } +const contextPriority: { [key: string]: number } = { + folder: 1, + file: 2, + smell: 3, + 'folder-stats': 4, +}; + +function compareTreeItems(a: MetricItem, b: MetricItem): number { + // Sort by contextValue priority first + const priorityA = contextPriority[a.contextValue] || 0; + const priorityB = contextPriority[b.contextValue] || 0; + if (priorityA < priorityB) return -1; + if (priorityA > priorityB) return 1; + + // If contextValue is the same, sort by label + if (a.label < b.label) return -1; + if (a.label > b.label) return 1; + + return 0; } -export interface MetricsDataItem { - totalCarbonSaved: number; - smellDistribution: { - [smell: string]: number; - }; +function formatNumber(number: number, decimalPlaces: number = 2): string { + const threshold = 0.001; + if (Math.abs(number) < threshold) { + return number.toExponential(decimalPlaces); + } else { + return number.toFixed(decimalPlaces); + } } diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 0a464df..03058c8 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -12,7 +12,8 @@ export class RefactoringDetailsViewProvider private refactoringDetails: RefactoringDetailItem[] = []; public targetFile: { original: string; refactored: string } | undefined; public affectedFiles: { original: string; refactored: string }[] = []; - public energySaved: number | null = null; // Add energySaved as a class property + public energySaved: number | undefined; // Add energySaved as a class property + public targetSmell: string | undefined; constructor() { this.resetRefactoringDetails(); @@ -25,10 +26,12 @@ export class RefactoringDetailsViewProvider * @param energySaved - The amount of energy saved in kg CO2. */ updateRefactoringDetails( + targetSmell: string, targetFile: { original: string; refactored: string }, affectedFiles: { original: string; refactored: string }[], - energySaved: number | null, + energySaved: number | undefined, ): void { + this.targetSmell = targetSmell; this.targetFile = targetFile; this.affectedFiles = affectedFiles; this.energySaved = energySaved; @@ -37,7 +40,7 @@ export class RefactoringDetailsViewProvider this.refactoringDetails = []; // Add energy saved as the first item - if (energySaved !== null) { + if (energySaved) { this.refactoringDetails.push( new RefactoringDetailItem( `Energy Saved: ${energySaved} kg CO2`, // Label @@ -73,7 +76,8 @@ export class RefactoringDetailsViewProvider resetRefactoringDetails(): void { this.targetFile = undefined; this.affectedFiles = []; - this.energySaved = null; + this.targetSmell = undefined; + this.energySaved = undefined; this.refactoringDetails = []; this._onDidChangeTreeData.fire(undefined); // Refresh the view } From 0a01bc2014daa89622a4262716462a71ec3fc45f Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 22:43:06 -0400 Subject: [PATCH 033/121] started making icon ui for refactoring --- package.json | 18 +++++++++--------- src/commands/refactorSmell.ts | 9 ++++++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 58a4124..6dd3aca 100644 --- a/package.json +++ b/package.json @@ -250,22 +250,22 @@ "view/title": [ { "command": "ecooptimizer.resetConfiguration", - "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured", + "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured && !refactoringInProgress", "group": "resource" }, { "command": "ecooptimizer.wipeWorkCache", - "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured", + "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured && !refactoringInProgress", "group": "resource" }, { "command": "ecooptimizer.selectAllFilterSmells", - "when": "view == ecooptimizer.filterView", + "when": "view == ecooptimizer.filterView && !refactoringInProgress", "group": "resource" }, { "command": "ecooptimizer.deselectAllFilterSmells", - "when": "view == ecooptimizer.filterView", + "when": "view == ecooptimizer.filterView && !refactoringInProgress", "group": "resource" }, { @@ -282,27 +282,27 @@ "view/item/context": [ { "command": "ecooptimizer.editSmellFilterOption", - "when": "viewItem == smellOption", + "when": "viewItem == smellOption && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.detectSmellsFolder", - "when": "viewItem == ecoOptimizerFolder", + "when": "viewItem == ecoOptimizerFolder && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.detectSmellsFile", - "when": "viewItem == ecoOptimizerFile || viewItem == ecoOptimizerFile-hasSmells", + "when": "(viewItem == ecoOptimizerFile || viewItem == ecoOptimizerFile-hasSmells) && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.refactorAllSmellsOfType", - "when": "viewItem == ecoOptimizerFile-hasSmells", + "when": "viewItem == ecoOptimizerFile-hasSmells && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.refactorSmell", - "when": "viewItem == ecoOptimizerSmell", + "when": "viewItem == ecoOptimizerSmell && !refactoringInProgress", "group": "inline" } ] diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index b7c9506..0b7cd7b 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -30,7 +30,13 @@ export async function refactorSmell( vscode.window.showInformationMessage(`Refactoring code smell: ${smell.symbol}`); + // Update UI to indicate the file is queued for analysis + treeDataProvider.updateStatus(smell.path, 'queued'); + try { + // Set a context key to track that refactoring is in progress + vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); + // Call the backend to refactor the smell const refactoredData = await backendRefactorSmell(smell); @@ -59,9 +65,6 @@ export async function refactorSmell( }, ); - // Set a context key to track that refactoring is in progress - vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); - // Focus on the Refactoring Details view await vscode.commands.executeCommand('ecooptimizer.refactoringDetails.focus'); From 930b2d593c4fa372dbb27d2527a4dd5a80aab971 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 23:07:23 -0400 Subject: [PATCH 034/121] small fix --- src/commands/refactorSmell.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index bb41a29..f25d263 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -33,7 +33,7 @@ export async function refactorSmell( vscode.window.showInformationMessage(`Refactoring code smell: ${smell.symbol}`); // Update UI to indicate the file is queued for analysis - treeDataProvider.updateStatus(smell.path, 'queued'); + smellsDataProvider.updateStatus(smell.path, 'queued'); try { // Set a context key to track that refactoring is in progress From 24a3d1e02893b36fb35fdb67f307e71f581239ce Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 23:34:51 -0400 Subject: [PATCH 035/121] configuring workspace fix --- package.json | 8 ++-- src/commands/configureWorkspace.ts | 62 ++++++++++++++++++------------ src/extension.ts | 7 +++- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 85d47a4..f0b22af 100644 --- a/package.json +++ b/package.json @@ -139,13 +139,13 @@ "icon": "assets/eco-icon.png" }, { - "id": "ecooptimizer.filterView", - "name": "Filter Smells", + "id": "ecooptimizer.metricsView", + "name": "Carbon Metrics", "icon": "assets/eco-icon.png" }, { - "id": "ecooptimizer.metricsView", - "name": "Carbon Metrics", + "id": "ecooptimizer.filterView", + "name": "Filter Smells", "icon": "assets/eco-icon.png" } ] diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index 7be0c7d..ab3ea36 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -81,7 +81,41 @@ async function configurePythonFile( } /** - * Prompts the user to select a Python folder from the workspace. + * Recursively finds all folders in the workspace that contain Python files or are Python modules. + * + * @param folderPath - The absolute path of the folder to start scanning from. + * @returns An array of folder paths that contain Python files or are Python modules. + */ +function findPythonFoldersRecursively(folderPath: string): string[] { + let pythonFolders: string[] = []; + + try { + const files = fs.readdirSync(folderPath); + + // Check if the current folder is a Python module or contains Python files + if ( + files.includes('__init__.py') || + files.some((file) => file.endsWith('.py')) + ) { + pythonFolders.push(folderPath); + } + + // Recursively scan subfolders + files.forEach((file) => { + const filePath = path.join(folderPath, file); + if (fs.statSync(filePath).isDirectory()) { + pythonFolders = pythonFolders.concat(findPythonFoldersRecursively(filePath)); + } + }); + } catch (error) { + console.error(`Error scanning folder ${folderPath}:`, error); + } + + return pythonFolders; +} + +/** + * Prompts the user to select a Python folder from the workspace, including nested folders. * Updates the workspace state with the selected folder and refreshes the tree view. * * @param context - The extension context used to persist workspace state. @@ -102,10 +136,10 @@ async function configurePythonFolder( return; } - // Filter workspace folders to include only those containing Python files + // Find all valid Python folders, including nested ones const validPythonFolders = workspaceFolders .map((folder) => folder.uri.fsPath) - .filter((folderPath) => containsPythonFiles(folderPath)); + .flatMap((folderPath) => findPythonFoldersRecursively(folderPath)); // Notify the user if no valid Python folders are found if (validPythonFolders.length === 0) { @@ -129,28 +163,6 @@ async function configurePythonFolder( } } -/** - * Checks if a given folder contains Python files. - * This function scans the folder for `.py` files or an `__init__.py` file. - * - * @param folderPath - The absolute path of the folder to check. - * @returns True if the folder contains Python files, otherwise false. - */ -function containsPythonFiles(folderPath: string): boolean { - try { - // Read the contents of the folder - const files = fs.readdirSync(folderPath); - - // Check if any file ends with `.py` or if the folder contains `__init__.py` - return ( - files.some((file) => file.endsWith('.py')) || files.includes('__init__.py') - ); - } catch (error) { - // Return false if an error occurs (e.g., folder is inaccessible) - return false; - } -} - /** * Updates the workspace state to reflect the configured Python file or folder. * Refreshes the tree view to reflect the changes. diff --git a/src/extension.ts b/src/extension.ts index 5c6c9be..32ec8a3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -59,7 +59,9 @@ export function activate(context: vscode.ExtensionContext): void { checkServerStatus(); setInterval(checkServerStatus, 10000); - // Track the workspace configuration state. + //////////////////////////////////////////////// + // WORKSPACE CONFIGURATION COMMANDS + //////////////////////////////////////////////// const workspaceConfigured = Boolean( context.workspaceState.get('workspaceConfiguredPath'), ); @@ -69,7 +71,6 @@ export function activate(context: vscode.ExtensionContext): void { workspaceConfigured, ); - // Register workspace-related commands. context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.configureWorkspace', () => configureWorkspace(context, smellsViewProvider), @@ -82,6 +83,8 @@ export function activate(context: vscode.ExtensionContext): void { ), ); + //////////////////////////////////////////////// + // Initialize the Filter Smells View. const filterSmellsProvider = new FilterViewProvider( context, From 9b52b44a4669f75f99e1eaf07ad2a0f7b182fb73 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 22 Mar 2025 23:38:08 -0400 Subject: [PATCH 036/121] renamed smells view --- src/managers/SmellsViewStateManager.ts | 2 +- src/managers/SmellsViewUIManager.ts | 6 +++--- src/providers/SmellsViewProvider.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/managers/SmellsViewStateManager.ts b/src/managers/SmellsViewStateManager.ts index 8f2c231..4a42b0d 100644 --- a/src/managers/SmellsViewStateManager.ts +++ b/src/managers/SmellsViewStateManager.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -export class SmellsStateManager { +export class SmellsViewStateManager { private fileStatusMap: Map = new Map(); private detectedSmells: Map = new Map(); // Use Smell[] instead of ProcessedSmell[] private smellToFileMap: Map = new Map(); diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts index e0dbb20..72e0c85 100644 --- a/src/managers/SmellsViewUIManager.ts +++ b/src/managers/SmellsViewUIManager.ts @@ -1,15 +1,15 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { SmellsStateManager } from './SmellsViewStateManager'; +import { SmellsViewStateManager } from './SmellsViewStateManager'; /** * Manages the UI representation of files, folders, and detected smells in the VS Code tree view. * This class handles creating tree items, assigning commands, and updating item states based on * the analysis status and file state (e.g., outdated, queued, passed, failed). */ -export class SmellsUIManager { - constructor(private stateManager: SmellsStateManager) {} +export class SmellsViewUIManager { + constructor(private stateManager: SmellsViewStateManager) {} /** * Creates a tree item for a given element (folder, file, or smell). diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index f61082e..4dcf917 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -1,19 +1,19 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { SmellsStateManager } from '../managers/SmellsViewStateManager'; -import { SmellsUIManager } from '../managers/SmellsViewUIManager'; +import { SmellsViewStateManager } from '../managers/SmellsViewStateManager'; +import { SmellsViewUIManager } from '../managers/SmellsViewUIManager'; export class SmellsViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - private stateManager: SmellsStateManager; - private uiManager: SmellsUIManager; + private stateManager: SmellsViewStateManager; + private uiManager: SmellsViewUIManager; constructor(private context: vscode.ExtensionContext) { - this.stateManager = new SmellsStateManager(); - this.uiManager = new SmellsUIManager(this.stateManager); + this.stateManager = new SmellsViewStateManager(); + this.uiManager = new SmellsViewUIManager(this.stateManager); } /** From a334f8e74fe4345368480a5975c2ed003c93e194 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 23 Mar 2025 01:14:58 -0400 Subject: [PATCH 037/121] Added LineSelection and Highlighting back in --- src/api/backend.ts | 4 +- src/commands/detectSmells.ts | 2 +- src/context/SmellsCacheManager.ts | 9 + src/context/configManager.ts | 39 +++++ src/{utils => emitters}/serverStatus.ts | 0 src/extension.ts | 20 ++- src/providers/FilterViewProvider.ts | 2 +- src/ui/FileHighlighter.ts | 210 ++++++++++++++++++++++++ src/ui/LineSelection.ts | 108 ++++++++++++ 9 files changed, 388 insertions(+), 6 deletions(-) create mode 100644 src/context/configManager.ts rename src/{utils => emitters}/serverStatus.ts (100%) create mode 100644 src/ui/FileHighlighter.ts create mode 100644 src/ui/LineSelection.ts diff --git a/src/api/backend.ts b/src/api/backend.ts index a8b533d..9df6b6e 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -1,6 +1,6 @@ import { envConfig } from '../utils/envConfig'; -import { serverStatus } from '../utils/serverStatus'; -import { ServerStatusType } from '../utils/serverStatus'; +import { serverStatus } from '../emitters/serverStatus'; +import { ServerStatusType } from '../emitters/serverStatus'; import * as vscode from 'vscode'; const BASE_URL = `http://${envConfig.SERVER_URL}`; // API URL for Python backend diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index cc645d7..65f9c7d 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -5,7 +5,7 @@ import { fetchSmells } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { getEnabledSmells } from '../utils/smellsData'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { serverStatus, ServerStatusType } from '../utils/serverStatus'; +import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; /** * Detects code smells for a given file. diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index f212882..1a94528 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -9,6 +9,9 @@ import { envConfig } from '../utils/envConfig'; * as well as refreshing the UI when the cache is updated. */ export class SmellsCacheManager { + private cacheUpdatedEmitter = new vscode.EventEmitter(); + public readonly onSmellsUpdated = this.cacheUpdatedEmitter.event; + constructor(private context: vscode.ExtensionContext) {} // ============================ @@ -54,6 +57,8 @@ export class SmellsCacheManager { // Update the cache with the new smells cache[filePath] = smellsWithIds; await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + + this.cacheUpdatedEmitter.fire(filePath); } /** @@ -91,6 +96,8 @@ export class SmellsCacheManager { */ public async clearSmellsCache(): Promise { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, undefined); + + this.cacheUpdatedEmitter.fire('all'); } /** @@ -102,6 +109,8 @@ export class SmellsCacheManager { const cache = this.getFullSmellCache(); delete cache[filePath]; await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + + this.cacheUpdatedEmitter.fire(filePath); } /** diff --git a/src/context/configManager.ts b/src/context/configManager.ts new file mode 100644 index 0000000..bd3a216 --- /dev/null +++ b/src/context/configManager.ts @@ -0,0 +1,39 @@ +import * as vscode from 'vscode'; + +export class ConfigManager { + private static readonly CONFIG_SECTION = 'ecooptimizer.detection'; + + /** + * Get a specific configuration value. + * @param key The key of the configuration property. + * @param _default The default value to return if the configuration property is not found. + * @returns The value of the configuration property. + */ + public static get(key: string, _default: any = undefined): T { + const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); + return config.get(key, _default); + } + + /** + * Update a specific configuration value. + * @param key The key of the configuration property. + * @param value The new value to set. + * @param global Whether to update the global configuration or workspace configuration. + */ + public static async update( + key: string, + value: T, + global: boolean = false, + ): Promise { + const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); + await config.update(key, value, global); + } + + /** + * Get all configuration values under the ecooptimizer.detection section. + * @returns The entire configuration object. + */ + public static getAll(): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration(this.CONFIG_SECTION); + } +} diff --git a/src/utils/serverStatus.ts b/src/emitters/serverStatus.ts similarity index 100% rename from src/utils/serverStatus.ts rename to src/emitters/serverStatus.ts diff --git a/src/extension.ts b/src/extension.ts index 5c6c9be..f35c69b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import { envConfig } from './utils/envConfig'; import * as vscode from 'vscode'; +import path from 'path'; import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; @@ -21,14 +22,16 @@ import { FilterViewProvider } from './providers/FilterViewProvider'; import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; import { MetricsViewProvider } from './providers/MetricsViewProvider'; +import { FileHighlighter } from './ui/FileHighlighter'; + import { SmellsCacheManager } from './context/SmellsCacheManager'; import { registerFileSaveListener } from './listeners/fileSaveListener'; +import { registerWorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { checkServerStatus } from './api/backend'; import { loadSmells } from './utils/smellsData'; -import path from 'path'; -import { registerWorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; +import { LineSelectionManager } from './ui/LineSelection'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -128,6 +131,11 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + // Initialize the FileHighlighter for highlighting code smells. + const fileHighlighter = FileHighlighter.getInstance(smellsCacheManager); + + fileHighlighter.updateHighlightsForVisibleEditors(); + // Initialize the RefactoringDetailsViewProvider const refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); // eslint-disable-next-line unused-imports/no-unused-vars @@ -258,6 +266,14 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + const lineSelectManager = new LineSelectionManager(smellsCacheManager); + context.subscriptions.push( + vscode.window.onDidChangeTextEditorSelection((event) => { + console.log('Eco: Detected line selection event'); + lineSelectManager.commentLine(event.textEditor); + }), + ); + // Register the file save listener to detect outdated files. const fileSaveListener = registerFileSaveListener( smellsCacheManager, diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index 7fee9f3..13ea8ce 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -169,7 +169,7 @@ export class FilterViewProvider implements vscode.TreeDataProvider { const cache = this.cacheManager.getFullSmellCache(); - for (const [filePath, smells] of Object.entries(cache)) { + for (const filePath of Object.keys(cache)) { await this.cacheManager.clearCachedSmellsForFile(filePath); this.smellsViewProvider.markFileAsOutdated(filePath); } diff --git a/src/ui/FileHighlighter.ts b/src/ui/FileHighlighter.ts new file mode 100644 index 0000000..1bfd226 --- /dev/null +++ b/src/ui/FileHighlighter.ts @@ -0,0 +1,210 @@ +import * as vscode from 'vscode'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { ConfigManager } from '../context/configManager'; +import { getEnabledSmells } from '../utils/smellsData'; + +/** + * The `FileHighlighter` class is responsible for managing and applying visual highlights + * to code smells in the VS Code editor. It uses cached smell data to determine which + * lines to highlight and applies decorations to the editor accordingly. + */ +export class FileHighlighter { + private static instance: FileHighlighter; + private decorations: vscode.TextEditorDecorationType[] = []; + + private constructor(private smellsCacheManager: SmellsCacheManager) { + this.smellsCacheManager.onSmellsUpdated((target) => { + if (target === 'all') { + this.updateHighlightsForVisibleEditors(); + } else { + this.updateHighlightsForFile(target); + } + }); + } + + /** + * Retrieves the singleton instance of the `FileHighlighter` class. + * If the instance does not exist, it is created. + * + * @param smellsCacheManager - The manager responsible for caching and providing smell data. + * @returns The singleton instance of `FileHighlighter`. + */ + public static getInstance( + smellsCacheManager: SmellsCacheManager, + ): FileHighlighter { + if (!FileHighlighter.instance) { + FileHighlighter.instance = new FileHighlighter(smellsCacheManager); + } + return FileHighlighter.instance; + } + + /** + * Updates highlights for a specific file if it is currently open in a visible editor. + * + * @param filePath - The file path of the target file to update highlights for. + */ + private updateHighlightsForFile(filePath: string): void { + const editor = vscode.window.visibleTextEditors.find( + (e) => e.document.uri.fsPath === filePath, + ); + if (editor) { + this.highlightSmells(editor); + } + } + + /** + * Updates highlights for all currently visible editors. + */ + public updateHighlightsForVisibleEditors(): void { + vscode.window.visibleTextEditors.forEach((editor) => { + this.highlightSmells(editor); + }); + } + + /** + * Resets all active highlights by disposing of all decorations. + */ + public resetHighlights(): void { + this.decorations.forEach((decoration) => decoration.dispose()); + this.decorations = []; + } + + /** + * Highlights code smells in the given editor based on cached smell data. + * Resets existing highlights before applying new ones. + * + * @param editor - The text editor to apply highlights to. + */ + public highlightSmells(editor: vscode.TextEditor): void { + this.resetHighlights(); + + const smells = this.smellsCacheManager.getCachedSmells( + editor.document.uri.fsPath, + ); + + if (!smells) { + return; + } + + const smellColours = ConfigManager.get<{ + [key: string]: string; + }>('smellsColours', {}); + + const useSingleColour = ConfigManager.get('useSingleColour', false); + const singleHighlightColour = ConfigManager.get( + 'singleHighlightColour', + 'rgba(255, 204, 0, 0.5)', + ); + const highlightStyle = ConfigManager.get('highlightStyle', 'underline'); + + const activeSmells = new Set(smells.map((smell) => smell.symbol)); + + const enabledSmells = getEnabledSmells(); + + activeSmells.forEach((smellType) => { + const smellColour = smellColours[smellType]; + + if (enabledSmells[smellType]) { + const colour = useSingleColour ? singleHighlightColour : smellColour; + + this.highlightSmell(editor, smells, smellType, colour, highlightStyle); + } + }); + } + + /** + * Highlights a specific type of smell in the given editor. + * Filters smell occurrences to ensure they are valid and match the target smell type. + * + * @param editor - The text editor to apply highlights to. + * @param smells - The list of all smells for the file. + * @param targetSmell - The specific smell type to highlight. + * @param colour - The colour to use for the highlight. + * @param style - The style of the highlight (e.g., underline, flashlight, border-arrow). + */ + private highlightSmell( + editor: vscode.TextEditor, + smells: Smell[], + targetSmell: string, + colour: string, + style: string, + ): void { + const smellLines: vscode.DecorationOptions[] = smells + .filter((smell: Smell) => { + const valid = smell.occurences.every((occurrence: { line: number }) => + isValidLine(occurrence.line, editor.document.lineCount), + ); + const isCorrectType = smell.symbol === targetSmell; + + return valid && isCorrectType; + }) + .map((smell: Smell) => { + const line = smell.occurences[0].line - 1; // convert to zero-based line index for VS editor + const lineText = editor.document.lineAt(line).text; + const indexStart = lineText.length - lineText.trimStart().length; + const indexEnd = lineText.trimEnd().length + 2; + const range = new vscode.Range(line, indexStart, line, indexEnd); + return { range }; + }); + + const decoration = this.getDecoration(colour, style); + + editor.setDecorations(decoration, smellLines); + this.decorations.push(decoration); + } + + /** + * Creates a text editor decoration type based on the given colour and style. + * + * @param colour - The colour to use for the decoration. + * @param style - The style of the decoration (e.g., underline, flashlight, border-arrow). + * @returns A `vscode.TextEditorDecorationType` object representing the decoration. + */ + private getDecoration( + colour: string, + style: string, + ): vscode.TextEditorDecorationType { + switch (style) { + case 'underline': + return vscode.window.createTextEditorDecorationType({ + textDecoration: `wavy ${colour} underline 1px`, + }); + case 'flashlight': + return vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + backgroundColor: colour, + }); + case 'border-arrow': + return vscode.window.createTextEditorDecorationType({ + borderWidth: '1px 2px 1px 0', + borderStyle: 'solid', + borderColor: colour, + after: { + contentText: '▶', + margin: '0 0 0 5px', + color: colour, + fontWeight: 'bold', + }, + overviewRulerColor: colour, + overviewRulerLane: vscode.OverviewRulerLane.Right, + }); + default: + return vscode.window.createTextEditorDecorationType({ + textDecoration: `wavy ${colour} underline 1px`, + }); + } + } +} + +function isValidLine(line: any, lineCount: number): boolean { + const isValid = + line !== undefined && + line !== null && + typeof line === 'number' && + Number.isFinite(line) && + line > 0 && + Number.isInteger(line) && + line <= lineCount; + + return isValid; +} diff --git a/src/ui/LineSelection.ts b/src/ui/LineSelection.ts new file mode 100644 index 0000000..9282c04 --- /dev/null +++ b/src/ui/LineSelection.ts @@ -0,0 +1,108 @@ +import * as vscode from 'vscode'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; + +/** + * Manages line selection and decoration in a VS Code editor, specifically for + * displaying comments related to code smells. + */ +export class LineSelectionManager { + private decoration: vscode.TextEditorDecorationType | null = null; + + /** + * Constructs a new instance of the `LineSelectionManager`. + * + * @param smellsCacheManager - An instance of `SmellsCacheManager` used to retrieve cached smells for a file. + */ + public constructor(private smellsCacheManager: SmellsCacheManager) {} + + /** + * Removes the last applied decoration from the editor, if any. + * + * This method ensures that only one decoration is applied at a time by disposing + * of the previous decoration before applying a new one. + */ + public removeLastComment(): void { + if (this.decoration) { + console.log('Removing decoration'); + this.decoration.dispose(); + } + } + + /** + * Adds a comment to the currently selected line in the editor, indicating the presence + * of code smells. If multiple smells are present, it displays the first smell and a count + * of additional smells. + * + * @param editor - The active `vscode.TextEditor` instance where the comment will be applied. + * + * @remarks + * - If no smells are cached for the file, or if the selection spans multiple lines, no comment is added. + * - The comment is displayed as a decoration appended to the end of the selected line. + */ + public commentLine(editor: vscode.TextEditor): void { + this.removeLastComment(); + + if (!editor) { + return; + } + + const filePath = editor.document.fileName; + const smells = this.smellsCacheManager.getCachedSmells(filePath); + + if (!smells) { + return; + } + + const { selection } = editor; + + if (!selection.isSingleLine) { + return; + } + + const selectedLine = selection.start.line; + console.log(`selection: ${selectedLine}`); + + const smellsAtLine = smells.filter((smell: Smell) => { + return smell.occurences[0].line === selectedLine + 1; + }); + + if (smellsAtLine.length === 0) { + return; + } + + let comment; + + if (smellsAtLine.length > 1) { + comment = `🍂 Smell: ${smellsAtLine[0].symbol} | (+${ + smellsAtLine.length - 1 + })`; + } else { + comment = `🍂 Smell: ${smellsAtLine[0].symbol}`; + } + + const themeColor = new vscode.ThemeColor('editorLineNumber.foreground'); + this.decoration = vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + after: { + contentText: comment, + color: themeColor, + margin: '0 0 0 10px', + textDecoration: 'none', + }, + }); + + const selectionLine: vscode.Range[] = []; + + // Calculate the range for the decoration based on the line's content. + const line_text = editor.document.lineAt(selectedLine).text; + const line_length = line_text.length; + const indexStart = line_length - line_text.trimStart().length; + const indexEnd = line_text.trimEnd().length + 1; + + selectionLine.push( + new vscode.Range(selectedLine, indexStart, selectedLine, indexEnd), + ); + + editor.setDecorations(this.decoration, selectionLine); + } +} From ce4e82e8ae91ce1b14c7d6ab0e00433fb9b91ca6 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 23 Mar 2025 02:08:53 -0400 Subject: [PATCH 038/121] Finalized editor decoration modules - Added back missing settings config - Refined event handling + cashing --- package.json | 91 +++++++++++++++++++++++++++++++ src/commands/detectSmells.ts | 39 +------------ src/context/SmellsCacheManager.ts | 27 +++++++-- src/extension.ts | 18 ++++-- src/listeners/fileSaveListener.ts | 11 ++-- 5 files changed, 136 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 85d47a4..b517316 100644 --- a/package.json +++ b/package.json @@ -347,6 +347,97 @@ "group": "inline" } ] + }, + "configuration": { + "title": "EcoOptimizer", + "properties": { + "ecooptimizer.detection.smellsColours": { + "order": 1, + "type": "object", + "additionalProperties": false, + "description": "Configure highlight colours for smells.", + "default": { + "long-element-chain": "lightblue", + "too-many-arguments": "lightcoral", + "long-lambda-expression": "mediumpurple", + "long-message-chain": "lightpink", + "cached-repeated-calls": "lightgreen", + "string-concat-loop": "lightsalmon", + "no-self-use": "lightcyan", + "use-a-generator": "yellow" + }, + "properties": { + "long-element-chain": { + "type": "string", + "default": "lightblue", + "description": "Colour (css syntax) for highlighting long element chains." + }, + "too-many-arguments": { + "type": "string", + "default": "lightcoral", + "description": "Colour (css syntax) for highlighting functions with too many arguments." + }, + "long-lambda-expression": { + "type": "string", + "default": "mediumpurple", + "description": "Colour (css syntax) for highlighting long lambda expressions." + }, + "long-message-chain": { + "type": "string", + "default": "lightpink", + "description": "Colour (css syntax) for highlighting long message chains." + }, + "cached-repeated-calls": { + "type": "string", + "default": "lightgreen", + "description": "Colour (css syntax) for highlighting cached repeated calls." + }, + "string-concat-loop": { + "type": "string", + "default": "lightsalmon", + "description": "Colour (css syntax) for highlighting string concatenation in loops." + }, + "no-self-use": { + "type": "string", + "default": "lightcyan", + "description": "Colour (css syntax) for highlighting methods with no self-use." + }, + "use-a-generator": { + "type": "string", + "default": "yellow", + "description": "Colour (css syntax) for highlighting places where a generator could be used." + } + } + }, + "ecooptimizer.detection.useSingleColour": { + "order": 2, + "type": "boolean", + "default": false, + "description": "Use a single colour for all smells. If enabled, the colour defined below will be used." + }, + "ecooptimizer.detection.singleHighlightColour": { + "order": 3, + "type": "string", + "default": "yellow", + "markdownDescription": "Colour (css syntax) to use for all smells if **Use Single Colour** is enabled." + }, + "ecooptimizer.detection.highlightStyle": { + "order": 0, + "type": "string", + "enum": [ + "underline", + "flashlight", + "border-arrow" + ], + "markdownEnumDescriptions": [ + "Your average wavy line", + "No pixel left untouched", + "Basically how it sounds" + ], + "default": "underline", + "description": "Choose a highlight style for all smells." + } + } } } } diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 65f9c7d..693cfe6 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -20,19 +20,18 @@ export async function detectSmellsFile( treeDataProvider: SmellsViewProvider, fileUri: vscode.Uri | string, ): Promise { - // Validate the file URI or path if (!fileUri) { vscode.window.showErrorMessage('No file selected for analysis.'); return; } - // Convert file URI to a path if necessary const filePath = typeof fileUri === 'string' ? fileUri : fileUri.fsPath; // Handle outdated files before proceeding - await handleOutdatedFile(filePath, smellsCacheManager, treeDataProvider); + if (treeDataProvider.isFileOutdated(filePath)) { + treeDataProvider.updateStatus(filePath, 'queued'); + } - // Retrieve enabled smells from configuration const enabledSmells = getEnabledSmells(); // Ensure that at least one smell type is enabled @@ -47,7 +46,6 @@ export async function detectSmellsFile( const cachedSmells = smellsCacheManager.getCachedSmells(filePath); console.log('Cached smells:', cachedSmells); if (cachedSmells !== undefined) { - // Use cached smells if available vscode.window.showInformationMessage( `Using cached smells for ${path.basename(filePath)}.`, ); @@ -78,10 +76,8 @@ export async function detectSmellsFile( Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), ); - // Request smell analysis from the backend const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); - // Handle response and update UI if (status === 200) { // Cache detected smells, even if no smells are found await smellsCacheManager.setCachedSmells(filePath, smells); @@ -164,32 +160,3 @@ export async function detectSmellsFolder( // Refresh UI to reflect folder analysis results treeDataProvider.refresh(); } - -/** - * Handles outdated files before detecting smells. - * Deletes cached smells and updates the UI for outdated files. - * - * @param filePath - The path of the file to analyze. - * @param smellsCacheManager - Manages caching of smells and file hashes. - * @param smellsDisplayProvider - The UI provider for updating the tree view. - */ -async function handleOutdatedFile( - filePath: string, - smellsCacheManager: SmellsCacheManager, - smellsDisplayProvider: SmellsViewProvider, -): Promise { - // Check if the file is marked as outdated - if (smellsDisplayProvider.isFileOutdated(filePath)) { - // Delete cached smells for the outdated file - await smellsCacheManager.clearCachedSmellsForFile(filePath); - - const document = await vscode.workspace.openTextDocument(filePath); - const fileContent = document.getText(); - - console.log('Storing file hash for:', filePath); - await smellsCacheManager.storeFileHash(filePath, fileContent); - - // Remove the outdated status from the UI - smellsDisplayProvider.updateStatus(filePath, 'queued'); // Reset to "queued" or another default status - } -} diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index 1a94528..d2f8667 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -135,20 +135,39 @@ export class SmellsCacheManager { * @param content - The file content as a string. * @returns A SHA256 hash string derived from the file content. */ - public computeFileHash(content: string): string { + private computeFileHash(content: string): string { return createHash('sha256').update(content).digest('hex'); } + public wasFileModified(filePath: string, content: string): boolean { + const newHash = this.computeFileHash(content); + const oldHash = this.getStoredFileHash(filePath); + + return newHash !== oldHash; + } + /** * Stores a hash of a file's contents in the workspace state. * * @param filePath - The absolute path of the file. * @param content - The file content to hash. */ - public async storeFileHash(filePath: string, content: string): Promise { + public async updateFileHash(filePath: string, content: string): Promise { const hashes = this.getFullFileHashCache(); - hashes[filePath] = this.computeFileHash(content); - await this.context.workspaceState.update(envConfig.FILE_HASH_CACHE_KEY!, hashes); + + let currentHash = hashes[filePath]; + const newHash = this.computeFileHash(content); + + if (!currentHash || currentHash !== newHash) { + hashes[filePath] = newHash; + await this.context.workspaceState.update( + envConfig.FILE_HASH_CACHE_KEY!, + hashes, + ); + return true; + } + + return false; } /** diff --git a/src/extension.ts b/src/extension.ts index f35c69b..dff11ae 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -131,11 +131,6 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // Initialize the FileHighlighter for highlighting code smells. - const fileHighlighter = FileHighlighter.getInstance(smellsCacheManager); - - fileHighlighter.updateHighlightsForVisibleEditors(); - // Initialize the RefactoringDetailsViewProvider const refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); // eslint-disable-next-line unused-imports/no-unused-vars @@ -266,6 +261,19 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + // Initialize the FileHighlighter for highlighting code smells. + const fileHighlighter = FileHighlighter.getInstance(smellsCacheManager); + + fileHighlighter.updateHighlightsForVisibleEditors(); + + context.subscriptions.push( + vscode.window.onDidChangeVisibleTextEditors((editors) => { + editors.forEach((editor) => { + fileHighlighter.highlightSmells(editor); + }); + }), + ); + const lineSelectManager = new LineSelectionManager(smellsCacheManager); context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection((event) => { diff --git a/src/listeners/fileSaveListener.ts b/src/listeners/fileSaveListener.ts index 63b3199..900764a 100644 --- a/src/listeners/fileSaveListener.ts +++ b/src/listeners/fileSaveListener.ts @@ -19,17 +19,18 @@ export function registerFileSaveListener( const cachedSmells = smellsCacheManager.getCachedSmells(filePath); if (!cachedSmells) return; - // Compute the new hash and compare it with the stored hash - const newHash = smellsCacheManager.computeFileHash(document.getText()); - const oldHash = smellsCacheManager.getStoredFileHash(filePath); - - if (oldHash && newHash !== oldHash) { + const updated = await smellsCacheManager.updateFileHash( + filePath, + document.getText(), + ); + if (updated) { vscode.window.showWarningMessage( `The file "${path.basename( filePath, )}" has been modified since the last analysis.`, ); + await smellsCacheManager.clearCachedSmellsForFile(filePath); // Mark file as outdated in the UI smellsDisplayProvider.markFileAsOutdated(filePath); } From b44e04e26f21613e74e5d5265d17b9a4bb98ba35 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 23 Mar 2025 02:41:53 -0400 Subject: [PATCH 039/121] Hide commands from command palette --- package.json | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index b517316..b1e4456 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "activitybar": [ { "id": "ecooptimizer", - "title": "Eco Optimizer", + "title": "Eco", "icon": "assets/eco-icon.png" } ] @@ -171,105 +171,116 @@ { "command": "ecooptimizer.configureWorkspace", "title": "Configure Workspace", - "category": "Eco Optimizer" + "category": "Eco" }, { "command": "ecooptimizer.resetConfiguration", "title": "Reset Configuration", - "category": "Eco Optimizer" + "category": "Eco" }, { "command": "ecooptimizer.toggleSmellFilter", "title": "Toggle Smell", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.editSmellFilterOption", "title": "Edit Option", "icon": "$(edit)", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.selectAllFilterSmells", "title": "Select All Smells", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.deselectAllFilterSmells", "title": "Deselect All Smells", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.openFile", "title": "Open File", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.exportMetricsData", "title": "Export Metrics Data as JSON", - "category": "Eco Optimizer" + "category": "Eco" }, { "command": "ecooptimizer.clearMetricsData", "title": "Clear Metrics Data", - "category": "Eco Optimizer" + "category": "Eco" }, { "command": "ecooptimizer.metricsView.refresh", "title": "Refresh Metrics Data", "icon": "$(sync)", - "category": "Eco Optimizer" + "category": "Eco" }, { "command": "ecooptimizer.detectSmellsFolder", "title": "Detect Smells for All Files", "icon": "$(search)", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.detectSmellsFile", "title": "Detect Smells", "icon": "$(search)", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.refactorAllSmellsOfType", "title": "Refactor Smells By Type", "icon": "$(tools)", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.refactorSmell", "title": "Refactor Smell", "icon": "$(tools)", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.acceptRefactoring", "title": "Accept Refactoring", - "category": "Eco Optimizer", + "category": "Eco", "icon": "$(check)" }, { "command": "ecooptimizer.rejectRefactoring", "title": "Reject Refactoring", - "category": "Eco Optimizer", + "category": "Eco", "icon": "$(close)" }, { "command": "ecooptimizer.jumpToSmell", "title": "Jump to Smell in File", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" }, { "command": "ecooptimizer.wipeWorkCache", "title": "Clear Smells Cache", - "category": "Eco Optimizer" + "category": "Eco" }, { "command": "ecooptimizer.openDiffEditor", "title": "Open Diff Editor", - "category": "Eco Optimizer" + "category": "Eco", + "enablement": "false" } ], "menus": { From f98fbae20ec6058aab12e0df7feb1cb8b903740c Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 13:20:15 -0400 Subject: [PATCH 040/121] renamed to smellsView --- package.json | 8 ++++---- src/extension.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 824f75b..146b32d 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "icon": "assets/eco-icon.png" }, { - "id": "ecooptimizer.view", + "id": "ecooptimizer.smellsView", "name": "Code Smells", "icon": "assets/eco-icon.png" }, @@ -157,7 +157,7 @@ "when": "!refactoringInProgress" }, { - "view": "ecooptimizer.view", + "view": "ecooptimizer.smellsView", "contents": "No code smells detected yet. Configure your workspace to start analysis.\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", "when": "!workspaceState.workspaceConfigured" }, @@ -287,12 +287,12 @@ "view/title": [ { "command": "ecooptimizer.resetConfiguration", - "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured && !refactoringInProgress", + "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured && !refactoringInProgress", "group": "resource" }, { "command": "ecooptimizer.wipeWorkCache", - "when": "view == ecooptimizer.view && workspaceState.workspaceConfigured && !refactoringInProgress", + "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured && !refactoringInProgress", "group": "resource" }, { diff --git a/src/extension.ts b/src/extension.ts index 6cae1d5..20b83f7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -47,7 +47,7 @@ export function activate(context: vscode.ExtensionContext): void { // Initialize the Code Smells View. const smellsViewProvider = new SmellsViewProvider(context); - const codeSmellsView = vscode.window.createTreeView('ecooptimizer.view', { + const codeSmellsView = vscode.window.createTreeView('ecooptimizer.smellsView', { treeDataProvider: smellsViewProvider, }); context.subscriptions.push(codeSmellsView); From b0f58878d75af6266a1e9c174144a377ce186b52 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 14:11:29 -0400 Subject: [PATCH 041/121] fixed configuring workspace --- package.json | 169 +----------------- src/commands/configureWorkspace.ts | 93 ++++------ src/commands/resetConfiguration.ts | 23 ++- src/context/SmellsCacheManager.ts | 2 +- src/extension.ts | 262 ++-------------------------- src/providers/SmellsViewProvider.ts | 164 +++++++---------- 6 files changed, 122 insertions(+), 591 deletions(-) diff --git a/package.json b/package.json index 146b32d..11b48ed 100644 --- a/package.json +++ b/package.json @@ -128,11 +128,6 @@ }, "views": { "ecooptimizer": [ - { - "id": "ecooptimizer.refactoringDetails", - "name": "Refactoring Details", - "icon": "assets/eco-icon.png" - }, { "id": "ecooptimizer.smellsView", "name": "Code Smells", @@ -142,23 +137,13 @@ "id": "ecooptimizer.metricsView", "name": "Carbon Metrics", "icon": "assets/eco-icon.png" - }, - { - "id": "ecooptimizer.filterView", - "name": "Filter Smells", - "icon": "assets/eco-icon.png" } ] }, "viewsWelcome": [ - { - "view": "ecooptimizer.refactoringDetails", - "contents": "Refactoring is currently not in progress. Try selecting a smell in the Code Smells view to start refactoring.", - "when": "!refactoringInProgress" - }, { "view": "ecooptimizer.smellsView", - "contents": "No code smells detected yet. Configure your workspace to start analysis.\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", + "contents": "No code smells detected yet. Configure your workspace to start analysis!\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", "when": "!workspaceState.workspaceConfigured" }, { @@ -178,31 +163,6 @@ "title": "Reset Configuration", "category": "Eco" }, - { - "command": "ecooptimizer.toggleSmellFilter", - "title": "Toggle Smell", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.editSmellFilterOption", - "title": "Edit Option", - "icon": "$(edit)", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.selectAllFilterSmells", - "title": "Select All Smells", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.deselectAllFilterSmells", - "title": "Deselect All Smells", - "category": "Eco", - "enablement": "false" - }, { "command": "ecooptimizer.openFile", "title": "Open File", @@ -224,140 +184,17 @@ "title": "Refresh Metrics Data", "icon": "$(sync)", "category": "Eco" - }, - { - "command": "ecooptimizer.detectSmellsFolder", - "title": "Detect Smells for All Files", - "icon": "$(search)", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.detectSmellsFile", - "title": "Detect Smells", - "icon": "$(search)", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.refactorAllSmellsOfType", - "title": "Refactor Smells By Type", - "icon": "$(tools)", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.refactorSmell", - "title": "Refactor Smell", - "icon": "$(tools)", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.acceptRefactoring", - "title": "Accept Refactoring", - "category": "Eco", - "icon": "$(check)" - }, - { - "command": "ecooptimizer.rejectRefactoring", - "title": "Reject Refactoring", - "category": "Eco", - "icon": "$(close)" - }, - { - "command": "ecooptimizer.jumpToSmell", - "title": "Jump to Smell in File", - "category": "Eco", - "enablement": "false" - }, - { - "command": "ecooptimizer.wipeWorkCache", - "title": "Clear Smells Cache", - "category": "Eco" - }, - { - "command": "ecooptimizer.openDiffEditor", - "title": "Open Diff Editor", - "category": "Eco", - "enablement": "false" } ], "menus": { "view/title": [ { "command": "ecooptimizer.resetConfiguration", - "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured && !refactoringInProgress", - "group": "resource" - }, - { - "command": "ecooptimizer.wipeWorkCache", - "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured && !refactoringInProgress", - "group": "resource" - }, - { - "command": "ecooptimizer.selectAllFilterSmells", - "when": "view == ecooptimizer.filterView && !refactoringInProgress", - "group": "resource" - }, - { - "command": "ecooptimizer.deselectAllFilterSmells", - "when": "view == ecooptimizer.filterView && !refactoringInProgress", - "group": "resource" - }, - { - "command": "ecooptimizer.exportMetricsData", - "when": "view == ecooptimizer.metricsView", - "group": "resource" - }, - { - "command": "ecooptimizer.clearMetricsData", - "when": "view == ecooptimizer.metricsView", + "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured", "group": "resource" - }, - { - "command": "ecooptimizer.metricsView.refresh", - "when": "view == ecooptimizer.metricsView", - "group": "navigation" - }, - { - "command": "ecooptimizer.acceptRefactoring", - "when": "view == ecooptimizer.refactoringDetails && refactoringInProgress", - "group": "navigation" - }, - { - "command": "ecooptimizer.rejectRefactoring", - "when": "view == ecooptimizer.refactoringDetails && refactoringInProgress", - "group": "navigation" } ], - "view/item/context": [ - { - "command": "ecooptimizer.editSmellFilterOption", - "when": "viewItem == smellOption && !refactoringInProgress", - "group": "inline" - }, - { - "command": "ecooptimizer.detectSmellsFolder", - "when": "viewItem == ecoOptimizerFolder && !refactoringInProgress", - "group": "inline" - }, - { - "command": "ecooptimizer.detectSmellsFile", - "when": "(viewItem == ecoOptimizerFile || viewItem == ecoOptimizerFile-hasSmells) && !refactoringInProgress", - "group": "inline" - }, - { - "command": "ecooptimizer.refactorAllSmellsOfType", - "when": "viewItem == ecoOptimizerFile-hasSmells && !refactoringInProgress", - "group": "inline" - }, - { - "command": "ecooptimizer.refactorSmell", - "when": "viewItem == ecoOptimizerSmell && !refactoringInProgress", - "group": "inline" - } - ] + "view/item/context": [] }, "configuration": { "title": "EcoOptimizer", diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index ab3ea36..fc93382 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -2,63 +2,51 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { MetricsViewProvider } from '../providers/MetricsViewProvider'; + /** * Prompts the user to configure a workspace by selecting either a Python file or folder. * Updates the workspace state accordingly and refreshes the tree view to reflect the changes. - * - * @param context - The extension context used to persist workspace state. - * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. */ export async function configureWorkspace( context: vscode.ExtensionContext, - treeDataProvider: vscode.TreeDataProvider, + smellsViewProvider: SmellsViewProvider, + metricsViewProvider: MetricsViewProvider, ) { - // Prompt the user to choose between configuring a Python file or folder const choice = await vscode.window.showQuickPick( ['Configure a Python File', 'Configure a Python Folder'], { placeHolder: 'Choose whether to configure a Python file or folder.' }, ); - // Exit if the user cancels the selection if (!choice) return; - // Call the appropriate function based on the user's choice if (choice === 'Configure a Python File') { - await configurePythonFile(context, treeDataProvider); + await configurePythonFile(context, smellsViewProvider, metricsViewProvider); } else { - await configurePythonFolder(context, treeDataProvider); + await configurePythonFolder(context, smellsViewProvider, metricsViewProvider); } } -/** - * Prompts the user to select a Python file from open editors or the workspace. - * Updates the workspace state with the selected file and refreshes the tree view. - * - * @param context - The extension context used to persist workspace state. - * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. - */ async function configurePythonFile( context: vscode.ExtensionContext, - treeDataProvider: vscode.TreeDataProvider, + smellsViewProvider: SmellsViewProvider, + metricsViewProvider: MetricsViewProvider, ) { - // Retrieve Python files from open editors const openEditorFiles = vscode.window.tabGroups.activeTabGroup.tabs .map((tab) => (tab.input as any)?.uri?.fsPath) .filter((filePath) => filePath && filePath.endsWith('.py')); - // Retrieve Python files from the workspace using a glob pattern const workspaceFiles = await vscode.workspace.findFiles( '**/*.py', '**/node_modules/**', ); const workspaceFilePaths = workspaceFiles.map((uri) => uri.fsPath); - // Combine and deduplicate the list of Python files const allPythonFiles = Array.from( new Set([...openEditorFiles, ...workspaceFilePaths]), ); - // Notify the user if no Python files are found if (allPythonFiles.length === 0) { vscode.window.showErrorMessage( 'No Python files found in open editors or workspace.', @@ -66,33 +54,29 @@ async function configurePythonFile( return; } - // Prompt the user to select a Python file from the combined list const selectedFile = await vscode.window.showQuickPick(allPythonFiles, { placeHolder: 'Select a Python file to use as your workspace.', }); - // Update the workspace state and notify the user if a file is selected if (selectedFile) { - await updateWorkspace(context, treeDataProvider, selectedFile); + await updateWorkspace( + context, + selectedFile, + smellsViewProvider, + metricsViewProvider, + ); vscode.window.showInformationMessage( `Workspace configured for file: ${path.basename(selectedFile)}`, ); } } -/** - * Recursively finds all folders in the workspace that contain Python files or are Python modules. - * - * @param folderPath - The absolute path of the folder to start scanning from. - * @returns An array of folder paths that contain Python files or are Python modules. - */ function findPythonFoldersRecursively(folderPath: string): string[] { let pythonFolders: string[] = []; try { const files = fs.readdirSync(folderPath); - // Check if the current folder is a Python module or contains Python files if ( files.includes('__init__.py') || files.some((file) => file.endsWith('.py')) @@ -100,13 +84,12 @@ function findPythonFoldersRecursively(folderPath: string): string[] { pythonFolders.push(folderPath); } - // Recursively scan subfolders - files.forEach((file) => { + for (const file of files) { const filePath = path.join(folderPath, file); if (fs.statSync(filePath).isDirectory()) { pythonFolders = pythonFolders.concat(findPythonFoldersRecursively(filePath)); } - }); + } } catch (error) { console.error(`Error scanning folder ${folderPath}:`, error); } @@ -114,21 +97,13 @@ function findPythonFoldersRecursively(folderPath: string): string[] { return pythonFolders; } -/** - * Prompts the user to select a Python folder from the workspace, including nested folders. - * Updates the workspace state with the selected folder and refreshes the tree view. - * - * @param context - The extension context used to persist workspace state. - * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. - */ async function configurePythonFolder( context: vscode.ExtensionContext, - treeDataProvider: vscode.TreeDataProvider, + smellsViewProvider: SmellsViewProvider, + metricsViewProvider: MetricsViewProvider, ) { - // Retrieve the workspace folders from the current workspace const workspaceFolders = vscode.workspace.workspaceFolders; - // Notify the user if no workspace folders are found if (!workspaceFolders || workspaceFolders.length === 0) { vscode.window.showErrorMessage( 'No workspace folders found. Open a project in Explorer first.', @@ -136,12 +111,10 @@ async function configurePythonFolder( return; } - // Find all valid Python folders, including nested ones const validPythonFolders = workspaceFolders .map((folder) => folder.uri.fsPath) .flatMap((folderPath) => findPythonFoldersRecursively(folderPath)); - // Notify the user if no valid Python folders are found if (validPythonFolders.length === 0) { vscode.window.showErrorMessage( 'No valid Python folders found in your workspace.', @@ -149,43 +122,37 @@ async function configurePythonFolder( return; } - // Prompt the user to select a Python folder from the filtered list const selectedFolder = await vscode.window.showQuickPick(validPythonFolders, { placeHolder: 'Select a Python folder to use as your workspace.', }); - // Update the workspace state and notify the user if a folder is selected if (selectedFolder) { - await updateWorkspace(context, treeDataProvider, selectedFolder); + await updateWorkspace( + context, + selectedFolder, + smellsViewProvider, + metricsViewProvider, + ); vscode.window.showInformationMessage( `Workspace configured for folder: ${path.basename(selectedFolder)}`, ); } } -/** - * Updates the workspace state to reflect the configured Python file or folder. - * Refreshes the tree view to reflect the changes. - * - * @param context - The extension context used to persist workspace state. - * @param treeDataProvider - The tree data provider responsible for refreshing the workspace view. - * @param workspacePath - The selected workspace path (file or folder). - */ -async function updateWorkspace( +export async function updateWorkspace( context: vscode.ExtensionContext, - treeDataProvider: vscode.TreeDataProvider, workspacePath: string, + smellsViewProvider: SmellsViewProvider, + metricsViewProvider: MetricsViewProvider, ) { - // Update the workspace state with the selected path await context.workspaceState.update('workspaceConfiguredPath', workspacePath); - // Set a context variable to indicate that the workspace is configured vscode.commands.executeCommand( 'setContext', 'workspaceState.workspaceConfigured', true, ); - // Refresh the tree view to reflect the updated workspace configuration - (treeDataProvider as any).refresh(); + smellsViewProvider.refresh(); + metricsViewProvider.refresh(); } diff --git a/src/commands/resetConfiguration.ts b/src/commands/resetConfiguration.ts index 945b7bc..32de7d8 100644 --- a/src/commands/resetConfiguration.ts +++ b/src/commands/resetConfiguration.ts @@ -1,19 +1,13 @@ import * as vscode from 'vscode'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; // Updated import +import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { MetricsViewProvider } from '../providers/MetricsViewProvider'; -/** - * Resets the workspace configuration by clearing the stored path and wiping cached smells. - * Prompts the user for confirmation before performing the reset. - * - * @param context - The VS Code extension context. - * @param smellsCacheManager - The cache manager handling cached smells. - * @param treeDataProvider - The tree data provider to refresh the UI. - */ export async function resetConfiguration( context: vscode.ExtensionContext, smellsCacheManager: SmellsCacheManager, - treeDataProvider: SmellsViewProvider, + smellsViewProvider: SmellsViewProvider, + metricsViewProvider: MetricsViewProvider, ) { const confirm = await vscode.window.showWarningMessage( 'Are you sure you want to reset the workspace configuration? This will remove the currently selected folder and wipe cached smells.', @@ -23,14 +17,19 @@ export async function resetConfiguration( if (confirm === 'Reset') { await context.workspaceState.update('workspaceConfiguredPath', undefined); + vscode.commands.executeCommand( 'setContext', 'workspaceState.workspaceConfigured', false, ); - // Clear smells & refresh UI using SmellsCacheManager - await smellsCacheManager.clearCacheAndRefreshUI(treeDataProvider); + // Clear any cached smells, if needed + // smellsCacheManager.clear(); + + // 🔥 Trigger view refreshes + smellsViewProvider.refresh(); + metricsViewProvider.refresh(); vscode.window.showInformationMessage( 'Workspace configuration has been reset. All cached smells have been cleared.', diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index d2f8667..bc748b3 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -210,7 +210,7 @@ export class SmellsCacheManager { await this.clearSmellsCache(); // Reset the UI state, including icons and dropdowns - smellsDisplayProvider.resetAllSmells(); + // smellsDisplayProvider.resetAllSmells(); // Refresh the UI to reflect the cleared cache smellsDisplayProvider.refresh(); diff --git a/src/extension.ts b/src/extension.ts index 20b83f7..dadf818 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,51 +1,19 @@ -import { envConfig } from './utils/envConfig'; - import * as vscode from 'vscode'; -import path from 'path'; import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; -import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; -import { openFile } from './commands/openFile'; -import { exportMetricsData } from './commands/exportMetricsData'; -import { registerFilterSmellCommands } from './commands/filterSmells'; -import { jumpToSmell } from './commands/jumpToSmell'; -import { wipeWorkCache } from './commands/wipeWorkCache'; -import { - refactorSmell, - acceptRefactoring, - rejectRefactoring, -} from './commands/refactorSmell'; import { SmellsViewProvider } from './providers/SmellsViewProvider'; -import { FilterViewProvider } from './providers/FilterViewProvider'; -import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; import { MetricsViewProvider } from './providers/MetricsViewProvider'; -import { FileHighlighter } from './ui/FileHighlighter'; - import { SmellsCacheManager } from './context/SmellsCacheManager'; -import { registerFileSaveListener } from './listeners/fileSaveListener'; -import { registerWorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; - -import { checkServerStatus } from './api/backend'; -import { loadSmells } from './utils/smellsData'; -import { LineSelectionManager } from './ui/LineSelection'; - /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. * @param context - The VS Code extension context. */ export function activate(context: vscode.ExtensionContext): void { - console.log('Activating Eco-Optimizer extension...'); - - loadSmells(); - - // Initialize the SmellsCacheManager for managing caching of smells and file hashes. const smellsCacheManager = new SmellsCacheManager(context); - - // Initialize the Code Smells View. const smellsViewProvider = new SmellsViewProvider(context); const codeSmellsView = vscode.window.createTreeView('ecooptimizer.smellsView', { treeDataProvider: smellsViewProvider, @@ -58,13 +26,6 @@ export function activate(context: vscode.ExtensionContext): void { showCollapseAll: true, }); - // Start periodic backend status checks (every 10 seconds). - checkServerStatus(); - setInterval(checkServerStatus, 10000); - - //////////////////////////////////////////////// - // WORKSPACE CONFIGURATION COMMANDS - //////////////////////////////////////////////// const workspaceConfigured = Boolean( context.workspaceState.get('workspaceConfiguredPath'), ); @@ -76,226 +37,23 @@ export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.configureWorkspace', () => - configureWorkspace(context, smellsViewProvider), - ), - ); - - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.resetConfiguration', () => - resetConfiguration(context, smellsCacheManager, smellsViewProvider), - ), - ); - - //////////////////////////////////////////////// - - // Initialize the Filter Smells View. - const filterSmellsProvider = new FilterViewProvider( - context, - smellsCacheManager, - smellsViewProvider, - metricsViewProvider, - ); - const filterSmellsView = vscode.window.createTreeView('ecooptimizer.filterView', { - treeDataProvider: filterSmellsProvider, - showCollapseAll: true, - }); - - // Associate the TreeView instance with the provider. - filterSmellsProvider.setTreeView(filterSmellsView); - - // Register filter-related commands. - registerFilterSmellCommands(context, filterSmellsProvider); - - // Register code smell analysis commands. - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.openFile', openFile), - ); - - context.subscriptions.push( - vscode.commands.registerCommand( - 'ecooptimizer.detectSmellsFolder', - (folderPath) => { - try { - detectSmellsFolder(smellsCacheManager, smellsViewProvider, folderPath); - } catch (error: any) { - vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); - } - }, - ), - ); - - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileUri) => { - try { - detectSmellsFile(smellsCacheManager, smellsViewProvider, fileUri); - } catch (error: any) { - vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); - } - }), - ); - - // Initialize the RefactoringDetailsViewProvider - const refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); - // eslint-disable-next-line unused-imports/no-unused-vars - const refactoringDetailsView = vscode.window.createTreeView( - 'ecooptimizer.refactoringDetails', - { - treeDataProvider: refactoringDetailsViewProvider, - }, - ); - - // Register the export command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.exportMetricsData', () => - exportMetricsData(context), - ), - ); - - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.metricsView.refresh', () => { - metricsViewProvider.refresh(); - }), - ); - - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.clearMetricsData', () => { - vscode.window - .showWarningMessage( - 'Are you sure you want to clear the metrics data? This action is irreversible, and the data will be permanently lost unless exported.', - { modal: true }, - 'Yes', - 'No', - ) - .then((selection) => { - if (selection === 'Yes') { - context.workspaceState.update( - envConfig.WORKSPACE_METRICS_DATA!, - undefined, - ); - vscode.window.showInformationMessage('Metrics data has been cleared.'); - } - }); - metricsViewProvider.refresh(); - }), - ); - - // Reset the refactoring details view initially - refactoringDetailsViewProvider.resetRefactoringDetails(); - - // Register the refactorSmell command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.refactorSmell', (fileUri) => { - // Extract the smell ID from the fileUri string (e.g., "(aa7) R0913: Line 15") - const smellIdMatch = fileUri.match(/\(ID:\s*([^)]+)\)/); - const smellId = smellIdMatch ? smellIdMatch[1] : null; - - if (!smellId) { - console.error('No smell ID found in the file URI:', fileUri); - return; - } - - // Retrieve the smell object by ID using the cache manager - const smell = smellsCacheManager.getSmellById(smellId); - if (!smell) { - console.error('No smell found with ID:', smellId); - return; - } - - // Print the smell object to the console - console.log('Smell Object:', smell); - - // Call the refactorSmell function with only the smell object - refactorSmell(smellsViewProvider, refactoringDetailsViewProvider, smell); - }), - ); - - // Register the acceptRefactoring command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', () => - acceptRefactoring( - refactoringDetailsViewProvider, - metricsViewProvider, - smellsCacheManager, - smellsViewProvider, - ), - ), - ); - - // Register the rejectRefactoring command - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', () => - rejectRefactoring(refactoringDetailsViewProvider), + configureWorkspace(context, smellsViewProvider, metricsViewProvider), ), ); - // Register the command to open the diff editor context.subscriptions.push( vscode.commands.registerCommand( - 'ecooptimizer.openDiffEditor', - (originalFilePath: string, refactoredFilePath: string) => { - // Get the file name for the diff editor title - const fileName = path.basename(originalFilePath); - - // Show the diff editor with the updated title - const originalUri = vscode.Uri.file(originalFilePath); - const refactoredUri = vscode.Uri.file(refactoredFilePath); - vscode.commands.executeCommand( - 'vscode.diff', - originalUri, - refactoredUri, - `Refactoring Comparison (${fileName})`, - { - preview: false, - }, - ); - }, + 'ecooptimizer.resetConfiguration', + () => + resetConfiguration( + context, + smellsCacheManager, + smellsViewProvider, + metricsViewProvider, + ), + smellsViewProvider.refresh(), ), ); - - // Register the "Jump to Smell" command. - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.jumpToSmell', jumpToSmell), - ); - - // Register the "Clear Smells Cache" command. - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.wipeWorkCache', async () => { - await wipeWorkCache(smellsCacheManager, smellsViewProvider); - }), - ); - - // Initialize the FileHighlighter for highlighting code smells. - const fileHighlighter = FileHighlighter.getInstance(smellsCacheManager); - - fileHighlighter.updateHighlightsForVisibleEditors(); - - context.subscriptions.push( - vscode.window.onDidChangeVisibleTextEditors((editors) => { - editors.forEach((editor) => { - fileHighlighter.highlightSmells(editor); - }); - }), - ); - - const lineSelectManager = new LineSelectionManager(smellsCacheManager); - context.subscriptions.push( - vscode.window.onDidChangeTextEditorSelection((event) => { - console.log('Eco: Detected line selection event'); - lineSelectManager.commentLine(event.textEditor); - }), - ); - - // Register the file save listener to detect outdated files. - const fileSaveListener = registerFileSaveListener( - smellsCacheManager, - smellsViewProvider, - ); - context.subscriptions.push(fileSaveListener); - - // Register the workspace modified listener - const workspaceModifiedListener = - registerWorkspaceModifiedListener(metricsViewProvider); - context.subscriptions.push(workspaceModifiedListener); } /** diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 4dcf917..0b46262 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -1,125 +1,95 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { SmellsViewStateManager } from '../managers/SmellsViewStateManager'; -import { SmellsViewUIManager } from '../managers/SmellsViewUIManager'; -export class SmellsViewProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; +export class SmellsViewProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; - private stateManager: SmellsViewStateManager; - private uiManager: SmellsViewUIManager; + constructor(private context: vscode.ExtensionContext) {} - constructor(private context: vscode.ExtensionContext) { - this.stateManager = new SmellsViewStateManager(); - this.uiManager = new SmellsViewUIManager(this.stateManager); - } - - /** - * Refreshes the tree view, triggering a UI update. - */ refresh(): void { - this._onDidChangeTreeData.fire(undefined); + this._onDidChangeTreeData.fire(); } - /** - * Returns a tree item representing a file, folder, or detected smell. - * @param element - The file or folder path, or a detected smell. - */ - getTreeItem(element: string): vscode.TreeItem { - return this.uiManager.createTreeItem(element); + getTreeItem(element: TreeItem): vscode.TreeItem { + return element; } - /** - * Retrieves child elements for a given tree item. - * @param element - The parent tree item (optional). - */ - async getChildren(element?: string): Promise { - if (!element) { - const configuredPath = this.context.workspaceState.get( - 'workspaceConfiguredPath', - ); - return configuredPath ? [configuredPath] : []; + async getChildren(element?: TreeItem): Promise { + const rootPath = this.context.workspaceState.get( + 'workspaceConfiguredPath', + ); + if (!rootPath) { + return []; } - const isDirectory = fs.existsSync(element) && fs.statSync(element).isDirectory(); - - if (isDirectory) { - return fs - .readdirSync(element) - .filter((file) => file.endsWith('.py')) - .map((file) => path.join(element, file)); + // Root level + if (!element) { + const stat = fs.statSync(rootPath); + if (stat.isFile()) { + return [this.createTreeItem(rootPath, true)]; + } else if (stat.isDirectory()) { + return this.readDirectory(rootPath); + } } - // Check if the file is outdated - if (this.stateManager.isFileOutdated(element)) { - return []; // Return an empty array if the file is outdated + // Nested level (directory) + if (element && element.resourceUri) { + return this.readDirectory(element.resourceUri.fsPath); } - // If the file is not outdated, return the detected smells - const smells = this.stateManager.getSmellsForFile(element); - return smells.map((smell) => { - const smellItem = `${smell.messageId}: Line ${smell.occurences - .map((o) => o.line) - .join(', ')} (ID: ${smell.id})`; - this.stateManager.mapSmellToFile(smellItem, element); - return smellItem; - }); + return []; } - /** - * Updates the detected smells for a file and refreshes the tree view. - * @param filePath - The analyzed file path. - * @param smells - The detected smells in the file. - */ - updateSmells(filePath: string, smells: Smell[]): void { - this.stateManager.updateSmells(filePath, smells); - this.refresh(); - } + private readDirectory(dirPath: string): TreeItem[] { + const children: TreeItem[] = []; + + try { + const entries = fs.readdirSync(dirPath); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry); + const stat = fs.statSync(fullPath); + + // If directory, always include + if (stat.isDirectory()) { + children.push(this.createTreeItem(fullPath, false)); + } + // If file, only include if it's a .py file + else if (stat.isFile() && entry.endsWith('.py')) { + children.push(this.createTreeItem(fullPath, true)); + } + } + } catch (err) { + console.error(`Failed to read directory ${dirPath}:`, err); + } - /** - * Marks a file as outdated, updating its appearance in the UI. - * @param filePath - The path of the modified file. - */ - markFileAsOutdated(filePath: string): void { - this.stateManager.markFileAsOutdated(filePath); - this.refresh(); + return children; } - /** - * Updates the status of a specific file or folder. - * @param element - The file or folder path. - * @param status - The new status to set. - */ - updateStatus(element: string, status: string): void { - this.stateManager.updateFileStatus(element, status); - this.refresh(); - } + private createTreeItem(filePath: string, isFile: boolean): TreeItem { + const label = path.basename(filePath); + const collapsibleState = isFile + ? vscode.TreeItemCollapsibleState.None + : vscode.TreeItemCollapsibleState.Collapsed; - /** - * Clears the outdated status for a file. - * @param filePath - The path of the file to clear. - */ - clearOutdatedStatus(filePath: string): void { - this.stateManager.clearOutdatedStatus(filePath); - this.refresh(); - } + const contextValue = isFile ? 'file' : 'directory'; - /** - * Checks if a file is marked as outdated. - * @param filePath - The path of the file to check. - * @returns `true` if the file is outdated, `false` otherwise. - */ - isFileOutdated(filePath: string): boolean { - return this.stateManager.isFileOutdated(filePath); + return new TreeItem(label, filePath, collapsibleState, contextValue); } +} - /** - * Clears all detected smells and resets file statuses. - */ - resetAllSmells(): void { - this.stateManager.resetAllSmells(); - this.refresh(); +class TreeItem extends vscode.TreeItem { + constructor( + label: string, + public readonly fullPath: string, + collapsibleState: vscode.TreeItemCollapsibleState, + contextValue: string, + ) { + super(label, collapsibleState); + this.resourceUri = vscode.Uri.file(fullPath); + this.contextValue = contextValue; } } From b183430b3b96775ac83dd08ee2b20692c8d2837b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 14:26:58 -0400 Subject: [PATCH 042/121] readded the statuses for the smells view --- src/extension.ts | 5 + src/listeners/fileSaveListener.ts | 38 ---- src/listeners/workspaceModifiedListener.ts | 191 ++++++++++++++++++--- src/providers/SmellsViewProvider.ts | 35 +++- src/utils/fileStatus.ts | 65 +++++++ 5 files changed, 260 insertions(+), 74 deletions(-) delete mode 100644 src/listeners/fileSaveListener.ts create mode 100644 src/utils/fileStatus.ts diff --git a/src/extension.ts b/src/extension.ts index dadf818..7d28bf4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,7 @@ import { SmellsViewProvider } from './providers/SmellsViewProvider'; import { MetricsViewProvider } from './providers/MetricsViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; +import { openFile } from './commands/openFile'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -54,6 +55,10 @@ export function activate(context: vscode.ExtensionContext): void { smellsViewProvider.refresh(), ), ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.openFile', openFile), + ); } /** diff --git a/src/listeners/fileSaveListener.ts b/src/listeners/fileSaveListener.ts deleted file mode 100644 index 900764a..0000000 --- a/src/listeners/fileSaveListener.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as vscode from 'vscode'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; -import path from 'path'; - -/** - * Listens for file save events to detect outdated files. - * @param smellsCacheManager - Manages the caching of smells and file hashes. - * @param smellsDisplayProvider - The UI provider for updating the tree view. - */ -export function registerFileSaveListener( - smellsCacheManager: SmellsCacheManager, - smellsDisplayProvider: SmellsViewProvider, -): vscode.Disposable { - return vscode.workspace.onDidSaveTextDocument(async (document) => { - const filePath = document.fileName; - - // Ignore files that have no cached smells - const cachedSmells = smellsCacheManager.getCachedSmells(filePath); - if (!cachedSmells) return; - - const updated = await smellsCacheManager.updateFileHash( - filePath, - document.getText(), - ); - if (updated) { - vscode.window.showWarningMessage( - `The file "${path.basename( - filePath, - )}" has been modified since the last analysis.`, - ); - - await smellsCacheManager.clearCachedSmellsForFile(filePath); - // Mark file as outdated in the UI - smellsDisplayProvider.markFileAsOutdated(filePath); - } - }); -} diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index 416c1d7..4e81f21 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -1,34 +1,169 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; + +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; /** - * Registers a listener for workspace modifications (file creation, deletion, and changes) - * and refreshes the MetricsViewProvider when any of these events occur. - * - * @param metricsViewProvider - The MetricsViewProvider instance to refresh. - * @returns A disposable that can be used to unregister the listener. + * Listens for workspace modifications (file creation, deletion, and changes) + * and refreshes the SmellsViewProvider and MetricsViewProvider when any of these events occur. */ -export function registerWorkspaceModifiedListener( - metricsViewProvider: MetricsViewProvider, -): vscode.Disposable { - const watcher = vscode.workspace.createFileSystemWatcher('**/*'); - - const onDidCreateDisposable = watcher.onDidCreate(() => { - metricsViewProvider.refresh(); - }); - - const onDidChangeDisposable = watcher.onDidChange(() => { - metricsViewProvider.refresh(); - }); - - const onDidDeleteDisposable = watcher.onDidDelete(() => { - metricsViewProvider.refresh(); - }); - - return vscode.Disposable.from( - watcher, - onDidCreateDisposable, - onDidChangeDisposable, - onDidDeleteDisposable, - ); +export class WorkspaceModifiedListener { + private fileWatcher: vscode.FileSystemWatcher | undefined; + private textDocumentChangeListener: vscode.Disposable | undefined; + private pollingInterval: NodeJS.Timeout | undefined; + + constructor( + private context: vscode.ExtensionContext, + private smellsCacheManager: SmellsCacheManager, + private smellsViewProvider: SmellsViewProvider, + private metricsViewProvider: MetricsViewProvider, + ) { + this.initializeFileWatcher(); + this.initializeTextDocumentListener(); + this.initializePolling(); + } + + /** + * Initializes the file system watcher for the configured workspace. + */ + private initializeFileWatcher(): void { + const configuredPath = this.context.workspaceState.get( + 'workspaceConfiguredPath', + ); + if (!configuredPath) { + return; // No workspace configured + } + + console.log('Watching workspace:', configuredPath); + + this.fileWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(configuredPath, '**/*.py'), + false, // do not ignore create events + false, // do not ignore change events + false, // do not ignore delete events + ); + + this.fileWatcher.onDidCreate(() => { + console.log('A Python file was created.'); + this.refreshViews(); + }); + + this.fileWatcher.onDidChange((uri) => { + console.log('A Python file was modified and saved.'); + this.handleFileChange(uri.fsPath); + }); + + this.fileWatcher.onDidDelete((uri) => { + console.log('A Python file was deleted.'); + this.handleFileDeletion(uri.fsPath); + }); + } + + /** + * Initializes the text document change listener. + */ + private initializeTextDocumentListener(): void { + this.textDocumentChangeListener = vscode.workspace.onDidChangeTextDocument( + (event) => { + const filePath = event.document.uri.fsPath; + if (filePath.endsWith('.py')) { + console.log(`File ${filePath} was modified (unsaved changes).`); + this.handleFileChange(filePath); + } + }, + ); + } + + /** + * Initializes polling to detect external modifications. + */ + private initializePolling(): void { + const pollingIntervalMs = 5000; // Poll every 5 seconds + this.pollingInterval = setInterval(() => { + this.checkForExternalModifications(); + }, pollingIntervalMs); + } + + /** + * Checks for external modifications by comparing file modification times. + */ + private async checkForExternalModifications(): Promise { + const configuredPath = this.context.workspaceState.get( + 'workspaceConfiguredPath', + ); + if (!configuredPath) { + return; + } + + const cache = this.smellsCacheManager.getFullSmellCache(); + for (const filePath in cache) { + try { + const stats = await fs.promises.stat(filePath); + const lastModified = stats.mtimeMs; + + const cachedStats = this.context.workspaceState.get( + `fileStats:${filePath}`, + ); + if (cachedStats && lastModified > cachedStats) { + console.log(`External modification detected in file: ${filePath}`); + this.handleFileChange(filePath); + } + + await this.context.workspaceState.update( + `fileStats:${filePath}`, + lastModified, + ); + } catch (error) { + console.error(`Error checking file ${filePath}:`, error); + } + } + } + + /** + * Handles file changes by clearing the cache for the modified file and marking it as outdated. + * @param filePath - The path of the modified file. + */ + private async handleFileChange(filePath: string): Promise { + if (this.smellsCacheManager.hasCachedSmells(filePath)) { + await this.smellsCacheManager.clearCachedSmellsForFile(filePath); + this.smellsViewProvider.updateStatus(filePath, 'outdated'); + } + this.refreshViews(); + } + + /** + * Handles file deletions by clearing the cache for the deleted file and refreshing the tree view. + * @param filePath - The path of the deleted file. + */ + private async handleFileDeletion(filePath: string): Promise { + if (this.smellsCacheManager.hasCachedSmells(filePath)) { + await this.smellsCacheManager.clearCachedSmellsForFile(filePath); + } + this.refreshViews(); + } + + /** + * Refreshes both the SmellsViewProvider and MetricsViewProvider. + */ + private refreshViews(): void { + this.smellsViewProvider.refresh(); + this.metricsViewProvider.refresh(); + } + + /** + * Disposes the file system watcher, text document listener, and polling interval. + */ + public dispose(): void { + if (this.fileWatcher) { + this.fileWatcher.dispose(); + } + if (this.textDocumentChangeListener) { + this.textDocumentChangeListener.dispose(); + } + if (this.pollingInterval) { + clearInterval(this.pollingInterval); + } + } } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 0b46262..7241e30 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; +import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; export class SmellsViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = @@ -8,12 +9,20 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + // File statuses (e.g. "queued", "failed", "outdated") + private fileStatuses: Map = new Map(); + constructor(private context: vscode.ExtensionContext) {} refresh(): void { this._onDidChangeTreeData.fire(); } + setStatus(filePath: string, status: string): void { + this.fileStatuses.set(filePath, status); + this._onDidChangeTreeData.fire(); // Trigger UI update + } + getTreeItem(element: TreeItem): vscode.TreeItem { return element; } @@ -26,7 +35,6 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { return []; } - // Root level if (!element) { const stat = fs.statSync(rootPath); if (stat.isFile()) { @@ -36,7 +44,6 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { } } - // Nested level (directory) if (element && element.resourceUri) { return this.readDirectory(element.resourceUri.fsPath); } @@ -53,12 +60,9 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { const fullPath = path.join(dirPath, entry); const stat = fs.statSync(fullPath); - // If directory, always include if (stat.isDirectory()) { children.push(this.createTreeItem(fullPath, false)); - } - // If file, only include if it's a .py file - else if (stat.isFile() && entry.endsWith('.py')) { + } else if (stat.isFile() && entry.endsWith('.py')) { children.push(this.createTreeItem(fullPath, true)); } } @@ -74,10 +78,17 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { const collapsibleState = isFile ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed; - const contextValue = isFile ? 'file' : 'directory'; - return new TreeItem(label, filePath, collapsibleState, contextValue); + const status = this.fileStatuses.get(filePath) ?? 'not_yet_detected'; // Default + const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); + const tooltip = isFile ? getStatusMessage(status) : undefined; + + const item = new TreeItem(label, filePath, collapsibleState, contextValue); + item.iconPath = icon; + item.tooltip = tooltip; + + return item; } } @@ -91,5 +102,13 @@ class TreeItem extends vscode.TreeItem { super(label, collapsibleState); this.resourceUri = vscode.Uri.file(fullPath); this.contextValue = contextValue; + + if (contextValue === 'file') { + this.command = { + title: 'Open File', + command: 'vscode.open', + arguments: [this.resourceUri], + }; + } } } diff --git a/src/utils/fileStatus.ts b/src/utils/fileStatus.ts new file mode 100644 index 0000000..214d3bb --- /dev/null +++ b/src/utils/fileStatus.ts @@ -0,0 +1,65 @@ +// src/utils/fileStatus.ts + +import * as vscode from 'vscode'; + +/** + * Retrieves the appropriate VS Code icon based on the smell analysis status. + * @param status - The analysis status. + * @returns The corresponding VS Code theme icon. + */ +export function getStatusIcon(status: string): vscode.ThemeIcon { + switch (status) { + case 'queued': + return new vscode.ThemeIcon( + 'sync~spin', + new vscode.ThemeColor('charts.yellow'), + ); + case 'passed': + return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.green')); + case 'no_issues': + return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.blue')); + case 'failed': + return new vscode.ThemeIcon('error', new vscode.ThemeColor('charts.red')); + case 'outdated': + return new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.orange')); + case 'server_down': + return new vscode.ThemeIcon( + 'server-process', + new vscode.ThemeColor('charts.red'), + ); + case 'refactoring': + return new vscode.ThemeIcon('robot', new vscode.ThemeColor('charts.purple')); + case 'accept-refactoring': + return new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.yellow')); + default: + return new vscode.ThemeIcon('circle-outline'); + } +} + +/** + * Retrieves the status message corresponding to the smell analysis state. + * @param status - The analysis status. + * @returns A descriptive status message. + */ +export function getStatusMessage(status: string): string { + switch (status) { + case 'queued': + return 'Analyzing Smells'; + case 'passed': + return 'Smells Successfully Detected'; + case 'failed': + return 'Error Detecting Smells'; + case 'no_issues': + return 'No Smells Found'; + case 'outdated': + return 'File Outdated - Needs Reanalysis'; + case 'server_down': + return 'Server Unavailable'; + case 'refactoring': + return 'Refactoring Currently Ongoing'; + case 'accept-refactoring': + return 'Successfully Refactored - Needs Reanalysis'; + default: + return 'Smells Not Yet Detected'; + } +} From 021d5f60a322ecd851796528edbcf8bbb5dc2f6d Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 15:30:14 -0400 Subject: [PATCH 043/121] clean --- package.json | 61 +++++++++--- src/commands/detectSmells.ts | 145 +++++----------------------- src/extension.ts | 37 +++++++ src/providers/FilterViewProvider.ts | 12 +-- 4 files changed, 110 insertions(+), 145 deletions(-) diff --git a/package.json b/package.json index 11b48ed..a9cda3a 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,11 @@ "id": "ecooptimizer.metricsView", "name": "Carbon Metrics", "icon": "assets/eco-icon.png" + }, + { + "id": "ecooptimizer.filterView", + "name": "Filter Smells", + "icon": "assets/eco-icon.png" } ] }, @@ -164,25 +169,36 @@ "category": "Eco" }, { - "command": "ecooptimizer.openFile", - "title": "Open File", - "category": "Eco", - "enablement": "false" + "command": "ecooptimizer.toggleSmellFilter", + "title": "Toggle Smell", + "category": "Eco" + }, + { + "command": "ecooptimizer.editSmellFilterOption", + "title": "Edit Option", + "icon": "$(edit)", + "category": "Eco" }, { - "command": "ecooptimizer.exportMetricsData", - "title": "Export Metrics Data as JSON", + "command": "ecooptimizer.selectAllFilterSmells", + "title": "Select All Smells", "category": "Eco" }, { - "command": "ecooptimizer.clearMetricsData", - "title": "Clear Metrics Data", + "command": "ecooptimizer.deselectAllFilterSmells", + "title": "Deselect All Smells", "category": "Eco" }, { - "command": "ecooptimizer.metricsView.refresh", - "title": "Refresh Metrics Data", - "icon": "$(sync)", + "command": "ecooptimizer.openFile", + "title": "Open File", + "category": "Eco", + "enablement": "false" + }, + { + "command": "ecooptimizer.detectSmellsFile", + "title": "Detect Smells", + "icon": "$(search)", "category": "Eco" } ], @@ -192,9 +208,30 @@ "command": "ecooptimizer.resetConfiguration", "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured", "group": "resource" + }, + { + "command": "ecooptimizer.selectAllFilterSmells", + "when": "view == ecooptimizer.filterView", + "group": "resource" + }, + { + "command": "ecooptimizer.deselectAllFilterSmells", + "when": "view == ecooptimizer.filterView", + "group": "resource" } ], - "view/item/context": [] + "view/item/context": [ + { + "command": "ecooptimizer.editSmellFilterOption", + "when": "viewItem == smellOption", + "group": "inline" + }, + { + "command": "ecooptimizer.detectSmellsFile", + "when": "view == ecooptimizer.smellsView && viewItem == file", + "group": "inline" + } + ] }, "configuration": { "title": "EcoOptimizer", diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 693cfe6..f36d166 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -1,40 +1,31 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; import { fetchSmells } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { getEnabledSmells } from '../utils/smellsData'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; /** * Detects code smells for a given file. * Uses cached smells if available; otherwise, fetches from the backend. * - * @param smellsCacheManager - Manages caching of smells and file hashes. - * @param treeDataProvider - UI provider for updating tree view. - * @param fileUri - The VS Code file URI or string path of the file to analyze. + * @param smellsViewProvider - UI provider for updating tree view. + * @param filePath - The VS Code file URI or string path of the file to analyze. */ export async function detectSmellsFile( - smellsCacheManager: SmellsCacheManager, - treeDataProvider: SmellsViewProvider, - fileUri: vscode.Uri | string, + smellsViewProvider: SmellsViewProvider, + filePath: string, ): Promise { - if (!fileUri) { - vscode.window.showErrorMessage('No file selected for analysis.'); + // STEP 2: Check if server is down + if (serverStatus.getStatus() === ServerStatusType.DOWN) { + vscode.window.showWarningMessage( + 'Action blocked: Server is down and no cached smells exist for this file version.', + ); + smellsViewProvider.setStatus(filePath, 'server_down'); return; } - const filePath = typeof fileUri === 'string' ? fileUri : fileUri.fsPath; - - // Handle outdated files before proceeding - if (treeDataProvider.isFileOutdated(filePath)) { - treeDataProvider.updateStatus(filePath, 'queued'); - } - + // STEP 3: Get enabled smells const enabledSmells = getEnabledSmells(); - - // Ensure that at least one smell type is enabled if (Object.keys(enabledSmells).length === 0) { vscode.window.showWarningMessage( 'No enabled smells found. Please configure enabled smells in the settings.', @@ -42,121 +33,29 @@ export async function detectSmellsFile( return; } - // Check if smells are already cached - const cachedSmells = smellsCacheManager.getCachedSmells(filePath); - console.log('Cached smells:', cachedSmells); - if (cachedSmells !== undefined) { - vscode.window.showInformationMessage( - `Using cached smells for ${path.basename(filePath)}.`, - ); - - if (cachedSmells.length > 0) { - treeDataProvider.updateSmells(filePath, cachedSmells); - } else { - treeDataProvider.updateStatus(filePath, 'no_issues'); - } - - return; - } - - if (serverStatus.getStatus() === ServerStatusType.DOWN) { - vscode.window.showWarningMessage( - 'Action blocked: Server is down and no cached smells exist for this file version.', - ); - treeDataProvider.updateStatus(filePath, 'server_down'); - return; - } + const enabledSmellsForBackend = Object.fromEntries( + Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), + ); - // Update UI to indicate the file is queued for analysis - treeDataProvider.updateStatus(filePath, 'queued'); + // STEP 4: Set status to queued + smellsViewProvider.setStatus(filePath, 'queued'); try { - // Prepare enabled smells for backend request - const enabledSmellsForBackend = Object.fromEntries( - Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), - ); - const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); if (status === 200) { - // Cache detected smells, even if no smells are found - await smellsCacheManager.setCachedSmells(filePath, smells); - const smellsWithID = smellsCacheManager.getCachedSmells(filePath) || []; - - // Remove the file from modifiedFiles after re-analysis - treeDataProvider.clearOutdatedStatus(filePath); - if (smells.length > 0) { - treeDataProvider.updateSmells(filePath, smellsWithID); - vscode.window.showInformationMessage( - `Analysis complete: Detected ${ - smells.length - } code smell(s) in ${path.basename(filePath)}.`, - ); + smellsViewProvider.setStatus(filePath, 'passed'); + // TODO: addSmellsToTreeView(smellsViewProvider, filePath, smells); } else { - treeDataProvider.updateStatus(filePath, 'no_issues'); // Update status based on backend result - vscode.window.showInformationMessage( - `Analysis complete: No code smells found in ${path.basename(filePath)}.`, - ); + smellsViewProvider.setStatus(filePath, 'no_issues'); } } else { - throw new Error(`Unexpected status code: ${status}`); + smellsViewProvider.setStatus(filePath, 'failed'); + vscode.window.showErrorMessage(`Analysis failed (status ${status}).`); } } catch (error: any) { - // Handle errors during analysis - treeDataProvider.updateStatus(filePath, 'failed'); + smellsViewProvider.setStatus(filePath, 'failed'); vscode.window.showErrorMessage(`Analysis failed: ${error.message}`); } } - -/** - * Detects code smells for all Python files within a folder. - * Uses cached smells where available. - * - * @param smellsCacheManager - Manages caching of smells and file hashes. - * @param treeDataProvider - UI provider for updating tree view. - * @param folderPath - The absolute path of the folder to analyze. - */ -export async function detectSmellsFolder( - smellsCacheManager: SmellsCacheManager, - treeDataProvider: SmellsViewProvider, - folderPath: string, -): Promise { - // Notify the user that folder analysis has started - vscode.window.showInformationMessage( - `Detecting code smells for all Python files in: ${path.basename(folderPath)}`, - ); - - // Retrieve all Python files in the specified folder - const pythonFiles = fs - .readdirSync(folderPath) - .filter((file) => file.endsWith('.py')) - .map((file) => path.join(folderPath, file)); - - // Ensure that Python files exist in the folder before analysis - if (pythonFiles.length === 0) { - vscode.window.showWarningMessage( - `No Python files found in ${path.basename(folderPath)}.`, - ); - return; - } - - // Retrieve enabled smells from configuration - const enabledSmells = getEnabledSmells(); - - // Ensure that at least one smell type is enabled - if (Object.keys(enabledSmells).length === 0) { - vscode.window.showWarningMessage( - 'No enabled smells found. Please configure enabled smells in the settings.', - ); - return; - } - - // Analyze each Python file in the folder - for (const file of pythonFiles) { - await detectSmellsFile(smellsCacheManager, treeDataProvider, file); - } - - // Refresh UI to reflect folder analysis results - treeDataProvider.refresh(); -} diff --git a/src/extension.ts b/src/extension.ts index 7d28bf4..b162b64 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,12 +8,17 @@ import { MetricsViewProvider } from './providers/MetricsViewProvider'; import { SmellsCacheManager } from './context/SmellsCacheManager'; import { openFile } from './commands/openFile'; +import { detectSmellsFile } from './commands/detectSmells'; +import { FilterViewProvider } from './providers/FilterViewProvider'; +import { registerFilterSmellCommands } from './commands/filterSmells'; +import { loadSmells } from './utils/smellsData'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. * @param context - The VS Code extension context. */ export function activate(context: vscode.ExtensionContext): void { + loadSmells(); const smellsCacheManager = new SmellsCacheManager(context); const smellsViewProvider = new SmellsViewProvider(context); const codeSmellsView = vscode.window.createTreeView('ecooptimizer.smellsView', { @@ -56,9 +61,41 @@ export function activate(context: vscode.ExtensionContext): void { ), ); + // Initialize the Filter Smells View. + const filterSmellsProvider = new FilterViewProvider(context, metricsViewProvider); + const filterSmellsView = vscode.window.createTreeView('ecooptimizer.filterView', { + treeDataProvider: filterSmellsProvider, + showCollapseAll: true, + }); + + // Associate the TreeView instance with the provider. + filterSmellsProvider.setTreeView(filterSmellsView); + + // Register filter-related commands. + registerFilterSmellCommands(context, filterSmellsProvider); + context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.openFile', openFile), ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileItem) => { + try { + const filePath = fileItem?.resourceUri?.fsPath; + + if (!filePath) { + vscode.window.showWarningMessage( + 'No file selected or file path not found.', + ); + return; + } + + detectSmellsFile(smellsViewProvider, filePath); + } catch (error: any) { + vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); + } + }), + ); } /** diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index 13ea8ce..1a91e7a 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -1,7 +1,5 @@ import * as vscode from 'vscode'; import { FilterSmellConfig, getFilterSmells, saveSmells } from '../utils/smellsData'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { SmellsViewProvider } from './SmellsViewProvider'; import { MetricsViewProvider } from './MetricsViewProvider'; /** @@ -19,8 +17,6 @@ export class FilterViewProvider implements vscode.TreeDataProvider { - const cache = this.cacheManager.getFullSmellCache(); - - for (const filePath of Object.keys(cache)) { - await this.cacheManager.clearCachedSmellsForFile(filePath); - this.smellsViewProvider.markFileAsOutdated(filePath); - } + console.log('Invalidating cached smells for affected files...'); this.metricsViewProvider.refresh(); } From ba088f3533871b1910616a7266b3ebf060ddb9bc Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 17:15:44 -0400 Subject: [PATCH 044/121] clean --- src/api/backend.ts | 2 +- src/commands/detectSmells.ts | 27 +++- src/commands/refactorSmell.ts | 6 +- src/context/SmellsCacheManager.ts | 157 ++++----------------- src/extension.ts | 16 ++- src/listeners/workspaceModifiedListener.ts | 111 ++++----------- src/providers/FilterViewProvider.ts | 3 +- src/providers/SmellsViewProvider.ts | 8 ++ src/ui/LineSelection.ts | 4 +- src/utils/smellsData.ts | 3 +- test/mocks/contextManager-mock.ts | 8 +- test/mocks/vscode-mock.ts | 10 +- 12 files changed, 115 insertions(+), 240 deletions(-) diff --git a/src/api/backend.ts b/src/api/backend.ts index 9df6b6e..531dfbf 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -94,7 +94,7 @@ export async function backendRefactorSmell( const workspaceFolderPath = workspaceFolder.uri.fsPath; - console.log( + ecoOutput.appendLine( `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspaceFolderPath}"`, ); diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index f36d166..c6f12d7 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -3,6 +3,7 @@ import { fetchSmells } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { getEnabledSmells } from '../utils/smellsData'; import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; /** * Detects code smells for a given file. @@ -12,10 +13,24 @@ import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; * @param filePath - The VS Code file URI or string path of the file to analyze. */ export async function detectSmellsFile( - smellsViewProvider: SmellsViewProvider, filePath: string, + smellsViewProvider: SmellsViewProvider, + smellsCacheManager: SmellsCacheManager, ): Promise { - // STEP 2: Check if server is down + // STEP 0: Check cache first + if (smellsCacheManager.hasCachedSmells(filePath)) { + const cached = smellsCacheManager.getCachedSmells(filePath); + vscode.window.showInformationMessage('Using cached smells for this file.'); + if (cached && cached.length > 0) { + smellsViewProvider.setStatus(filePath, 'passed'); + // TODO: render cached smells in tree + } else { + smellsViewProvider.setStatus(filePath, 'no_issues'); + } + return; + } + + // STEP 1: Check if server is down if (serverStatus.getStatus() === ServerStatusType.DOWN) { vscode.window.showWarningMessage( 'Action blocked: Server is down and no cached smells exist for this file version.', @@ -24,7 +39,7 @@ export async function detectSmellsFile( return; } - // STEP 3: Get enabled smells + // STEP 2: Get enabled smells const enabledSmells = getEnabledSmells(); if (Object.keys(enabledSmells).length === 0) { vscode.window.showWarningMessage( @@ -37,7 +52,7 @@ export async function detectSmellsFile( Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), ); - // STEP 4: Set status to queued + // STEP 3: Queue analysis smellsViewProvider.setStatus(filePath, 'queued'); try { @@ -46,9 +61,11 @@ export async function detectSmellsFile( if (status === 200) { if (smells.length > 0) { smellsViewProvider.setStatus(filePath, 'passed'); - // TODO: addSmellsToTreeView(smellsViewProvider, filePath, smells); + await smellsCacheManager.setCachedSmells(filePath, smells); + // TODO: render smells in tree } else { smellsViewProvider.setStatus(filePath, 'no_issues'); + await smellsCacheManager.setCachedSmells(filePath, []); } } else { smellsViewProvider.setStatus(filePath, 'failed'); diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index f25d263..3200662 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -43,7 +43,7 @@ export async function refactorSmell( const refactoredData = await backendRefactorSmell(smell); // Log the response from the backend - console.log('Refactoring response:', refactoredData); + ecoOutput.appendLine('Refactoring response:', refactoredData); // Update the refactoring details view with the target file, affected files, and energy saved refactoringDetailsViewProvider.updateRefactoringDetails( @@ -116,10 +116,10 @@ export async function acceptRefactoring( const targetSmell = refactoringDetailsViewProvider.targetSmell; const file = vscode.Uri.file(targetFile.original).fsPath; - console.log('Energy: %d, smell: %s', energySaved, targetSmell); + ecoOutput.appendLine('Energy: %d, smell: %s', energySaved, targetSmell); if (energySaved && targetSmell) { - console.log('Updating metrics for', file); + ecoOutput.appendLine('Updating metrics for', file); metricsDataProvider.updateMetrics(file, energySaved, targetSmell); } diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index bc748b3..2e43ef8 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -1,17 +1,12 @@ import * as vscode from 'vscode'; import { createHash } from 'crypto'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { envConfig } from '../utils/envConfig'; /** - * Manages caching of detected smells and file hashes to avoid redundant backend calls. - * This class handles storing, retrieving, and clearing cached smells and file hashes, - * as well as refreshing the UI when the cache is updated. + * Manages caching of detected smells to avoid redundant backend calls. + * This class handles storing, retrieving, and clearing cached smells. */ export class SmellsCacheManager { - private cacheUpdatedEmitter = new vscode.EventEmitter(); - public readonly onSmellsUpdated = this.cacheUpdatedEmitter.event; - constructor(private context: vscode.ExtensionContext) {} // ============================ @@ -19,11 +14,11 @@ export class SmellsCacheManager { // ============================ /** - * Generates a unique string ID for a smell based on its content. + * Generates a unique 5-character ID for a smell based on its content. * The ID is derived from a SHA256 hash of the smell object. * * @param smell - The smell object to generate an ID for. - * @returns A unique string ID for the smell. + * @returns A unique 5-character string ID. */ private generateSmellId(smell: Smell): string { // Generate a SHA256 hash of the smell object @@ -31,39 +26,31 @@ export class SmellsCacheManager { .update(JSON.stringify(smell)) .digest('hex'); - // Use the first 8 characters of the hash as the ID - return smellHash.substring(0, 3); + // Use the first 5 characters of the hash as the ID + return smellHash.substring(0, 5); } /** - * Caches detected smells for a given file and assigns unique string IDs to each smell. - * The smells are stored in the workspace state for persistence. - * + * Caches detected smells for a given file and assigns unique IDs to each smell. * @param filePath - The absolute path of the file. * @param smells - The detected smells to store. */ public async setCachedSmells(filePath: string, smells: Smell[]): Promise { const cache = this.getFullSmellCache(); - // Assign unique string IDs to each smell - const smellsWithIds = smells.map((smell) => { - const id = this.generateSmellId(smell); - return { - ...smell, - id, // Add the unique string ID to the smell object - }; - }); + // Assign unique IDs to each smell + const smellsWithIds = smells.map((smell) => ({ + ...smell, + id: this.generateSmellId(smell), // Add a unique 5-character ID + })); // Update the cache with the new smells cache[filePath] = smellsWithIds; await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); - - this.cacheUpdatedEmitter.fire(filePath); } /** * Retrieves cached smells for a given file. - * * @param filePath - The absolute path of the file. * @returns An array of detected smells with unique IDs, or undefined if no smells are cached. */ @@ -73,9 +60,18 @@ export class SmellsCacheManager { } /** - * Retrieves a smell by its unique string ID. - * - * @param id - The unique string ID of the smell. + * Checks if a file has cached smells. + * @param filePath - The absolute path of the file. + * @returns `true` if the file has cached smells, `false` otherwise. + */ + public hasCachedSmells(filePath: string): boolean { + const cache = this.getFullSmellCache(); + return cache[filePath] !== undefined; + } + + /** + * Retrieves a smell by its unique ID. + * @param id - The unique ID of the smell. * @returns The smell object, or undefined if no smell matches the ID. */ public getSmellById(id: string): Smell | undefined { @@ -90,32 +86,18 @@ export class SmellsCacheManager { return undefined; } - /** - * Clears all cached smells from the workspace state. - * This forces a fresh analysis of all files when `detectSmellsFile` is called. - */ - public async clearSmellsCache(): Promise { - await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, undefined); - - this.cacheUpdatedEmitter.fire('all'); - } - /** * Clears cached smells for a specific file. - * * @param filePath - The path of the file to clear. */ public async clearCachedSmellsForFile(filePath: string): Promise { const cache = this.getFullSmellCache(); delete cache[filePath]; await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); - - this.cacheUpdatedEmitter.fire(filePath); } /** * Retrieves the entire smell cache from the workspace state. - * * @returns A record of file paths to their corresponding cached smells. */ public getFullSmellCache(): Record { @@ -124,95 +106,4 @@ export class SmellsCacheManager { {}, ); } - - // ============================ - // File Hash Caching Methods - // ============================ - - /** - * Computes a SHA256 hash of a file's contents and returns it as a string. - * - * @param content - The file content as a string. - * @returns A SHA256 hash string derived from the file content. - */ - private computeFileHash(content: string): string { - return createHash('sha256').update(content).digest('hex'); - } - - public wasFileModified(filePath: string, content: string): boolean { - const newHash = this.computeFileHash(content); - const oldHash = this.getStoredFileHash(filePath); - - return newHash !== oldHash; - } - - /** - * Stores a hash of a file's contents in the workspace state. - * - * @param filePath - The absolute path of the file. - * @param content - The file content to hash. - */ - public async updateFileHash(filePath: string, content: string): Promise { - const hashes = this.getFullFileHashCache(); - - let currentHash = hashes[filePath]; - const newHash = this.computeFileHash(content); - - if (!currentHash || currentHash !== newHash) { - hashes[filePath] = newHash; - await this.context.workspaceState.update( - envConfig.FILE_HASH_CACHE_KEY!, - hashes, - ); - return true; - } - - return false; - } - - /** - * Retrieves the stored hash for a given file. - * - * @param filePath - The absolute path of the file. - * @returns The stored hash as a string, or undefined if no hash is found. - */ - public getStoredFileHash(filePath: string): string | undefined { - const hashes = this.getFullFileHashCache(); - return hashes[filePath]; - } - - /** - * Retrieves the entire file hash cache from the workspace state. - * - * @returns A record of file paths to their corresponding SHA256 hashes. - */ - private getFullFileHashCache(): Record { - return this.context.workspaceState.get>( - envConfig.FILE_HASH_CACHE_KEY!, - {}, - ); - } - - // ============================ - // UI Refresh Methods - // ============================ - - /** - * Clears all cached smells and refreshes the UI. - * This method is used by both "Clear Smells Cache" and "Reset Configuration" commands. - * - * @param smellsDisplayProvider - The tree view provider responsible for the UI. - */ - public async clearCacheAndRefreshUI( - smellsDisplayProvider: SmellsViewProvider, - ): Promise { - // Remove all cached smells from the workspace state - await this.clearSmellsCache(); - - // Reset the UI state, including icons and dropdowns - // smellsDisplayProvider.resetAllSmells(); - - // Refresh the UI to reflect the cleared cache - smellsDisplayProvider.refresh(); - } } diff --git a/src/extension.ts b/src/extension.ts index b162b64..74d02bf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,7 @@ import * as vscode from 'vscode'; +export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); + import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; @@ -12,6 +14,7 @@ import { detectSmellsFile } from './commands/detectSmells'; import { FilterViewProvider } from './providers/FilterViewProvider'; import { registerFilterSmellCommands } from './commands/filterSmells'; import { loadSmells } from './utils/smellsData'; +import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -90,17 +93,26 @@ export function activate(context: vscode.ExtensionContext): void { return; } - detectSmellsFile(smellsViewProvider, filePath); + detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); } catch (error: any) { vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); } }), ); + + const workspaceModifiedListener = new WorkspaceModifiedListener( + context, + smellsCacheManager, + smellsViewProvider, + metricsViewProvider, + ); + + context.subscriptions.push(workspaceModifiedListener); } /** * Deactivates the Eco-Optimizer extension. */ export function deactivate(): void { - console.log('Deactivating Eco-Optimizer extension...'); + ecoOutput.appendLine('Deactivating Eco-Optimizer extension...\n'); } diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index 4e81f21..68109a3 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -1,18 +1,15 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; - import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; +import { ecoOutput } from '../extension'; /** - * Listens for workspace modifications (file creation, deletion, and changes) - * and refreshes the SmellsViewProvider and MetricsViewProvider when any of these events occur. + * Listens for workspace modifications (file creation, deletion, and saves) + * and refreshes the SmellsViewProvider and MetricsViewProvider accordingly. */ export class WorkspaceModifiedListener { private fileWatcher: vscode.FileSystemWatcher | undefined; - private textDocumentChangeListener: vscode.Disposable | undefined; - private pollingInterval: NodeJS.Timeout | undefined; constructor( private context: vscode.ExtensionContext, @@ -21,8 +18,6 @@ export class WorkspaceModifiedListener { private metricsViewProvider: MetricsViewProvider, ) { this.initializeFileWatcher(); - this.initializeTextDocumentListener(); - this.initializePolling(); } /** @@ -36,7 +31,7 @@ export class WorkspaceModifiedListener { return; // No workspace configured } - console.log('Watching workspace:', configuredPath); + ecoOutput.appendLine(`Watching workspace: ${configuredPath}`); this.fileWatcher = vscode.workspace.createFileSystemWatcher( new vscode.RelativePattern(configuredPath, '**/*.py'), @@ -46,81 +41,21 @@ export class WorkspaceModifiedListener { ); this.fileWatcher.onDidCreate(() => { - console.log('A Python file was created.'); + ecoOutput.appendLine('A Python file was created.'); this.refreshViews(); }); this.fileWatcher.onDidChange((uri) => { - console.log('A Python file was modified and saved.'); + ecoOutput.appendLine(`A Python file was modified and saved: ${uri.fsPath}`); this.handleFileChange(uri.fsPath); }); this.fileWatcher.onDidDelete((uri) => { - console.log('A Python file was deleted.'); + ecoOutput.appendLine(`A Python file was deleted: ${uri.fsPath}`); this.handleFileDeletion(uri.fsPath); }); } - /** - * Initializes the text document change listener. - */ - private initializeTextDocumentListener(): void { - this.textDocumentChangeListener = vscode.workspace.onDidChangeTextDocument( - (event) => { - const filePath = event.document.uri.fsPath; - if (filePath.endsWith('.py')) { - console.log(`File ${filePath} was modified (unsaved changes).`); - this.handleFileChange(filePath); - } - }, - ); - } - - /** - * Initializes polling to detect external modifications. - */ - private initializePolling(): void { - const pollingIntervalMs = 5000; // Poll every 5 seconds - this.pollingInterval = setInterval(() => { - this.checkForExternalModifications(); - }, pollingIntervalMs); - } - - /** - * Checks for external modifications by comparing file modification times. - */ - private async checkForExternalModifications(): Promise { - const configuredPath = this.context.workspaceState.get( - 'workspaceConfiguredPath', - ); - if (!configuredPath) { - return; - } - - const cache = this.smellsCacheManager.getFullSmellCache(); - for (const filePath in cache) { - try { - const stats = await fs.promises.stat(filePath); - const lastModified = stats.mtimeMs; - - const cachedStats = this.context.workspaceState.get( - `fileStats:${filePath}`, - ); - if (cachedStats && lastModified > cachedStats) { - console.log(`External modification detected in file: ${filePath}`); - this.handleFileChange(filePath); - } - - await this.context.workspaceState.update( - `fileStats:${filePath}`, - lastModified, - ); - } catch (error) { - console.error(`Error checking file ${filePath}:`, error); - } - } - } - /** * Handles file changes by clearing the cache for the modified file and marking it as outdated. * @param filePath - The path of the modified file. @@ -128,24 +63,40 @@ export class WorkspaceModifiedListener { private async handleFileChange(filePath: string): Promise { if (this.smellsCacheManager.hasCachedSmells(filePath)) { await this.smellsCacheManager.clearCachedSmellsForFile(filePath); - this.smellsViewProvider.updateStatus(filePath, 'outdated'); + this.smellsViewProvider.setStatus(filePath, 'outdated'); + + vscode.window.showInformationMessage( + `File modified: ${filePath}\nSmell results are now outdated. Please reanalyze.`, + ); } this.refreshViews(); } /** - * Handles file deletions by clearing the cache for the deleted file and refreshing the tree view. - * @param filePath - The path of the deleted file. + * Handles file deletions by clearing the cache and removing from the tree view. */ private async handleFileDeletion(filePath: string): Promise { + let removed = false; + if (this.smellsCacheManager.hasCachedSmells(filePath)) { await this.smellsCacheManager.clearCachedSmellsForFile(filePath); + removed = true; + } + + const removedFromTree = this.smellsViewProvider.removeFile(filePath); + removed ||= removedFromTree; + + if (removed) { + vscode.window.showInformationMessage( + `Removed deleted file from smells view: ${filePath}`, + ); } + this.refreshViews(); } /** - * Refreshes both the SmellsViewProvider and MetricsViewProvider. + * Refreshes both views. */ private refreshViews(): void { this.smellsViewProvider.refresh(); @@ -153,17 +104,11 @@ export class WorkspaceModifiedListener { } /** - * Disposes the file system watcher, text document listener, and polling interval. + * Disposes any resources. */ public dispose(): void { if (this.fileWatcher) { this.fileWatcher.dispose(); } - if (this.textDocumentChangeListener) { - this.textDocumentChangeListener.dispose(); - } - if (this.pollingInterval) { - clearInterval(this.pollingInterval); - } } } diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index 1a91e7a..412b07f 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { FilterSmellConfig, getFilterSmells, saveSmells } from '../utils/smellsData'; import { MetricsViewProvider } from './MetricsViewProvider'; +import { ecoOutput } from '../extension'; /** * Provides a tree view for filtering code smells within the VS Code extension. @@ -164,7 +165,7 @@ export class FilterViewProvider implements vscode.TreeDataProvider { - console.log('Invalidating cached smells for affected files...'); + ecoOutput.appendLine('Invalidating cached smells for affected files...'); this.metricsViewProvider.refresh(); } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 7241e30..3d7ebae 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -23,6 +23,14 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { this._onDidChangeTreeData.fire(); // Trigger UI update } + public removeFile(filePath: string): boolean { + const exists = this.fileStatuses.has(filePath); + if (exists) { + this.fileStatuses.delete(filePath); + } + return exists; + } + getTreeItem(element: TreeItem): vscode.TreeItem { return element; } diff --git a/src/ui/LineSelection.ts b/src/ui/LineSelection.ts index 9282c04..e2933af 100644 --- a/src/ui/LineSelection.ts +++ b/src/ui/LineSelection.ts @@ -23,7 +23,7 @@ export class LineSelectionManager { */ public removeLastComment(): void { if (this.decoration) { - console.log('Removing decoration'); + ecoOutput.appendLine('Removing decoration'); this.decoration.dispose(); } } @@ -60,7 +60,7 @@ export class LineSelectionManager { } const selectedLine = selection.start.line; - console.log(`selection: ${selectedLine}`); + ecoOutput.appendLine(`selection: ${selectedLine}`); const smellsAtLine = smells.filter((smell: Smell) => { return smell.occurences[0].line === selectedLine + 1; diff --git a/src/utils/smellsData.ts b/src/utils/smellsData.ts index 92102a1..c43d2c4 100644 --- a/src/utils/smellsData.ts +++ b/src/utils/smellsData.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; +import { ecoOutput } from '../extension'; /** * Defines the structure of the smell configuration in smells.json. @@ -46,7 +47,7 @@ export function loadSmells(): void { filterSmells = JSON.parse(fs.readFileSync(filePath, 'utf-8')); enabledSmells = parseSmells(filterSmells); - console.log('Smells loaded'); + ecoOutput.appendLine('\nSmells loaded\n'); } catch (error) { vscode.window.showErrorMessage( 'Error loading smells.json. Please check the file format.', diff --git a/test/mocks/contextManager-mock.ts b/test/mocks/contextManager-mock.ts index 745d9a6..81b4f7f 100644 --- a/test/mocks/contextManager-mock.ts +++ b/test/mocks/contextManager-mock.ts @@ -14,21 +14,21 @@ const contextStorage: ContextStorage = { const mockExtensionContext: Partial = { globalState: { get: jest.fn((key: string, defaultVal?: any) => { - console.log(`MOCK getGlobalData: ${key}`); + ecoOutput.appendLine(`MOCK getGlobalData: ${key}`); return contextStorage.globalState[key] ?? defaultVal; }), update: jest.fn(async (key: string, value: any) => { - console.log(`MOCK setGlobalData: ${key}:${value}`); + ecoOutput.appendLine(`MOCK setGlobalData: ${key}:${value}`); contextStorage.globalState[key] = value; }), } as any, // Casting to `any` to satisfy `vscode.ExtensionContext` workspaceState: { get: jest.fn((key: string, defaultVal?: any) => { - console.log(`MOCK getWorkspaceData: ${key}`); + ecoOutput.appendLine(`MOCK getWorkspaceData: ${key}`); return contextStorage.workspaceState[key] ?? defaultVal; }), update: jest.fn(async (key: string, value: any) => { - console.log(`MOCK setWorkspaceData ${key}:${value}`); + ecoOutput.appendLine(`MOCK setWorkspaceData ${key}:${value}`); contextStorage.workspaceState[key] = value; }), } as any, // Casting to `any` to satisfy `vscode.ExtensionContext` diff --git a/test/mocks/vscode-mock.ts b/test/mocks/vscode-mock.ts index e058729..45c4504 100644 --- a/test/mocks/vscode-mock.ts +++ b/test/mocks/vscode-mock.ts @@ -17,7 +17,7 @@ export const TextDocument = { fileName: config.filePath, languageId: 'python', lineAt: jest.fn((line: number) => { - console.log('MOCK lineAt:', line); + ecoOutput.appendLine('MOCK lineAt:', line); return { text: 'Mock line text', }; @@ -55,21 +55,21 @@ interface Window { export const window: Window = { showInformationMessage: jest.fn(async (message: string) => { - console.log('MOCK showInformationMessage:', message); + ecoOutput.appendLine('MOCK showInformationMessage:', message); return message; }), showErrorMessage: jest.fn(async (message: string) => { - console.log('MOCK showErrorMessage:', message); + ecoOutput.appendLine('MOCK showErrorMessage:', message); return message; }), showWarningMessage: jest.fn(async (message: string) => { - console.log('MOCK showWarningMessage:', message); + ecoOutput.appendLine('MOCK showWarningMessage:', message); return message; }), activeTextEditor: TextEditor, visibleTextEditors: [], createTextEditorDecorationType: jest.fn((_options: any) => { - console.log('MOCK createTextEditorDecorationType:'); + ecoOutput.appendLine('MOCK createTextEditorDecorationType:'); return textEditorDecorationType; }), }; From 42d907b4e7ccef12e972ba80ef950983f97e1981 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 17:56:04 -0400 Subject: [PATCH 045/121] clean --- src/commands/configureWorkspace.ts | 16 +++++- src/providers/SmellsViewProvider.ts | 88 +++++++++-------------------- src/utils/TreeStructureBuilder.ts | 75 ++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 64 deletions(-) create mode 100644 src/utils/TreeStructureBuilder.ts diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index fc93382..43ebfb4 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -73,23 +73,35 @@ async function configurePythonFile( function findPythonFoldersRecursively(folderPath: string): string[] { let pythonFolders: string[] = []; + let hasPythonFiles = false; try { const files = fs.readdirSync(folderPath); + // Check if current folder contains Python files or __init__.py if ( files.includes('__init__.py') || files.some((file) => file.endsWith('.py')) ) { - pythonFolders.push(folderPath); + hasPythonFiles = true; } + // Recursively scan subfolders for (const file of files) { const filePath = path.join(folderPath, file); if (fs.statSync(filePath).isDirectory()) { - pythonFolders = pythonFolders.concat(findPythonFoldersRecursively(filePath)); + const subfolderPythonFolders = findPythonFoldersRecursively(filePath); + if (subfolderPythonFolders.length > 0) { + hasPythonFiles = true; + pythonFolders.push(...subfolderPythonFolders); + } } } + + // Only add this folder if it or its subfolders contain Python + if (hasPythonFiles) { + pythonFolders.push(folderPath); + } } catch (error) { console.error(`Error scanning folder ${folderPath}:`, error); } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 3d7ebae..5feccea 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -1,7 +1,6 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; +import { buildPythonTree } from '../utils/TreeStructureBuilder'; export class SmellsViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = @@ -9,7 +8,6 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - // File statuses (e.g. "queued", "failed", "outdated") private fileStatuses: Map = new Map(); constructor(private context: vscode.ExtensionContext) {} @@ -20,7 +18,7 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { setStatus(filePath: string, status: string): void { this.fileStatuses.set(filePath, status); - this._onDidChangeTreeData.fire(); // Trigger UI update + this._onDidChangeTreeData.fire(); } public removeFile(filePath: string): boolean { @@ -39,64 +37,30 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { const rootPath = this.context.workspaceState.get( 'workspaceConfiguredPath', ); - if (!rootPath) { - return []; - } - - if (!element) { - const stat = fs.statSync(rootPath); - if (stat.isFile()) { - return [this.createTreeItem(rootPath, true)]; - } else if (stat.isDirectory()) { - return this.readDirectory(rootPath); - } - } - - if (element && element.resourceUri) { - return this.readDirectory(element.resourceUri.fsPath); - } - - return []; - } - - private readDirectory(dirPath: string): TreeItem[] { - const children: TreeItem[] = []; - - try { - const entries = fs.readdirSync(dirPath); - for (const entry of entries) { - const fullPath = path.join(dirPath, entry); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - children.push(this.createTreeItem(fullPath, false)); - } else if (stat.isFile() && entry.endsWith('.py')) { - children.push(this.createTreeItem(fullPath, true)); - } - } - } catch (err) { - console.error(`Failed to read directory ${dirPath}:`, err); - } - - return children; - } - - private createTreeItem(filePath: string, isFile: boolean): TreeItem { - const label = path.basename(filePath); - const collapsibleState = isFile - ? vscode.TreeItemCollapsibleState.None - : vscode.TreeItemCollapsibleState.Collapsed; - const contextValue = isFile ? 'file' : 'directory'; - - const status = this.fileStatuses.get(filePath) ?? 'not_yet_detected'; // Default - const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); - const tooltip = isFile ? getStatusMessage(status) : undefined; - - const item = new TreeItem(label, filePath, collapsibleState, contextValue); - item.iconPath = icon; - item.tooltip = tooltip; - - return item; + if (!rootPath) return []; + + const currentPath = element?.resourceUri?.fsPath ?? rootPath; + const nodes = buildPythonTree(currentPath); + + return nodes.map(({ label, fullPath, isFile }) => { + const status = this.fileStatuses.get(fullPath) ?? 'not_yet_detected'; + const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); + const tooltip = isFile ? getStatusMessage(status) : undefined; + + const collapsibleState = isFile + ? vscode.TreeItemCollapsibleState.None + : vscode.TreeItemCollapsibleState.Collapsed; + + const item = new TreeItem( + label, + fullPath, + collapsibleState, + isFile ? 'file' : 'directory', + ); + item.iconPath = icon; + item.tooltip = tooltip; + return item; + }); } } diff --git a/src/utils/TreeStructureBuilder.ts b/src/utils/TreeStructureBuilder.ts new file mode 100644 index 0000000..9206725 --- /dev/null +++ b/src/utils/TreeStructureBuilder.ts @@ -0,0 +1,75 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +export interface TreeNodeOptions { + contextValue: string; + icon?: vscode.ThemeIcon; + tooltip?: string; + command?: vscode.Command; +} + +export interface TreeNode { + label: string; + fullPath: string; + isFile: boolean; + options?: TreeNodeOptions; +} + +/** + * Recursively builds a tree structure of Python files and valid folders. + */ +export function buildPythonTree(rootPath: string): TreeNode[] { + const nodes: TreeNode[] = []; + + try { + const entries = fs.readdirSync(rootPath); + for (const entry of entries) { + const fullPath = path.join(rootPath, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory() && containsPythonFiles(fullPath)) { + nodes.push({ + label: entry, + fullPath, + isFile: false, + }); + } else if (stat.isFile() && entry.endsWith('.py')) { + nodes.push({ + label: entry, + fullPath, + isFile: true, + }); + } + } + } catch (err) { + console.error(`Error reading directory ${rootPath}:`, err); + } + + return nodes; +} + +/** + * Checks if a folder (or its subfolders) contains any .py files. + */ +function containsPythonFiles(folderPath: string): boolean { + try { + const entries = fs.readdirSync(folderPath); + for (const entry of entries) { + const fullPath = path.join(folderPath, entry); + const stat = fs.statSync(fullPath); + + if (stat.isFile() && entry.endsWith('.py')) { + return true; + } else if (stat.isDirectory()) { + if (containsPythonFiles(fullPath)) { + return true; + } + } + } + } catch (err) { + console.error(`Error checking folder ${folderPath}:`, err); + } + + return false; +} From a90c07cb14395c6f62429efb5fbe2c2f92b0b886 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 19:09:19 -0400 Subject: [PATCH 046/121] clean --- src/commands/resetConfiguration.ts | 3 ++- src/context/SmellsCacheManager.ts | 4 ++++ src/providers/SmellsViewProvider.ts | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/commands/resetConfiguration.ts b/src/commands/resetConfiguration.ts index 32de7d8..37e914b 100644 --- a/src/commands/resetConfiguration.ts +++ b/src/commands/resetConfiguration.ts @@ -25,7 +25,8 @@ export async function resetConfiguration( ); // Clear any cached smells, if needed - // smellsCacheManager.clear(); + smellsCacheManager.clearAllCachedSmells(); + smellsViewProvider.clearAllStatuses(); // 🔥 Trigger view refreshes smellsViewProvider.refresh(); diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index 2e43ef8..9d526d9 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -106,4 +106,8 @@ export class SmellsCacheManager { {}, ); } + + public async clearAllCachedSmells(): Promise { + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, {}); + } } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 5feccea..373b1b0 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -29,6 +29,11 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { return exists; } + public clearAllStatuses(): void { + this.fileStatuses.clear(); + this._onDidChangeTreeData.fire(); + } + getTreeItem(element: TreeItem): vscode.TreeItem { return element; } From c6d30e7d2ce6391aa031dbfe57e5c7491ee4734b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 19:14:55 -0400 Subject: [PATCH 047/121] clean configureWorkspace.ts --- src/commands/configureWorkspace.ts | 52 ++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index 43ebfb4..36fce31 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -8,6 +8,10 @@ import { MetricsViewProvider } from '../providers/MetricsViewProvider'; /** * Prompts the user to configure a workspace by selecting either a Python file or folder. * Updates the workspace state accordingly and refreshes the tree view to reflect the changes. + * + * @param context - The extension context for managing workspace state. + * @param smellsViewProvider - The provider for the smells view. + * @param metricsViewProvider - The provider for the metrics view. */ export async function configureWorkspace( context: vscode.ExtensionContext, @@ -28,21 +32,32 @@ export async function configureWorkspace( } } +/** + * Configures the workspace using a selected Python file. + * Prompts the user to select a Python file from open editors or the workspace. + * + * @param context - The extension context for managing workspace state. + * @param smellsViewProvider - The provider for the smells view. + * @param metricsViewProvider - The provider for the metrics view. + */ async function configurePythonFile( context: vscode.ExtensionContext, smellsViewProvider: SmellsViewProvider, metricsViewProvider: MetricsViewProvider, ) { + // Get Python files from open editors const openEditorFiles = vscode.window.tabGroups.activeTabGroup.tabs .map((tab) => (tab.input as any)?.uri?.fsPath) .filter((filePath) => filePath && filePath.endsWith('.py')); + // Get Python files from the workspace const workspaceFiles = await vscode.workspace.findFiles( '**/*.py', '**/node_modules/**', ); const workspaceFilePaths = workspaceFiles.map((uri) => uri.fsPath); + // Combine and deduplicate file paths const allPythonFiles = Array.from( new Set([...openEditorFiles, ...workspaceFilePaths]), ); @@ -54,6 +69,7 @@ async function configurePythonFile( return; } + // Prompt the user to select a Python file const selectedFile = await vscode.window.showQuickPick(allPythonFiles, { placeHolder: 'Select a Python file to use as your workspace.', }); @@ -71,6 +87,12 @@ async function configurePythonFile( } } +/** + * Recursively scans a folder to find subfolders containing Python files or __init__.py. + * + * @param folderPath - The path of the folder to scan. + * @returns An array of folder paths that contain Python files. + */ function findPythonFoldersRecursively(folderPath: string): string[] { let pythonFolders: string[] = []; let hasPythonFiles = false; @@ -78,7 +100,7 @@ function findPythonFoldersRecursively(folderPath: string): string[] { try { const files = fs.readdirSync(folderPath); - // Check if current folder contains Python files or __init__.py + // Check if the current folder contains Python files or __init__.py if ( files.includes('__init__.py') || files.some((file) => file.endsWith('.py')) @@ -98,17 +120,28 @@ function findPythonFoldersRecursively(folderPath: string): string[] { } } - // Only add this folder if it or its subfolders contain Python + // Only add this folder if it or its subfolders contain Python files if (hasPythonFiles) { pythonFolders.push(folderPath); } } catch (error) { - console.error(`Error scanning folder ${folderPath}:`, error); + // Log the error and notify the user + vscode.window.showErrorMessage( + `Error scanning folder ${folderPath}: ${(error as Error).message}`, + ); } return pythonFolders; } +/** + * Configures the workspace using a selected Python folder. + * Prompts the user to select a folder containing Python files from the workspace. + * + * @param context - The extension context for managing workspace state. + * @param smellsViewProvider - The provider for the smells view. + * @param metricsViewProvider - The provider for the metrics view. + */ async function configurePythonFolder( context: vscode.ExtensionContext, smellsViewProvider: SmellsViewProvider, @@ -123,6 +156,7 @@ async function configurePythonFolder( return; } + // Find all valid Python folders in the workspace const validPythonFolders = workspaceFolders .map((folder) => folder.uri.fsPath) .flatMap((folderPath) => findPythonFoldersRecursively(folderPath)); @@ -134,6 +168,7 @@ async function configurePythonFolder( return; } + // Prompt the user to select a Python folder const selectedFolder = await vscode.window.showQuickPick(validPythonFolders, { placeHolder: 'Select a Python folder to use as your workspace.', }); @@ -151,20 +186,31 @@ async function configurePythonFolder( } } +/** + * Updates the workspace configuration and refreshes the views. + * + * @param context - The extension context for managing workspace state. + * @param workspacePath - The path of the selected workspace (file or folder). + * @param smellsViewProvider - The provider for the smells view. + * @param metricsViewProvider - The provider for the metrics view. + */ export async function updateWorkspace( context: vscode.ExtensionContext, workspacePath: string, smellsViewProvider: SmellsViewProvider, metricsViewProvider: MetricsViewProvider, ) { + // Update the workspace state with the selected path await context.workspaceState.update('workspaceConfiguredPath', workspacePath); + // Set the workspace context to indicate that the workspace is configured vscode.commands.executeCommand( 'setContext', 'workspaceState.workspaceConfigured', true, ); + // Refresh the views to reflect the changes smellsViewProvider.refresh(); metricsViewProvider.refresh(); } From aa4b1779764ed5129b32976ea5c60ea2f18e0e46 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 19:18:39 -0400 Subject: [PATCH 048/121] clean resetConfiguration.ts --- src/commands/resetConfiguration.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/commands/resetConfiguration.ts b/src/commands/resetConfiguration.ts index 37e914b..890c42c 100644 --- a/src/commands/resetConfiguration.ts +++ b/src/commands/resetConfiguration.ts @@ -3,37 +3,51 @@ import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; +/** + * Resets the workspace configuration by clearing the selected workspace path, + * analysis data, and refreshing the views. Prompts the user for confirmation + * before performing the reset. + * + * @param context - The extension context for managing workspace state. + * @param smellsCacheManager - The manager for handling cached smells. + * @param smellsViewProvider - The provider for the smells view. + * @param metricsViewProvider - The provider for the metrics view. + */ export async function resetConfiguration( context: vscode.ExtensionContext, smellsCacheManager: SmellsCacheManager, smellsViewProvider: SmellsViewProvider, metricsViewProvider: MetricsViewProvider, ) { + // Prompt the user for confirmation before resetting the configuration const confirm = await vscode.window.showWarningMessage( - 'Are you sure you want to reset the workspace configuration? This will remove the currently selected folder and wipe cached smells.', + 'Are you sure you want to reset the workspace configuration? This will remove the currently selected workspace and all analysis data will be lost.', { modal: true }, 'Reset', ); if (confirm === 'Reset') { + // Clear the configured workspace path from the workspace state await context.workspaceState.update('workspaceConfiguredPath', undefined); + // Update the workspace context to indicate that no workspace is configured vscode.commands.executeCommand( 'setContext', 'workspaceState.workspaceConfigured', false, ); - // Clear any cached smells, if needed + // Clear all analysis data and reset statuses in the smells view smellsCacheManager.clearAllCachedSmells(); smellsViewProvider.clearAllStatuses(); - // 🔥 Trigger view refreshes + // Refresh the views to reflect the reset state smellsViewProvider.refresh(); metricsViewProvider.refresh(); + // Notify the user that the reset was successful vscode.window.showInformationMessage( - 'Workspace configuration has been reset. All cached smells have been cleared.', + 'Workspace configuration has been reset. All analysis data has been cleared.', ); } } From d58df876dafea71b28125888cbfaeee472253211 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sun, 23 Mar 2025 19:42:52 -0400 Subject: [PATCH 049/121] clean --- src/extension.ts | 3 +++ src/listeners/workspaceModifiedListener.ts | 25 ++++++++---------- src/providers/SmellsViewProvider.ts | 6 +++++ src/utils/initializeStatusesFromCache.ts | 30 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 src/utils/initializeStatusesFromCache.ts diff --git a/src/extension.ts b/src/extension.ts index 74d02bf..704ef40 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { FilterViewProvider } from './providers/FilterViewProvider'; import { registerFilterSmellCommands } from './commands/filterSmells'; import { loadSmells } from './utils/smellsData'; import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; +import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; /** * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. @@ -24,6 +25,8 @@ export function activate(context: vscode.ExtensionContext): void { loadSmells(); const smellsCacheManager = new SmellsCacheManager(context); const smellsViewProvider = new SmellsViewProvider(context); + initializeStatusesFromCache(context, smellsCacheManager, smellsViewProvider); + const codeSmellsView = vscode.window.createTreeView('ecooptimizer.smellsView', { treeDataProvider: smellsViewProvider, }); diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index 68109a3..235c603 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -2,10 +2,9 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; -import { ecoOutput } from '../extension'; /** - * Listens for workspace modifications (file creation, deletion, and saves) + * Listens for workspace modifications (file creation, deletion, and changes) * and refreshes the SmellsViewProvider and MetricsViewProvider accordingly. */ export class WorkspaceModifiedListener { @@ -31,27 +30,22 @@ export class WorkspaceModifiedListener { return; // No workspace configured } - ecoOutput.appendLine(`Watching workspace: ${configuredPath}`); - this.fileWatcher = vscode.workspace.createFileSystemWatcher( new vscode.RelativePattern(configuredPath, '**/*.py'), - false, // do not ignore create events - false, // do not ignore change events - false, // do not ignore delete events + false, // Do not ignore create events + false, // Do not ignore change events + false, // Do not ignore delete events ); this.fileWatcher.onDidCreate(() => { - ecoOutput.appendLine('A Python file was created.'); this.refreshViews(); }); this.fileWatcher.onDidChange((uri) => { - ecoOutput.appendLine(`A Python file was modified and saved: ${uri.fsPath}`); this.handleFileChange(uri.fsPath); }); this.fileWatcher.onDidDelete((uri) => { - ecoOutput.appendLine(`A Python file was deleted: ${uri.fsPath}`); this.handleFileDeletion(uri.fsPath); }); } @@ -66,14 +60,15 @@ export class WorkspaceModifiedListener { this.smellsViewProvider.setStatus(filePath, 'outdated'); vscode.window.showInformationMessage( - `File modified: ${filePath}\nSmell results are now outdated. Please reanalyze.`, + `File modified: ${filePath}\nAnalysis data for this file is now outdated. Please reanalyze.`, ); } this.refreshViews(); } /** - * Handles file deletions by clearing the cache and removing from the tree view. + * Handles file deletions by clearing the cache and removing the file from the tree view. + * @param filePath - The path of the deleted file. */ private async handleFileDeletion(filePath: string): Promise { let removed = false; @@ -88,7 +83,7 @@ export class WorkspaceModifiedListener { if (removed) { vscode.window.showInformationMessage( - `Removed deleted file from smells view: ${filePath}`, + `Removed deleted file from analysis view: ${filePath}`, ); } @@ -96,7 +91,7 @@ export class WorkspaceModifiedListener { } /** - * Refreshes both views. + * Refreshes both the SmellsViewProvider and MetricsViewProvider. */ private refreshViews(): void { this.smellsViewProvider.refresh(); @@ -104,7 +99,7 @@ export class WorkspaceModifiedListener { } /** - * Disposes any resources. + * Disposes of the file watcher and any associated resources. */ public dispose(): void { if (this.fileWatcher) { diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 373b1b0..18e297d 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -64,6 +64,12 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { ); item.iconPath = icon; item.tooltip = tooltip; + + // Add "Outdated" description + if (status === 'outdated') { + item.description = 'outdated'; + } + return item; }); } diff --git a/src/utils/initializeStatusesFromCache.ts b/src/utils/initializeStatusesFromCache.ts new file mode 100644 index 0000000..ccda7ac --- /dev/null +++ b/src/utils/initializeStatusesFromCache.ts @@ -0,0 +1,30 @@ +import * as vscode from 'vscode'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; + +/** + * Initializes file statuses in the SmellsViewProvider from the smell cache. + * Also validates that cached files are part of the current workspace. + */ +export async function initializeStatusesFromCache( + context: vscode.ExtensionContext, + smellsCacheManager: SmellsCacheManager, + smellsViewProvider: SmellsViewProvider, +): Promise { + const configuredPath = context.workspaceState.get( + 'workspaceConfiguredPath', + ); + if (!configuredPath) return; + + const cache = smellsCacheManager.getFullSmellCache(); + for (const filePath of Object.keys(cache)) { + if (!filePath.startsWith(configuredPath)) { + // Remove cache entry outside of configured workspace + await smellsCacheManager.clearCachedSmellsForFile(filePath); + } else { + const smells = cache[filePath]; + const status = smells.length > 0 ? 'passed' : 'no_issues'; + smellsViewProvider.setStatus(filePath, status); + } + } +} From bbdc2f00b4ca80ace1eb59bcf91e22bd91d22306 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 01:24:10 -0400 Subject: [PATCH 050/121] clean --- .env | 2 +- src/commands/configureWorkspace.ts | 51 +----- src/commands/resetConfiguration.ts | 34 +--- src/context/SmellsCacheManager.ts | 171 ++++++++++++++++----- src/extension.ts | 131 +++++++++------- src/listeners/workspaceModifiedListener.ts | 58 +++---- src/utils/envConfig.ts | 4 +- src/utils/initializeStatusesFromCache.ts | 21 ++- 8 files changed, 266 insertions(+), 206 deletions(-) diff --git a/.env b/.env index 779967a..7482d22 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ SERVER_URL='127.0.0.1:8000' -FILE_HASH_CACHE_KEY='fileHashCache' +HASH_PATH_MAP_KEY='hashPathMap' SMELL_CACHE_KEY='smellCache' WORKSPACE_METRICS_DATA='metricsData' diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index 36fce31..62d88c9 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -2,22 +2,13 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; -import { MetricsViewProvider } from '../providers/MetricsViewProvider'; - /** * Prompts the user to configure a workspace by selecting either a Python file or folder. * Updates the workspace state accordingly and refreshes the tree view to reflect the changes. * * @param context - The extension context for managing workspace state. - * @param smellsViewProvider - The provider for the smells view. - * @param metricsViewProvider - The provider for the metrics view. */ -export async function configureWorkspace( - context: vscode.ExtensionContext, - smellsViewProvider: SmellsViewProvider, - metricsViewProvider: MetricsViewProvider, -) { +export async function configureWorkspace(context: vscode.ExtensionContext) { const choice = await vscode.window.showQuickPick( ['Configure a Python File', 'Configure a Python Folder'], { placeHolder: 'Choose whether to configure a Python file or folder.' }, @@ -26,9 +17,9 @@ export async function configureWorkspace( if (!choice) return; if (choice === 'Configure a Python File') { - await configurePythonFile(context, smellsViewProvider, metricsViewProvider); + await configurePythonFile(context); } else { - await configurePythonFolder(context, smellsViewProvider, metricsViewProvider); + await configurePythonFolder(context); } } @@ -40,11 +31,7 @@ export async function configureWorkspace( * @param smellsViewProvider - The provider for the smells view. * @param metricsViewProvider - The provider for the metrics view. */ -async function configurePythonFile( - context: vscode.ExtensionContext, - smellsViewProvider: SmellsViewProvider, - metricsViewProvider: MetricsViewProvider, -) { +async function configurePythonFile(context: vscode.ExtensionContext) { // Get Python files from open editors const openEditorFiles = vscode.window.tabGroups.activeTabGroup.tabs .map((tab) => (tab.input as any)?.uri?.fsPath) @@ -75,12 +62,7 @@ async function configurePythonFile( }); if (selectedFile) { - await updateWorkspace( - context, - selectedFile, - smellsViewProvider, - metricsViewProvider, - ); + await updateWorkspace(context, selectedFile); vscode.window.showInformationMessage( `Workspace configured for file: ${path.basename(selectedFile)}`, ); @@ -139,14 +121,8 @@ function findPythonFoldersRecursively(folderPath: string): string[] { * Prompts the user to select a folder containing Python files from the workspace. * * @param context - The extension context for managing workspace state. - * @param smellsViewProvider - The provider for the smells view. - * @param metricsViewProvider - The provider for the metrics view. */ -async function configurePythonFolder( - context: vscode.ExtensionContext, - smellsViewProvider: SmellsViewProvider, - metricsViewProvider: MetricsViewProvider, -) { +async function configurePythonFolder(context: vscode.ExtensionContext) { const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { @@ -174,12 +150,7 @@ async function configurePythonFolder( }); if (selectedFolder) { - await updateWorkspace( - context, - selectedFolder, - smellsViewProvider, - metricsViewProvider, - ); + await updateWorkspace(context, selectedFolder); vscode.window.showInformationMessage( `Workspace configured for folder: ${path.basename(selectedFolder)}`, ); @@ -191,14 +162,10 @@ async function configurePythonFolder( * * @param context - The extension context for managing workspace state. * @param workspacePath - The path of the selected workspace (file or folder). - * @param smellsViewProvider - The provider for the smells view. - * @param metricsViewProvider - The provider for the metrics view. */ export async function updateWorkspace( context: vscode.ExtensionContext, workspacePath: string, - smellsViewProvider: SmellsViewProvider, - metricsViewProvider: MetricsViewProvider, ) { // Update the workspace state with the selected path await context.workspaceState.update('workspaceConfiguredPath', workspacePath); @@ -209,8 +176,4 @@ export async function updateWorkspace( 'workspaceState.workspaceConfigured', true, ); - - // Refresh the views to reflect the changes - smellsViewProvider.refresh(); - metricsViewProvider.refresh(); } diff --git a/src/commands/resetConfiguration.ts b/src/commands/resetConfiguration.ts index 890c42c..49262cc 100644 --- a/src/commands/resetConfiguration.ts +++ b/src/commands/resetConfiguration.ts @@ -1,25 +1,14 @@ import * as vscode from 'vscode'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; -import { MetricsViewProvider } from '../providers/MetricsViewProvider'; /** - * Resets the workspace configuration by clearing the selected workspace path, - * analysis data, and refreshing the views. Prompts the user for confirmation - * before performing the reset. + * Resets the workspace configuration by clearing the selected workspace path. + * Prompts the user for confirmation before performing the reset. * * @param context - The extension context for managing workspace state. - * @param smellsCacheManager - The manager for handling cached smells. - * @param smellsViewProvider - The provider for the smells view. - * @param metricsViewProvider - The provider for the metrics view. */ export async function resetConfiguration( context: vscode.ExtensionContext, - smellsCacheManager: SmellsCacheManager, - smellsViewProvider: SmellsViewProvider, - metricsViewProvider: MetricsViewProvider, -) { - // Prompt the user for confirmation before resetting the configuration +): Promise { const confirm = await vscode.window.showWarningMessage( 'Are you sure you want to reset the workspace configuration? This will remove the currently selected workspace and all analysis data will be lost.', { modal: true }, @@ -27,27 +16,16 @@ export async function resetConfiguration( ); if (confirm === 'Reset') { - // Clear the configured workspace path from the workspace state await context.workspaceState.update('workspaceConfiguredPath', undefined); - // Update the workspace context to indicate that no workspace is configured vscode.commands.executeCommand( 'setContext', 'workspaceState.workspaceConfigured', false, ); - // Clear all analysis data and reset statuses in the smells view - smellsCacheManager.clearAllCachedSmells(); - smellsViewProvider.clearAllStatuses(); - - // Refresh the views to reflect the reset state - smellsViewProvider.refresh(); - metricsViewProvider.refresh(); - - // Notify the user that the reset was successful - vscode.window.showInformationMessage( - 'Workspace configuration has been reset. All analysis data has been cleared.', - ); + return true; // signal that reset happened } + + return false; } diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index 9d526d9..c1439a4 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; import { createHash } from 'crypto'; import { envConfig } from '../utils/envConfig'; @@ -9,96 +10,122 @@ import { envConfig } from '../utils/envConfig'; export class SmellsCacheManager { constructor(private context: vscode.ExtensionContext) {} - // ============================ - // Smell Caching Methods - // ============================ - /** * Generates a unique 5-character ID for a smell based on its content. - * The ID is derived from a SHA256 hash of the smell object. - * * @param smell - The smell object to generate an ID for. - * @returns A unique 5-character string ID. + * @returns A unique 5-character hash ID. */ private generateSmellId(smell: Smell): string { - // Generate a SHA256 hash of the smell object - const smellHash = createHash('sha256') + return createHash('sha256') .update(JSON.stringify(smell)) - .digest('hex'); + .digest('hex') + .substring(0, 5); + } - // Use the first 5 characters of the hash as the ID - return smellHash.substring(0, 5); + /** + * Generates a hash of the file contents. + * @param filePath - The absolute path to the file. + * @returns A SHA-256 hash of the file's content. + */ + private generateFileHash(filePath: string): string { + const content = fs.readFileSync(filePath, 'utf-8'); + return createHash('sha256').update(content).digest('hex'); } /** - * Caches detected smells for a given file and assigns unique IDs to each smell. - * @param filePath - The absolute path of the file. - * @param smells - The detected smells to store. + * Caches smells based on the content hash of the file and maps the hash to the file path. + * @param filePath - The path of the file being cached. + * @param smells - The list of smells to cache. */ public async setCachedSmells(filePath: string, smells: Smell[]): Promise { const cache = this.getFullSmellCache(); + const pathMap = this.getHashToPathMap(); + const fileHash = this.generateFileHash(filePath); - // Assign unique IDs to each smell const smellsWithIds = smells.map((smell) => ({ ...smell, - id: this.generateSmellId(smell), // Add a unique 5-character ID + id: this.generateSmellId(smell), })); - // Update the cache with the new smells - cache[filePath] = smellsWithIds; + cache[fileHash] = smellsWithIds; + pathMap[fileHash] = filePath; + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); } /** - * Retrieves cached smells for a given file. - * @param filePath - The absolute path of the file. - * @returns An array of detected smells with unique IDs, or undefined if no smells are cached. + * Retrieves cached smells for a given file by hashing its contents. + * @param filePath - The path of the file to retrieve cached smells for. + * @returns The cached smells for the file, or `undefined` if not found. */ public getCachedSmells(filePath: string): Smell[] | undefined { + const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); - return cache[filePath]; + return cache[fileHash]; } /** - * Checks if a file has cached smells. - * @param filePath - The absolute path of the file. - * @returns `true` if the file has cached smells, `false` otherwise. + * Checks if a file has cached smells by checking its content hash. + * @param filePath - The path of the file to check. + * @returns `true` if the file has cached smells, otherwise `false`. */ public hasCachedSmells(filePath: string): boolean { + const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); - return cache[filePath] !== undefined; + return cache[fileHash] !== undefined; } /** - * Retrieves a smell by its unique ID. - * @param id - The unique ID of the smell. - * @returns The smell object, or undefined if no smell matches the ID. + * Retrieves a smell by its ID from any file in the cache. + * @param id - The ID of the smell to retrieve. + * @returns The smell object if found, otherwise `undefined`. */ public getSmellById(id: string): Smell | undefined { const cache = this.getFullSmellCache(); - for (const filePath in cache) { - const smells = cache[filePath]; - const smell = smells.find((s) => s.id === id); - if (smell) { - return smell; - } + for (const hash in cache) { + const smell = cache[hash].find((s) => s.id === id); + if (smell) return smell; } return undefined; } /** - * Clears cached smells for a specific file. - * @param filePath - The path of the file to clear. + * Clears cached smells for a file by its content hash. + * @param filePath - The path of the file to clear cached smells for. */ public async clearCachedSmellsForFile(filePath: string): Promise { + const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); - delete cache[filePath]; + const pathMap = this.getHashToPathMap(); + + delete cache[fileHash]; + delete pathMap[fileHash]; + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); } /** - * Retrieves the entire smell cache from the workspace state. - * @returns A record of file paths to their corresponding cached smells. + * Clears cached smells for a file by its path. + * @param filePath - The path of the file to clear cached smells for. + */ + public async clearCachedSmellsByPath(filePath: string): Promise { + const pathMap = this.getHashToPathMap(); + const hash = Object.keys(pathMap).find((h) => pathMap[h] === filePath); + if (!hash) return; + + const cache = this.getFullSmellCache(); + delete cache[hash]; + delete pathMap[hash]; + + await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); + await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); + } + + /** + * Retrieves the entire smell cache. + * @returns A record mapping file hashes to their cached smells. */ public getFullSmellCache(): Record { return this.context.workspaceState.get>( @@ -107,7 +134,69 @@ export class SmellsCacheManager { ); } + /** + * Retrieves the hash-to-file-path map. + * @returns A record mapping file hashes to their file paths. + */ + public getHashToPathMap(): Record { + return this.context.workspaceState.get>( + envConfig.HASH_PATH_MAP_KEY!, + {}, + ); + } + + /** + * Clears all cached smells and the hash-to-path map. + */ public async clearAllCachedSmells(): Promise { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, {}); + await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, {}); + } + + /** + * Reassociates a cached file hash with a new file path. + * @param hash - The hash of the file to reassociate. + * @param newPath - The new file path to associate with the hash. + * @returns `true` if the reassociation was successful, otherwise `false`. + */ + public async reassociateCacheFromHash( + hash: string, + newPath: string, + ): Promise { + const cache = this.getFullSmellCache(); + const pathMap = this.getHashToPathMap(); + + if (cache[hash]) { + pathMap[hash] = newPath; + await this.context.workspaceState.update( + envConfig.HASH_PATH_MAP_KEY!, + pathMap, + ); + return true; + } + + return false; + } + + /** + * Retrieves the previous file path associated with a hash. + * @param hash - The hash to look up. + * @returns The file path associated with the hash, or `undefined` if not found. + */ + public getPreviousFilePathForHash(hash: string): string | undefined { + const pathMap = this.getHashToPathMap(); + return pathMap[hash]; + } + + /** + * Retrieves all file paths currently in the cache. + * @returns An array of file paths. + */ + public getAllFilePaths(): string[] { + const map = this.context.workspaceState.get>( + envConfig.HASH_PATH_MAP_KEY!, + {}, + ); + return Object.values(map); } } diff --git a/src/extension.ts b/src/extension.ts index 704ef40..75e3302 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,43 +1,67 @@ import * as vscode from 'vscode'; +// Output channel for logging export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); -import { configureWorkspace } from './commands/configureWorkspace'; -import { resetConfiguration } from './commands/resetConfiguration'; +// Core utilities +import { loadSmells } from './utils/smellsData'; +import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; +// Context & View Providers +import { SmellsCacheManager } from './context/SmellsCacheManager'; import { SmellsViewProvider } from './providers/SmellsViewProvider'; import { MetricsViewProvider } from './providers/MetricsViewProvider'; +import { FilterViewProvider } from './providers/FilterViewProvider'; -import { SmellsCacheManager } from './context/SmellsCacheManager'; -import { openFile } from './commands/openFile'; +// Commands +import { configureWorkspace } from './commands/configureWorkspace'; +import { resetConfiguration } from './commands/resetConfiguration'; import { detectSmellsFile } from './commands/detectSmells'; -import { FilterViewProvider } from './providers/FilterViewProvider'; +import { openFile } from './commands/openFile'; import { registerFilterSmellCommands } from './commands/filterSmells'; -import { loadSmells } from './utils/smellsData'; + +// Listeners import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; -import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; /** - * Activates the Eco-Optimizer extension and registers all necessary commands, providers, and listeners. - * @param context - The VS Code extension context. + * Activates the Eco-Optimizer extension and registers all necessary components. */ export function activate(context: vscode.ExtensionContext): void { + // Load smell definitions and initialize context loadSmells(); + const smellsCacheManager = new SmellsCacheManager(context); const smellsViewProvider = new SmellsViewProvider(context); + const metricsViewProvider = new MetricsViewProvider(context); + const filterSmellsProvider = new FilterViewProvider(context, metricsViewProvider); + + // Restore cached statuses initializeStatusesFromCache(context, smellsCacheManager, smellsViewProvider); - const codeSmellsView = vscode.window.createTreeView('ecooptimizer.smellsView', { - treeDataProvider: smellsViewProvider, - }); - context.subscriptions.push(codeSmellsView); + // === Tree Views === + context.subscriptions.push( + vscode.window.createTreeView('ecooptimizer.smellsView', { + treeDataProvider: smellsViewProvider, + }), + vscode.window.createTreeView('ecooptimizer.metricsView', { + treeDataProvider: metricsViewProvider, + showCollapseAll: true, + }), + vscode.window.createTreeView('ecooptimizer.filterView', { + treeDataProvider: filterSmellsProvider, + showCollapseAll: true, + }), + ); - const metricsViewProvider = new MetricsViewProvider(context); - vscode.window.createTreeView('ecooptimizer.metricsView', { - treeDataProvider: metricsViewProvider, - showCollapseAll: true, - }); + // Link checkbox UI to filter logic + filterSmellsProvider.setTreeView( + vscode.window.createTreeView('ecooptimizer.filterView', { + treeDataProvider: filterSmellsProvider, + showCollapseAll: true, + }), + ); + // === Workspace Context Flag === const workspaceConfigured = Boolean( context.workspaceState.get('workspaceConfiguredPath'), ); @@ -47,55 +71,40 @@ export function activate(context: vscode.ExtensionContext): void { workspaceConfigured, ); + // === Command Registration === context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.configureWorkspace', () => - configureWorkspace(context, smellsViewProvider, metricsViewProvider), - ), - ); - - context.subscriptions.push( - vscode.commands.registerCommand( - 'ecooptimizer.resetConfiguration', - () => - resetConfiguration( - context, - smellsCacheManager, - smellsViewProvider, - metricsViewProvider, - ), - smellsViewProvider.refresh(), - ), - ); + vscode.commands.registerCommand('ecooptimizer.configureWorkspace', async () => { + await configureWorkspace(context); + smellsViewProvider.refresh(); + metricsViewProvider.refresh(); + }), - // Initialize the Filter Smells View. - const filterSmellsProvider = new FilterViewProvider(context, metricsViewProvider); - const filterSmellsView = vscode.window.createTreeView('ecooptimizer.filterView', { - treeDataProvider: filterSmellsProvider, - showCollapseAll: true, - }); + vscode.commands.registerCommand('ecooptimizer.resetConfiguration', async () => { + const didReset = await resetConfiguration(context); - // Associate the TreeView instance with the provider. - filterSmellsProvider.setTreeView(filterSmellsView); + if (didReset) { + smellsCacheManager.clearAllCachedSmells(); + smellsViewProvider.clearAllStatuses(); + smellsViewProvider.refresh(); + metricsViewProvider.refresh(); - // Register filter-related commands. - registerFilterSmellCommands(context, filterSmellsProvider); + vscode.window.showInformationMessage( + 'Workspace configuration has been reset. All analysis data has been cleared.', + ); + } + }), - context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.openFile', openFile), - ); - context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileItem) => { try { const filePath = fileItem?.resourceUri?.fsPath; - if (!filePath) { vscode.window.showWarningMessage( 'No file selected or file path not found.', ); return; } - detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); } catch (error: any) { vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); @@ -103,18 +112,22 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - const workspaceModifiedListener = new WorkspaceModifiedListener( - context, - smellsCacheManager, - smellsViewProvider, - metricsViewProvider, - ); + // Register filter UI toggle/edit/select-all/deselect-all + registerFilterSmellCommands(context, filterSmellsProvider); - context.subscriptions.push(workspaceModifiedListener); + // === Workspace File Listener === + context.subscriptions.push( + new WorkspaceModifiedListener( + context, + smellsCacheManager, + smellsViewProvider, + metricsViewProvider, + ), + ); } /** - * Deactivates the Eco-Optimizer extension. + * Called when the extension is deactivated. */ export function deactivate(): void { ecoOutput.appendLine('Deactivating Eco-Optimizer extension...\n'); diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index 235c603..e8d88f3 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -9,6 +9,7 @@ import { MetricsViewProvider } from '../providers/MetricsViewProvider'; */ export class WorkspaceModifiedListener { private fileWatcher: vscode.FileSystemWatcher | undefined; + private saveListener: vscode.Disposable | undefined; constructor( private context: vscode.ExtensionContext, @@ -17,6 +18,7 @@ export class WorkspaceModifiedListener { private metricsViewProvider: MetricsViewProvider, ) { this.initializeFileWatcher(); + this.initializeSaveListener(); } /** @@ -26,9 +28,7 @@ export class WorkspaceModifiedListener { const configuredPath = this.context.workspaceState.get( 'workspaceConfiguredPath', ); - if (!configuredPath) { - return; // No workspace configured - } + if (!configuredPath) return; this.fileWatcher = vscode.workspace.createFileSystemWatcher( new vscode.RelativePattern(configuredPath, '**/*.py'), @@ -37,32 +37,38 @@ export class WorkspaceModifiedListener { false, // Do not ignore delete events ); - this.fileWatcher.onDidCreate(() => { - this.refreshViews(); - }); - - this.fileWatcher.onDidChange((uri) => { - this.handleFileChange(uri.fsPath); - }); + this.fileWatcher.onDidCreate(() => this.refreshViews()); + this.fileWatcher.onDidChange((uri) => this.handleFileChange(uri.fsPath)); + this.fileWatcher.onDidDelete((uri) => this.handleFileDeletion(uri.fsPath)); + } - this.fileWatcher.onDidDelete((uri) => { - this.handleFileDeletion(uri.fsPath); + /** + * Initializes a listener for file save events to handle changes in Python files. + */ + private initializeSaveListener(): void { + this.saveListener = vscode.workspace.onDidSaveTextDocument((document) => { + if (document.languageId === 'python') { + this.handleFileChange(document.uri.fsPath); + } }); } /** - * Handles file changes by clearing the cache for the modified file and marking it as outdated. + * Handles file changes by clearing the cache for the modified file and marking it as outdated, + * but only if the file had previously been analyzed (i.e., is in the cache). * @param filePath - The path of the modified file. */ private async handleFileChange(filePath: string): Promise { - if (this.smellsCacheManager.hasCachedSmells(filePath)) { - await this.smellsCacheManager.clearCachedSmellsForFile(filePath); - this.smellsViewProvider.setStatus(filePath, 'outdated'); + const hadCache = this.smellsCacheManager.getAllFilePaths().includes(filePath); + if (!hadCache) return; // Skip if no analysis was done before + + await this.smellsCacheManager.clearCachedSmellsForFile(filePath); + this.smellsViewProvider.setStatus(filePath, 'outdated'); + + vscode.window.showInformationMessage( + `File modified: ${filePath}\nAnalysis data is now outdated.`, + ); - vscode.window.showInformationMessage( - `File modified: ${filePath}\nAnalysis data for this file is now outdated. Please reanalyze.`, - ); - } this.refreshViews(); } @@ -71,10 +77,11 @@ export class WorkspaceModifiedListener { * @param filePath - The path of the deleted file. */ private async handleFileDeletion(filePath: string): Promise { + const hadCache = this.smellsCacheManager.getAllFilePaths().includes(filePath); let removed = false; - if (this.smellsCacheManager.hasCachedSmells(filePath)) { - await this.smellsCacheManager.clearCachedSmellsForFile(filePath); + if (hadCache) { + await this.smellsCacheManager.clearCachedSmellsByPath(filePath); removed = true; } @@ -99,11 +106,10 @@ export class WorkspaceModifiedListener { } /** - * Disposes of the file watcher and any associated resources. + * Disposes of the file watcher and save listener to clean up resources. */ public dispose(): void { - if (this.fileWatcher) { - this.fileWatcher.dispose(); - } + this.fileWatcher?.dispose(); + this.saveListener?.dispose(); } } diff --git a/src/utils/envConfig.ts b/src/utils/envConfig.ts index f3dc8ec..85f9ad1 100644 --- a/src/utils/envConfig.ts +++ b/src/utils/envConfig.ts @@ -5,13 +5,13 @@ dotenv.config(); export interface EnvConfig { SERVER_URL?: string; SMELL_CACHE_KEY?: string; - FILE_HASH_CACHE_KEY?: string; + HASH_PATH_MAP_KEY?: string; WORKSPACE_METRICS_DATA?: string; } export const envConfig: EnvConfig = { SERVER_URL: process.env.SERVER_URL, SMELL_CACHE_KEY: process.env.SMELL_CACHE_KEY, - FILE_HASH_CACHE_KEY: process.env.FILE_HASH_CACHE_KEY, + HASH_PATH_MAP_KEY: process.env.FILE_HASH_CACHE_KEY, WORKSPACE_METRICS_DATA: process.env.WORKSPACE_METRICS_DATA, }; diff --git a/src/utils/initializeStatusesFromCache.ts b/src/utils/initializeStatusesFromCache.ts index ccda7ac..a5f49dc 100644 --- a/src/utils/initializeStatusesFromCache.ts +++ b/src/utils/initializeStatusesFromCache.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as fs from 'fs/promises'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; @@ -16,13 +17,23 @@ export async function initializeStatusesFromCache( ); if (!configuredPath) return; - const cache = smellsCacheManager.getFullSmellCache(); - for (const filePath of Object.keys(cache)) { + const pathMap = smellsCacheManager.getAllFilePaths(); // returns string[] + for (const filePath of pathMap) { + // Ignore files outside the configured workspace or that don't exist anymore if (!filePath.startsWith(configuredPath)) { - // Remove cache entry outside of configured workspace await smellsCacheManager.clearCachedSmellsForFile(filePath); - } else { - const smells = cache[filePath]; + continue; + } + + try { + await fs.access(filePath); // throws if file doesn't exist + } catch { + await smellsCacheManager.clearCachedSmellsForFile(filePath); + continue; + } + + const smells = smellsCacheManager.getCachedSmells(filePath); + if (smells !== undefined) { const status = smells.length > 0 ? 'passed' : 'no_issues'; smellsViewProvider.setStatus(filePath, status); } From 693bac39ee4c5c00f184fdf4d74bf60f0e9d9fc4 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 02:23:12 -0400 Subject: [PATCH 051/121] readded the smells view --- src/commands/detectSmells.ts | 13 +++-- src/providers/SmellsViewProvider.ts | 67 ++++++++++++++++++++---- src/utils/initializeStatusesFromCache.ts | 7 +-- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index c6f12d7..8139626 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -9,8 +9,9 @@ import { SmellsCacheManager } from '../context/SmellsCacheManager'; * Detects code smells for a given file. * Uses cached smells if available; otherwise, fetches from the backend. * - * @param smellsViewProvider - UI provider for updating tree view. * @param filePath - The VS Code file URI or string path of the file to analyze. + * @param smellsViewProvider - UI provider for updating tree view. + * @param smellsCacheManager - Manager for caching smells. */ export async function detectSmellsFile( filePath: string, @@ -23,9 +24,10 @@ export async function detectSmellsFile( vscode.window.showInformationMessage('Using cached smells for this file.'); if (cached && cached.length > 0) { smellsViewProvider.setStatus(filePath, 'passed'); - // TODO: render cached smells in tree + smellsViewProvider.setSmells(filePath, cached); // Render cached smells in the tree } else { smellsViewProvider.setStatus(filePath, 'no_issues'); + smellsViewProvider.setSmells(filePath, []); // Clear any existing smells } return; } @@ -36,6 +38,7 @@ export async function detectSmellsFile( 'Action blocked: Server is down and no cached smells exist for this file version.', ); smellsViewProvider.setStatus(filePath, 'server_down'); + smellsViewProvider.setSmells(filePath, []); // Clear any existing smells return; } @@ -45,6 +48,7 @@ export async function detectSmellsFile( vscode.window.showWarningMessage( 'No enabled smells found. Please configure enabled smells in the settings.', ); + smellsViewProvider.setSmells(filePath, []); // Clear any existing smells return; } @@ -62,17 +66,20 @@ export async function detectSmellsFile( if (smells.length > 0) { smellsViewProvider.setStatus(filePath, 'passed'); await smellsCacheManager.setCachedSmells(filePath, smells); - // TODO: render smells in tree + smellsViewProvider.setSmells(filePath, smells); // Render detected smells in the tree } else { smellsViewProvider.setStatus(filePath, 'no_issues'); await smellsCacheManager.setCachedSmells(filePath, []); + smellsViewProvider.setSmells(filePath, []); // Clear any existing smells } } else { smellsViewProvider.setStatus(filePath, 'failed'); vscode.window.showErrorMessage(`Analysis failed (status ${status}).`); + smellsViewProvider.setSmells(filePath, []); // Clear any existing smells } } catch (error: any) { smellsViewProvider.setStatus(filePath, 'failed'); vscode.window.showErrorMessage(`Analysis failed: ${error.message}`); + smellsViewProvider.setSmells(filePath, []); // Clear any existing smells } } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 18e297d..47773b3 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -2,13 +2,18 @@ import * as vscode from 'vscode'; import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; import { buildPythonTree } from '../utils/TreeStructureBuilder'; -export class SmellsViewProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = - new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = - this._onDidChangeTreeData.event; +export class SmellsViewProvider + implements vscode.TreeDataProvider +{ + private _onDidChangeTreeData: vscode.EventEmitter< + TreeItem | SmellTreeItem | undefined | void + > = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event< + TreeItem | SmellTreeItem | undefined | void + > = this._onDidChangeTreeData.event; private fileStatuses: Map = new Map(); + private fileSmells: Map = new Map(); // Store smells for each file constructor(private context: vscode.ExtensionContext) {} @@ -21,40 +26,69 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { this._onDidChangeTreeData.fire(); } + /** + * Sets the smells for a specific file. + * @param filePath - The path of the file. + * @param smells - The list of smells for the file. + */ + setSmells(filePath: string, smells: Smell[]): void { + this.fileSmells.set(filePath, smells); + this._onDidChangeTreeData.fire(); + } + public removeFile(filePath: string): boolean { const exists = this.fileStatuses.has(filePath); if (exists) { this.fileStatuses.delete(filePath); + this.fileSmells.delete(filePath); // Remove smells for the file as well } return exists; } public clearAllStatuses(): void { this.fileStatuses.clear(); + this.fileSmells.clear(); // Clear all smells this._onDidChangeTreeData.fire(); } - getTreeItem(element: TreeItem): vscode.TreeItem { + getTreeItem(element: TreeItem | SmellTreeItem): vscode.TreeItem { return element; } - async getChildren(element?: TreeItem): Promise { + async getChildren( + element?: TreeItem | SmellTreeItem, + ): Promise<(TreeItem | SmellTreeItem)[]> { const rootPath = this.context.workspaceState.get( 'workspaceConfiguredPath', ); if (!rootPath) return []; + // If the element is a smell item, it has no children + if (element instanceof SmellTreeItem) { + return []; + } + const currentPath = element?.resourceUri?.fsPath ?? rootPath; const nodes = buildPythonTree(currentPath); + // If the element is a file, return its smells as children + if (element?.contextValue === 'file') { + const smells = this.fileSmells.get(element.fullPath) ?? []; + return smells.map((smell) => new SmellTreeItem(smell)); + } + + // Otherwise, return the files and folders return nodes.map(({ label, fullPath, isFile }) => { const status = this.fileStatuses.get(fullPath) ?? 'not_yet_detected'; const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); const tooltip = isFile ? getStatusMessage(status) : undefined; + // Set collapsible state for files based on whether they have smells const collapsibleState = isFile - ? vscode.TreeItemCollapsibleState.None - : vscode.TreeItemCollapsibleState.Collapsed; + ? this.fileSmells.has(fullPath) && this.fileSmells.get(fullPath)!.length > 0 + ? vscode.TreeItemCollapsibleState.Collapsed // Files with smells are collapsible + : vscode.TreeItemCollapsibleState.None // Files without smells are not collapsible + : vscode.TreeItemCollapsibleState.Collapsed; // Folders are always collapsible const item = new TreeItem( label, @@ -75,6 +109,9 @@ export class SmellsViewProvider implements vscode.TreeDataProvider { } } +/** + * Represents a file or folder in the tree. + */ class TreeItem extends vscode.TreeItem { constructor( label: string, @@ -95,3 +132,15 @@ class TreeItem extends vscode.TreeItem { } } } + +/** + * Represents a smell item in the tree. + */ +class SmellTreeItem extends vscode.TreeItem { + constructor(public readonly smell: Smell) { + super(smell.messageId, vscode.TreeItemCollapsibleState.None); // Smells are leaf nodes + this.tooltip = smell.message; // Show the full message as a tooltip + this.contextValue = 'smell'; // Context value for smell items + this.iconPath = new vscode.ThemeIcon('snake'); // Use a warning icon for smells + } +} diff --git a/src/utils/initializeStatusesFromCache.ts b/src/utils/initializeStatusesFromCache.ts index a5f49dc..2364408 100644 --- a/src/utils/initializeStatusesFromCache.ts +++ b/src/utils/initializeStatusesFromCache.ts @@ -4,7 +4,7 @@ import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; /** - * Initializes file statuses in the SmellsViewProvider from the smell cache. + * Initializes file statuses and smells in the SmellsViewProvider from the smell cache. * Also validates that cached files are part of the current workspace. */ export async function initializeStatusesFromCache( @@ -17,7 +17,7 @@ export async function initializeStatusesFromCache( ); if (!configuredPath) return; - const pathMap = smellsCacheManager.getAllFilePaths(); // returns string[] + const pathMap = smellsCacheManager.getAllFilePaths(); // Returns string[] for (const filePath of pathMap) { // Ignore files outside the configured workspace or that don't exist anymore if (!filePath.startsWith(configuredPath)) { @@ -26,7 +26,7 @@ export async function initializeStatusesFromCache( } try { - await fs.access(filePath); // throws if file doesn't exist + await fs.access(filePath); // Throws if file doesn't exist } catch { await smellsCacheManager.clearCachedSmellsForFile(filePath); continue; @@ -36,6 +36,7 @@ export async function initializeStatusesFromCache( if (smells !== undefined) { const status = smells.length > 0 ? 'passed' : 'no_issues'; smellsViewProvider.setStatus(filePath, status); + smellsViewProvider.setSmells(filePath, smells); // Set smells in the tree view } } } From a47ed225483690e4c662adeb9d6035debdfbdb0d Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 02:35:15 -0400 Subject: [PATCH 052/121] readded the smells view --- src/providers/SmellsViewProvider.ts | 20 ++++++++++++++++---- src/utils/smellsData.ts | 12 ++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 47773b3..cc7871f 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; import { buildPythonTree } from '../utils/TreeStructureBuilder'; +import { getAcronymByMessageId } from '../utils/smellsData'; export class SmellsViewProvider implements vscode.TreeDataProvider @@ -138,9 +139,20 @@ class TreeItem extends vscode.TreeItem { */ class SmellTreeItem extends vscode.TreeItem { constructor(public readonly smell: Smell) { - super(smell.messageId, vscode.TreeItemCollapsibleState.None); // Smells are leaf nodes - this.tooltip = smell.message; // Show the full message as a tooltip - this.contextValue = 'smell'; // Context value for smell items - this.iconPath = new vscode.ThemeIcon('snake'); // Use a warning icon for smells + const acronym = getAcronymByMessageId(smell.messageId) ?? smell.messageId; + + // Build the line number string: "Line 13, 18, 19" + const lines = smell.occurences + ?.map((occ) => occ.line) + .filter((line) => line !== undefined) + .sort((a, b) => a - b) + .join(', '); + + const label = lines ? `${acronym}: Line ${lines}` : acronym; + + super(label, vscode.TreeItemCollapsibleState.None); + this.tooltip = smell.message; + this.contextValue = 'smell'; + this.iconPath = new vscode.ThemeIcon('snake'); } } diff --git a/src/utils/smellsData.ts b/src/utils/smellsData.ts index c43d2c4..251399b 100644 --- a/src/utils/smellsData.ts +++ b/src/utils/smellsData.ts @@ -121,3 +121,15 @@ function parseSmells( ]), ); } + +/** + * Returns the acronym for a given message ID. + * @param messageId - The message ID to look up (e.g., "R0913"). + * @returns The acronym (e.g., "LPL") or undefined if not found. + */ +export function getAcronymByMessageId(messageId: string): string | undefined { + const match = Object.values(filterSmells).find( + (smell) => smell.message_id === messageId, + ); + return match?.acronym; +} From b307fc63cd655bd7f6807532571746550f1a6d6b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 02:40:11 -0400 Subject: [PATCH 053/121] readded the smells view --- src/extension.ts | 3 +++ src/providers/SmellsViewProvider.ts | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 75e3302..c51a693 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,6 +22,7 @@ import { registerFilterSmellCommands } from './commands/filterSmells'; // Listeners import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; +import { jumpToSmell } from './commands/jumpToSmell'; /** * Activates the Eco-Optimizer extension and registers all necessary components. @@ -96,6 +97,8 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('ecooptimizer.openFile', openFile), + vscode.commands.registerCommand('ecooptimizer.jumpToSmell', jumpToSmell), + vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileItem) => { try { const filePath = fileItem?.resourceUri?.fsPath; diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index cc7871f..ee946c6 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -140,8 +140,6 @@ class TreeItem extends vscode.TreeItem { class SmellTreeItem extends vscode.TreeItem { constructor(public readonly smell: Smell) { const acronym = getAcronymByMessageId(smell.messageId) ?? smell.messageId; - - // Build the line number string: "Line 13, 18, 19" const lines = smell.occurences ?.map((occ) => occ.line) .filter((line) => line !== undefined) @@ -149,10 +147,19 @@ class SmellTreeItem extends vscode.TreeItem { .join(', '); const label = lines ? `${acronym}: Line ${lines}` : acronym; - super(label, vscode.TreeItemCollapsibleState.None); + this.tooltip = smell.message; this.contextValue = 'smell'; this.iconPath = new vscode.ThemeIcon('snake'); + + const firstLine = smell.occurences?.[0]?.line; + if (smell.path && typeof firstLine === 'number') { + this.command = { + title: 'Jump to Smell', + command: 'ecooptimizer.jumpToSmell', + arguments: [smell.path, firstLine], + }; + } } } From e7ac0153116661f2035d6bb05a36e26ac9372bb8 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 02:55:28 -0400 Subject: [PATCH 054/121] readded the smells view --- src/providers/SmellsViewProvider.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index ee946c6..bb1a942 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -24,6 +24,11 @@ export class SmellsViewProvider setStatus(filePath: string, status: string): void { this.fileStatuses.set(filePath, status); + + if (status === 'outdated') { + this.fileSmells.delete(filePath); // Remove associated smells + } + this._onDidChangeTreeData.fire(); } From 810e87580b5edeaf5d1193e0f9394c9a093b458c Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 03:06:53 -0400 Subject: [PATCH 055/121] readded filter view --- src/extension.ts | 7 ++- src/providers/FilterViewProvider.ts | 83 +++++++++++------------------ 2 files changed, 36 insertions(+), 54 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index c51a693..4f456c4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -34,7 +34,12 @@ export function activate(context: vscode.ExtensionContext): void { const smellsCacheManager = new SmellsCacheManager(context); const smellsViewProvider = new SmellsViewProvider(context); const metricsViewProvider = new MetricsViewProvider(context); - const filterSmellsProvider = new FilterViewProvider(context, metricsViewProvider); + const filterSmellsProvider = new FilterViewProvider( + context, + metricsViewProvider, + smellsCacheManager, + smellsViewProvider, + ); // Restore cached statuses initializeStatusesFromCache(context, smellsCacheManager, smellsViewProvider); diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index 412b07f..eec8ec6 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; import { FilterSmellConfig, getFilterSmells, saveSmells } from '../utils/smellsData'; import { MetricsViewProvider } from './MetricsViewProvider'; -import { ecoOutput } from '../extension'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { SmellsViewProvider } from './SmellsViewProvider'; /** * Provides a tree view for filtering code smells within the VS Code extension. @@ -19,14 +20,12 @@ export class FilterViewProvider implements vscode.TreeDataProvider): void { this.treeView = treeView; @@ -37,7 +36,6 @@ export class FilterViewProvider implements vscode.TreeDataProvider { if (!element) { return Promise.resolve( @@ -95,11 +84,6 @@ export class FilterViewProvider implements vscode.TreeDataProvider { if (this.smells[smellKey]) { this.smells[smellKey].enabled = !this.smells[smellKey].enabled; @@ -109,13 +93,6 @@ export class FilterViewProvider implements vscode.TreeDataProvider { const confirmed = await this.confirmFilterChange(); if (!confirmed) return; @@ -160,36 +129,47 @@ export class FilterViewProvider implements vscode.TreeDataProvider { - ecoOutput.appendLine('Invalidating cached smells for affected files...'); + const cachedFilePaths = this.smellsCacheManager.getAllFilePaths(); + + for (const filePath of cachedFilePaths) { + this.smellsCacheManager.clearCachedSmellsForFile(filePath); + this.smellsViewProvider.setStatus(filePath, 'outdated'); + } this.metricsViewProvider.refresh(); + this.smellsViewProvider.refresh(); } - /** - * Prompts the user to confirm a smell filter change. - * Displays a modal warning that changing filters will invalidate cached analysis results. - * If the user confirms, returns true. If the user cancels, returns false. - * - * @returns Whether the user chose to proceed with the filter change. - */ private async confirmFilterChange(): Promise { + const suppressWarning = this.context.workspaceState.get( + 'ecooptimizer.suppressFilterWarning', + false, + ); + + if (suppressWarning) { + return true; // Skip confirmation + } + const result = await vscode.window.showWarningMessage( 'Changing smell filters will invalidate existing analysis results. Do you want to continue?', { modal: true }, 'Yes', + "Don't Remind Me Again", ); + + if (result === "Don't Remind Me Again") { + await this.context.workspaceState.update( + 'ecooptimizer.suppressFilterWarning', + true, + ); + return true; + } + return result === 'Yes'; } } -/** - * Represents a smell item in the tree view. - */ class SmellItem extends vscode.TreeItem { constructor( public readonly key: string, @@ -205,9 +185,6 @@ class SmellItem extends vscode.TreeItem { } } -/** - * Represents an option item for a smell in the tree view. - */ class SmellOptionItem extends vscode.TreeItem { constructor( public readonly optionKey: string, From 843c936dd9788b3f95b28bd2a8185f93db3fd77b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 03:42:43 -0400 Subject: [PATCH 056/121] bugs --- package.json | 16 ++++++++++------ src/commands/wipeWorkCache.ts | 9 +++++---- src/extension.ts | 8 +++++--- src/utils/initializeStatusesFromCache.ts | 13 ++++++++++--- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index a9cda3a..678d606 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,11 @@ "title": "Reset Configuration", "category": "Eco" }, + { + "command": "ecooptimizer.wipeWorkCache", + "title": "Clear Smells Cache", + "category": "Eco" + }, { "command": "ecooptimizer.toggleSmellFilter", "title": "Toggle Smell", @@ -189,12 +194,6 @@ "title": "Deselect All Smells", "category": "Eco" }, - { - "command": "ecooptimizer.openFile", - "title": "Open File", - "category": "Eco", - "enablement": "false" - }, { "command": "ecooptimizer.detectSmellsFile", "title": "Detect Smells", @@ -209,6 +208,11 @@ "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured", "group": "resource" }, + { + "command": "ecooptimizer.wipeWorkCache", + "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured", + "group": "resource" + }, { "command": "ecooptimizer.selectAllFilterSmells", "when": "view == ecooptimizer.filterView", diff --git a/src/commands/wipeWorkCache.ts b/src/commands/wipeWorkCache.ts index e8c6aa8..79b9bab 100644 --- a/src/commands/wipeWorkCache.ts +++ b/src/commands/wipeWorkCache.ts @@ -5,11 +5,11 @@ import { SmellsViewProvider } from '../providers/SmellsViewProvider'; /** * Clears the smells cache and refreshes the UI. * @param smellsCacheManager - Manages the caching of smells and file hashes. - * @param smellsDisplayProvider - The UI provider for updating the tree view. + * @param smellsViewProvider - The UI provider for updating the tree view. */ export async function wipeWorkCache( smellsCacheManager: SmellsCacheManager, - smellsDisplayProvider: SmellsViewProvider, + smellsViewProvider: SmellsViewProvider, ) { const userResponse = await vscode.window.showWarningMessage( 'Are you sure you want to clear the smells cache? This action cannot be undone.', @@ -18,8 +18,9 @@ export async function wipeWorkCache( ); if (userResponse === 'Confirm') { - // Use SmellsCacheManager to clear cache & refresh UI - await smellsCacheManager.clearCacheAndRefreshUI(smellsDisplayProvider); + smellsCacheManager.clearAllCachedSmells(); + smellsViewProvider.clearAllStatuses(); + smellsViewProvider.refresh(); vscode.window.showInformationMessage('Smells cache cleared successfully.'); } else { diff --git a/src/extension.ts b/src/extension.ts index 4f456c4..62a969b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,12 +17,12 @@ import { FilterViewProvider } from './providers/FilterViewProvider'; import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; import { detectSmellsFile } from './commands/detectSmells'; -import { openFile } from './commands/openFile'; import { registerFilterSmellCommands } from './commands/filterSmells'; // Listeners import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { jumpToSmell } from './commands/jumpToSmell'; +import { wipeWorkCache } from './commands/wipeWorkCache'; /** * Activates the Eco-Optimizer extension and registers all necessary components. @@ -100,10 +100,12 @@ export function activate(context: vscode.ExtensionContext): void { } }), - vscode.commands.registerCommand('ecooptimizer.openFile', openFile), - vscode.commands.registerCommand('ecooptimizer.jumpToSmell', jumpToSmell), + vscode.commands.registerCommand('ecooptimizer.wipeWorkCache', async () => { + await wipeWorkCache(smellsCacheManager, smellsViewProvider); + }), + vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileItem) => { try { const filePath = fileItem?.resourceUri?.fsPath; diff --git a/src/utils/initializeStatusesFromCache.ts b/src/utils/initializeStatusesFromCache.ts index 2364408..f8bcb20 100644 --- a/src/utils/initializeStatusesFromCache.ts +++ b/src/utils/initializeStatusesFromCache.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs/promises'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { ecoOutput } from '../extension'; /** * Initializes file statuses and smells in the SmellsViewProvider from the smell cache. @@ -20,6 +21,7 @@ export async function initializeStatusesFromCache( const pathMap = smellsCacheManager.getAllFilePaths(); // Returns string[] for (const filePath of pathMap) { // Ignore files outside the configured workspace or that don't exist anymore + ecoOutput.appendLine(`Checking file: ${filePath}`); if (!filePath.startsWith(configuredPath)) { await smellsCacheManager.clearCachedSmellsForFile(filePath); continue; @@ -34,9 +36,14 @@ export async function initializeStatusesFromCache( const smells = smellsCacheManager.getCachedSmells(filePath); if (smells !== undefined) { - const status = smells.length > 0 ? 'passed' : 'no_issues'; - smellsViewProvider.setStatus(filePath, status); - smellsViewProvider.setSmells(filePath, smells); // Set smells in the tree view + if (smells.length > 0) { + // The file has one or more smells + smellsViewProvider.setStatus(filePath, 'passed'); + smellsViewProvider.setSmells(filePath, smells); + } else { + // The file was analyzed but has no smells + smellsViewProvider.setStatus(filePath, 'no_issues'); + } } } } From 7d8e6f2bccd6ee17816aeb5e92a9e09bb81fe4c4 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 03:59:24 -0400 Subject: [PATCH 057/121] bugs --- src/providers/SmellsViewProvider.ts | 116 ++++++++++++++++------------ 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index bb1a942..59f0ccc 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -1,7 +1,10 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; import { buildPythonTree } from '../utils/TreeStructureBuilder'; import { getAcronymByMessageId } from '../utils/smellsData'; +import { ecoOutput } from '../extension'; export class SmellsViewProvider implements vscode.TreeDataProvider @@ -14,7 +17,7 @@ export class SmellsViewProvider > = this._onDidChangeTreeData.event; private fileStatuses: Map = new Map(); - private fileSmells: Map = new Map(); // Store smells for each file + private fileSmells: Map = new Map(); constructor(private context: vscode.ExtensionContext) {} @@ -26,17 +29,12 @@ export class SmellsViewProvider this.fileStatuses.set(filePath, status); if (status === 'outdated') { - this.fileSmells.delete(filePath); // Remove associated smells + this.fileSmells.delete(filePath); } this._onDidChangeTreeData.fire(); } - /** - * Sets the smells for a specific file. - * @param filePath - The path of the file. - * @param smells - The list of smells for the file. - */ setSmells(filePath: string, smells: Smell[]): void { this.fileSmells.set(filePath, smells); this._onDidChangeTreeData.fire(); @@ -46,14 +44,14 @@ export class SmellsViewProvider const exists = this.fileStatuses.has(filePath); if (exists) { this.fileStatuses.delete(filePath); - this.fileSmells.delete(filePath); // Remove smells for the file as well + this.fileSmells.delete(filePath); } return exists; } public clearAllStatuses(): void { this.fileStatuses.clear(); - this.fileSmells.clear(); // Clear all smells + this.fileSmells.clear(); this._onDidChangeTreeData.fire(); } @@ -67,57 +65,82 @@ export class SmellsViewProvider const rootPath = this.context.workspaceState.get( 'workspaceConfiguredPath', ); - if (!rootPath) return []; + if (!rootPath) { + ecoOutput.appendLine('No workspace configured.'); + return []; + } - // If the element is a smell item, it has no children + // Smell nodes never have children if (element instanceof SmellTreeItem) { + ecoOutput.appendLine('SmellTreeItem has no children.'); return []; } - const currentPath = element?.resourceUri?.fsPath ?? rootPath; - const nodes = buildPythonTree(currentPath); - - // If the element is a file, return its smells as children + // If file node, show smells if (element?.contextValue === 'file') { + ecoOutput.appendLine(`Getting smells for file: ${element.fullPath}`); const smells = this.fileSmells.get(element.fullPath) ?? []; return smells.map((smell) => new SmellTreeItem(smell)); } - // Otherwise, return the files and folders - return nodes.map(({ label, fullPath, isFile }) => { - const status = this.fileStatuses.get(fullPath) ?? 'not_yet_detected'; - const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); - const tooltip = isFile ? getStatusMessage(status) : undefined; - - // Set collapsible state for files based on whether they have smells - const collapsibleState = isFile - ? this.fileSmells.has(fullPath) && this.fileSmells.get(fullPath)!.length > 0 - ? vscode.TreeItemCollapsibleState.Collapsed // Files with smells are collapsible - : vscode.TreeItemCollapsibleState.None // Files without smells are not collapsible - : vscode.TreeItemCollapsibleState.Collapsed; // Folders are always collapsible - - const item = new TreeItem( - label, - fullPath, - collapsibleState, - isFile ? 'file' : 'directory', - ); - item.iconPath = icon; - item.tooltip = tooltip; - - // Add "Outdated" description - if (status === 'outdated') { - item.description = 'outdated'; + // If root element (first load) + if (!element) { + const stat = fs.statSync(rootPath); + if (stat.isFile()) { + ecoOutput.appendLine(`Root is a file: ${rootPath}`); + return [this.createTreeItem(rootPath, true)]; + } else if (stat.isDirectory()) { + ecoOutput.appendLine(`Root is a directory: ${rootPath}`); + return [this.createTreeItem(rootPath, false)]; // 👈 Show the root folder as the top node } + } + + // Folder node – get its children + const currentPath = element?.resourceUri?.fsPath; + if (!currentPath) return []; + + ecoOutput.appendLine(`Getting children of folder: ${currentPath}`); + const childNodes = buildPythonTree(currentPath); + ecoOutput.appendLine(` Found ${childNodes.length} children.`); + childNodes.forEach((node) => + ecoOutput.appendLine(` - ${node.fullPath} (isFile: ${node.isFile})`), + ); + + return childNodes.map(({ fullPath, isFile }) => + this.createTreeItem(fullPath, isFile), + ); + } + + private createTreeItem(filePath: string, isFile: boolean): TreeItem { + const label = path.basename(filePath); + const status = this.fileStatuses.get(filePath) ?? 'not_yet_detected'; + const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); + const tooltip = isFile ? getStatusMessage(status) : undefined; + + // Set collapsible state + const collapsibleState = isFile + ? this.fileSmells.has(filePath) && this.fileSmells.get(filePath)!.length > 0 + ? vscode.TreeItemCollapsibleState.Collapsed // Files with smells are collapsible + : vscode.TreeItemCollapsibleState.None // Files without smells are not collapsible + : vscode.TreeItemCollapsibleState.Collapsed; // Folders are always collapsible + + const item = new TreeItem( + label, + filePath, + collapsibleState, + isFile ? 'file' : 'directory', + ); + item.iconPath = icon; + item.tooltip = tooltip; + + if (status === 'outdated') { + item.description = 'outdated'; + } - return item; - }); + return item; } } -/** - * Represents a file or folder in the tree. - */ class TreeItem extends vscode.TreeItem { constructor( label: string, @@ -139,9 +162,6 @@ class TreeItem extends vscode.TreeItem { } } -/** - * Represents a smell item in the tree. - */ class SmellTreeItem extends vscode.TreeItem { constructor(public readonly smell: Smell) { const acronym = getAcronymByMessageId(smell.messageId) ?? smell.messageId; From f1ac1c755c8f623f2e23ce52c68171d146797a96 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 04:45:44 -0400 Subject: [PATCH 058/121] added line selection --- package.json | 23 ++- src/context/SmellsCacheManager.ts | 76 ++------ src/extension.ts | 22 +++ src/managers/SmellsViewStateManager.ts | 106 ----------- src/managers/SmellsViewUIManager.ts | 239 ------------------------- src/providers/SmellsViewProvider.ts | 2 +- src/ui/LineSelection.ts | 99 +++++----- 7 files changed, 102 insertions(+), 465 deletions(-) delete mode 100644 src/managers/SmellsViewStateManager.ts delete mode 100644 src/managers/SmellsViewUIManager.ts diff --git a/package.json b/package.json index 678d606..38e609a 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,11 @@ }, "views": { "ecooptimizer": [ + { + "id": "ecooptimizer.refactoringDetails", + "name": "Refactoring Details", + "icon": "assets/eco-icon.png" + }, { "id": "ecooptimizer.smellsView", "name": "Code Smells", @@ -146,9 +151,14 @@ ] }, "viewsWelcome": [ + { + "view": "ecooptimizer.refactoringDetails", + "contents": "Refactoring is currently not in progress. Try selecting a smell in the Code Smells view to start refactoring.", + "when": "!refactoringInProgress" + }, { "view": "ecooptimizer.smellsView", - "contents": "No code smells detected yet. Configure your workspace to start analysis!\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", + "contents": "No code smells detected yet. Configure your workspace to start analysis.\n\n[Configure Workspace](command:ecooptimizer.configureWorkspace)\n\n[Read the docs](https://code.visualstudio.com/api) to learn how to use Eco-Optimizer.", "when": "!workspaceState.workspaceConfigured" }, { @@ -194,6 +204,12 @@ "title": "Deselect All Smells", "category": "Eco" }, + { + "command": "ecooptimizer.detectSmellsFolder", + "title": "Detect Smells for All Files", + "icon": "$(search)", + "category": "Eco" + }, { "command": "ecooptimizer.detectSmellsFile", "title": "Detect Smells", @@ -230,6 +246,11 @@ "when": "viewItem == smellOption", "group": "inline" }, + { + "command": "ecooptimizer.detectSmellsFolder", + "when": "viewItem == ecooptimizer.smellsView && viewItem == folder", + "group": "inline" + }, { "command": "ecooptimizer.detectSmellsFile", "when": "view == ecooptimizer.smellsView && viewItem == file", diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index c1439a4..07a5aef 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -8,13 +8,11 @@ import { envConfig } from '../utils/envConfig'; * This class handles storing, retrieving, and clearing cached smells. */ export class SmellsCacheManager { + private cacheUpdatedEmitter = new vscode.EventEmitter(); + public readonly onSmellsUpdated = this.cacheUpdatedEmitter.event; + constructor(private context: vscode.ExtensionContext) {} - /** - * Generates a unique 5-character ID for a smell based on its content. - * @param smell - The smell object to generate an ID for. - * @returns A unique 5-character hash ID. - */ private generateSmellId(smell: Smell): string { return createHash('sha256') .update(JSON.stringify(smell)) @@ -22,21 +20,11 @@ export class SmellsCacheManager { .substring(0, 5); } - /** - * Generates a hash of the file contents. - * @param filePath - The absolute path to the file. - * @returns A SHA-256 hash of the file's content. - */ private generateFileHash(filePath: string): string { const content = fs.readFileSync(filePath, 'utf-8'); return createHash('sha256').update(content).digest('hex'); } - /** - * Caches smells based on the content hash of the file and maps the hash to the file path. - * @param filePath - The path of the file being cached. - * @param smells - The list of smells to cache. - */ public async setCachedSmells(filePath: string, smells: Smell[]): Promise { const cache = this.getFullSmellCache(); const pathMap = this.getHashToPathMap(); @@ -52,35 +40,22 @@ export class SmellsCacheManager { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); + + this.cacheUpdatedEmitter.fire(filePath); } - /** - * Retrieves cached smells for a given file by hashing its contents. - * @param filePath - The path of the file to retrieve cached smells for. - * @returns The cached smells for the file, or `undefined` if not found. - */ public getCachedSmells(filePath: string): Smell[] | undefined { const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); return cache[fileHash]; } - /** - * Checks if a file has cached smells by checking its content hash. - * @param filePath - The path of the file to check. - * @returns `true` if the file has cached smells, otherwise `false`. - */ public hasCachedSmells(filePath: string): boolean { const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); return cache[fileHash] !== undefined; } - /** - * Retrieves a smell by its ID from any file in the cache. - * @param id - The ID of the smell to retrieve. - * @returns The smell object if found, otherwise `undefined`. - */ public getSmellById(id: string): Smell | undefined { const cache = this.getFullSmellCache(); for (const hash in cache) { @@ -90,10 +65,6 @@ export class SmellsCacheManager { return undefined; } - /** - * Clears cached smells for a file by its content hash. - * @param filePath - The path of the file to clear cached smells for. - */ public async clearCachedSmellsForFile(filePath: string): Promise { const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); @@ -104,12 +75,10 @@ export class SmellsCacheManager { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); + + this.cacheUpdatedEmitter.fire(filePath); } - /** - * Clears cached smells for a file by its path. - * @param filePath - The path of the file to clear cached smells for. - */ public async clearCachedSmellsByPath(filePath: string): Promise { const pathMap = this.getHashToPathMap(); const hash = Object.keys(pathMap).find((h) => pathMap[h] === filePath); @@ -121,12 +90,10 @@ export class SmellsCacheManager { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); + + this.cacheUpdatedEmitter.fire(filePath); } - /** - * Retrieves the entire smell cache. - * @returns A record mapping file hashes to their cached smells. - */ public getFullSmellCache(): Record { return this.context.workspaceState.get>( envConfig.SMELL_CACHE_KEY!, @@ -134,10 +101,6 @@ export class SmellsCacheManager { ); } - /** - * Retrieves the hash-to-file-path map. - * @returns A record mapping file hashes to their file paths. - */ public getHashToPathMap(): Record { return this.context.workspaceState.get>( envConfig.HASH_PATH_MAP_KEY!, @@ -145,20 +108,13 @@ export class SmellsCacheManager { ); } - /** - * Clears all cached smells and the hash-to-path map. - */ public async clearAllCachedSmells(): Promise { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, {}); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, {}); + + this.cacheUpdatedEmitter.fire('all'); } - /** - * Reassociates a cached file hash with a new file path. - * @param hash - The hash of the file to reassociate. - * @param newPath - The new file path to associate with the hash. - * @returns `true` if the reassociation was successful, otherwise `false`. - */ public async reassociateCacheFromHash( hash: string, newPath: string, @@ -172,26 +128,18 @@ export class SmellsCacheManager { envConfig.HASH_PATH_MAP_KEY!, pathMap, ); + this.cacheUpdatedEmitter.fire(newPath); return true; } return false; } - /** - * Retrieves the previous file path associated with a hash. - * @param hash - The hash to look up. - * @returns The file path associated with the hash, or `undefined` if not found. - */ public getPreviousFilePathForHash(hash: string): string | undefined { const pathMap = this.getHashToPathMap(); return pathMap[hash]; } - /** - * Retrieves all file paths currently in the cache. - * @returns An array of file paths. - */ public getAllFilePaths(): string[] { const map = this.context.workspaceState.get>( envConfig.HASH_PATH_MAP_KEY!, diff --git a/src/extension.ts b/src/extension.ts index 62a969b..22cfaf9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,6 +23,7 @@ import { registerFilterSmellCommands } from './commands/filterSmells'; import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; +import { LineSelectionManager } from './ui/LineSelection'; /** * Activates the Eco-Optimizer extension and registers all necessary components. @@ -134,6 +135,27 @@ export function activate(context: vscode.ExtensionContext): void { metricsViewProvider, ), ); + + // Initialize the FileHighlighter for highlighting code smells. + // const fileHighlighter = FileHighlighter.getInstance(smellsCacheManager); + + // fileHighlighter.updateHighlightsForVisibleEditors(); + + // context.subscriptions.push( + // vscode.window.onDidChangeVisibleTextEditors((editors) => { + // editors.forEach((editor) => { + // fileHighlighter.highlightSmells(editor); + // }); + // }), + // ); + + const lineSelectManager = new LineSelectionManager(smellsCacheManager); + context.subscriptions.push( + vscode.window.onDidChangeTextEditorSelection((event) => { + console.log('Eco: Detected line selection event'); + lineSelectManager.commentLine(event.textEditor); + }), + ); } /** diff --git a/src/managers/SmellsViewStateManager.ts b/src/managers/SmellsViewStateManager.ts deleted file mode 100644 index 4a42b0d..0000000 --- a/src/managers/SmellsViewStateManager.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as path from 'path'; - -export class SmellsViewStateManager { - private fileStatusMap: Map = new Map(); - private detectedSmells: Map = new Map(); // Use Smell[] instead of ProcessedSmell[] - private smellToFileMap: Map = new Map(); - private modifiedFiles: Map = new Map(); - - /** - * Updates the detected smells for a file. - * @param filePath - The analyzed file path. - * @param smells - The detected smells in the file. - */ - updateSmells(filePath: string, smells: Smell[]): void { - this.fileStatusMap.set(filePath, 'passed'); - - // Update the detected smells for the file - this.detectedSmells.set(filePath, smells); - - // Update the detected smells for the folder - const folderPath = path.dirname(filePath); - if (!this.detectedSmells.has(folderPath)) { - this.detectedSmells.set(folderPath, []); - } - this.detectedSmells.get(folderPath)?.push(...smells); - } - - /** - * Marks a file as outdated. - * @param filePath - The path of the modified file. - */ - markFileAsOutdated(filePath: string): void { - this.modifiedFiles.set(filePath, true); - } - - /** - * Clears the outdated status for a file. - * @param filePath - The path of the file to clear. - */ - clearOutdatedStatus(filePath: string): void { - this.modifiedFiles.delete(filePath); - } - - /** - * Updates the status of a specific file or folder. - * @param filePath - The file or folder path. - * @param status - The new status to set. - */ - updateFileStatus(filePath: string, status: string): void { - this.fileStatusMap.set(filePath, status); - } - - /** - * Checks if a file is marked as outdated. - * @param filePath - The path of the file to check. - * @returns `true` if the file is outdated, `false` otherwise. - */ - isFileOutdated(filePath: string): boolean { - return this.modifiedFiles.has(filePath); - } - - /** - * Clears all detected smells and resets file statuses. - */ - resetAllSmells(): void { - this.detectedSmells.clear(); - this.fileStatusMap.clear(); - this.modifiedFiles.clear(); - } - - /** - * Retrieves the status of a file. - * @param filePath - The path of the file. - * @returns The status of the file. - */ - getFileStatus(filePath: string): string { - return this.fileStatusMap.get(filePath) || 'not_detected'; - } - - /** - * Retrieves the detected smells for a file. - * @param filePath - The path of the file. - * @returns An array of smell entries. - */ - getSmellsForFile(filePath: string): Smell[] { - return this.detectedSmells.get(filePath) || []; - } - - /** - * Maps a smell description to a file path. - * @param smellDescription - The description of the smell. - * @param filePath - The path of the file. - */ - mapSmellToFile(smellDescription: string, filePath: string): void { - this.smellToFileMap.set(smellDescription, filePath); - } - - /** - * Retrieves the file path for a smell description. - * @param smellDescription - The description of the smell. - * @returns The file path, or `undefined` if not found. - */ - getFileForSmell(smellDescription: string): string | undefined { - return this.smellToFileMap.get(smellDescription); - } -} diff --git a/src/managers/SmellsViewUIManager.ts b/src/managers/SmellsViewUIManager.ts deleted file mode 100644 index 72e0c85..0000000 --- a/src/managers/SmellsViewUIManager.ts +++ /dev/null @@ -1,239 +0,0 @@ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import { SmellsViewStateManager } from './SmellsViewStateManager'; - -/** - * Manages the UI representation of files, folders, and detected smells in the VS Code tree view. - * This class handles creating tree items, assigning commands, and updating item states based on - * the analysis status and file state (e.g., outdated, queued, passed, failed). - */ -export class SmellsViewUIManager { - constructor(private stateManager: SmellsViewStateManager) {} - - /** - * Creates a tree item for a given element (folder, file, or smell). - * The tree item's appearance and behavior depend on the type of element and its current state. - * - * @param element - The file or folder path, or a detected smell. - * @returns A `vscode.TreeItem` representing the element. - */ - createTreeItem(element: string): vscode.TreeItem { - // Retrieve the current status and smell information for the element - const status = this.stateManager.getFileStatus(element); - const hasSmells = this.stateManager.getSmellsForFile(element).length > 0; - const isDirectory = fs.existsSync(element) && fs.statSync(element).isDirectory(); - const isSmellItem = !fs.existsSync(element) && !isDirectory; - - // Check if the file is outdated (needs reanalysis) - const isOutdated = - !isDirectory && !isSmellItem && this.stateManager.isFileOutdated(element); - - // Determine the collapsible state of the tree item - let collapsibleState: vscode.TreeItemCollapsibleState; - if (isDirectory) { - collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; // Folders are collapsible - } else if (isSmellItem) { - collapsibleState = vscode.TreeItemCollapsibleState.None; // Smells are not collapsible - } else if (isOutdated) { - collapsibleState = vscode.TreeItemCollapsibleState.None; // Outdated files are not collapsible - } else { - collapsibleState = hasSmells - ? vscode.TreeItemCollapsibleState.Collapsed // Files with smells are collapsible - : vscode.TreeItemCollapsibleState.None; // Files without smells are not collapsible - } - - // Create the tree item with the element's basename and collapsible state - const item = new vscode.TreeItem(path.basename(element), collapsibleState); - - // Customize the tree item based on its type (folder, file, or smell) - if (isDirectory) { - // Folders have a specific context value for styling and behavior - item.contextValue = 'ecoOptimizerFolder'; - } else if (!isSmellItem) { - // Files have a specific context value and can be opened - item.contextValue = 'ecoOptimizerFile'; - this.assignOpenFileCommand(item, element); // Assign a command to open the file - this.updateFileItem(item, status, isOutdated); // Update the item's appearance based on status - - // Add a context value for files with detected smells - if (hasSmells && status === 'passed' && !isOutdated) { - item.contextValue = 'ecoOptimizerFile-hasSmells'; - } - } else { - // Smells have a specific context value and display detailed information - item.contextValue = 'ecoOptimizerSmell'; - - // Retrieve the parent file and smell object for the smell item - const parentFile = this.stateManager.getFileForSmell(element); - if (parentFile) { - const smells = this.stateManager.getSmellsForFile(parentFile); - - // Extract the smell ID from the element's label - const idMatch = element.match(/\(ID:\s*([^)]+)\)/); - const id = idMatch ? idMatch[1] : null; - - // Find the smell by its ID - const smell = smells.find((s) => s.id === id); - - if (smell) { - // Set the label and description for the smell item - item.label = `${smell.messageId}: Line ${smell.occurences - .map((o) => o.line) - .join(', ')} (ID: ${smell.id}) `; - - // Assign a command to jump to the first occurrence of the smell in the file - const firstLine = smell.occurences[0]?.line - 1 || 0; // Default to line 0 if no occurrences - this.assignJumpToSmellCommand(item, parentFile, firstLine); - } - } - - // Set the tooltip for the smell item - this.setSmellTooltip(item, element); - } - - return item; - } - - /** - * Assigns a command to open a file when the tree item is clicked. - * - * @param item - The tree item to update. - * @param filePath - The path of the file to open. - */ - private assignOpenFileCommand(item: vscode.TreeItem, filePath: string): void { - item.command = { - command: 'ecooptimizer.openFile', - title: 'Open File', - arguments: [vscode.Uri.file(filePath)], - }; - } - - /** - * Assigns a command to jump to a specific line in a file when the tree item is clicked. - * - * @param item - The tree item to update. - * @param filePath - The path of the file containing the smell. - * @param line - The line number to jump to. - */ - private assignJumpToSmellCommand( - item: vscode.TreeItem, - filePath: string, - line: number, - ): void { - item.command = { - command: 'ecooptimizer.jumpToSmell', - title: 'Jump to Smell', - arguments: [filePath, line], - }; - } - - /** - * Updates the file item's appearance based on its analysis status and whether it is outdated. - * - * @param item - The tree item to update. - * @param status - The analysis status (e.g., "queued", "passed", "failed", "outdated"). - * @param isOutdated - Whether the file is outdated. - */ - private updateFileItem( - item: vscode.TreeItem, - status: string, - isOutdated: boolean, - ): void { - if (isOutdated) { - // Mark the file as outdated with a warning icon and description - item.description = 'outdated'; - item.iconPath = new vscode.ThemeIcon( - 'warning', - new vscode.ThemeColor('charts.orange'), - ); - item.tooltip = `${path.basename(this.getStatusMessage('outdated'))}`; - } else { - // Set the icon and tooltip based on the analysis status - item.iconPath = this.getStatusIcon(status); - item.tooltip = `${path.basename( - item.label as string, - )} (${this.getStatusMessage(status)})`; - } - } - - /** - * Sets the tooltip for a smell item. - * - * @param item - The tree item to update. - * @param smellDescription - The description of the smell. - */ - private setSmellTooltip(item: vscode.TreeItem, smellDescription: string): void { - item.tooltip = smellDescription; - } - - /** - * Retrieves the appropriate VS Code icon based on the smell analysis status. - * - * @param status - The analysis status. - * @returns The corresponding VS Code theme icon. - */ - private getStatusIcon(status: string): vscode.ThemeIcon { - switch (status) { - case 'queued': - return new vscode.ThemeIcon( - 'sync~spin', - new vscode.ThemeColor('charts.yellow'), - ); - case 'passed': - return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.green')); - case 'no_issues': - return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.blue')); - case 'failed': - return new vscode.ThemeIcon('error', new vscode.ThemeColor('charts.red')); - case 'outdated': - return new vscode.ThemeIcon( - 'warning', - new vscode.ThemeColor('charts.orange'), - ); - case 'server_down': - return new vscode.ThemeIcon( - 'server-process', - new vscode.ThemeColor('charts.red'), - ); - case 'refactoring': - return new vscode.ThemeIcon('robot', new vscode.ThemeColor('charts.purple')); - case 'accept-refactoring': - return new vscode.ThemeIcon( - 'warning', - new vscode.ThemeColor('charts.yellow'), - ); - default: - return new vscode.ThemeIcon('circle-outline'); - } - } - - /** - * Retrieves the status message corresponding to the smell analysis state. - * - * @param status - The analysis status. - * @returns A descriptive status message. - */ - private getStatusMessage(status: string): string { - switch (status) { - case 'queued': - return 'Analyzing Smells'; - case 'passed': - return 'Smells Successfully Detected'; - case 'failed': - return 'Error Detecting Smells'; - case 'no_issues': - return 'No Smells Found'; - case 'outdated': - return 'File Outdated - Needs Reanalysis'; - case 'server_down': - return 'Server Unavailable'; - case 'refactoring': - return 'Refactoring Currently Ongoing'; - case 'accept-refactoring': - return 'Successfully Refactored - Needs Reanalysis'; - default: - return 'Smells Not Yet Detected'; - } - } -} diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 59f0ccc..e591d11 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -183,7 +183,7 @@ class SmellTreeItem extends vscode.TreeItem { this.command = { title: 'Jump to Smell', command: 'ecooptimizer.jumpToSmell', - arguments: [smell.path, firstLine], + arguments: [smell.path, firstLine - 1], }; } } diff --git a/src/ui/LineSelection.ts b/src/ui/LineSelection.ts index e2933af..5160d1a 100644 --- a/src/ui/LineSelection.ts +++ b/src/ui/LineSelection.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { ecoOutput } from '../extension'; /** * Manages line selection and decoration in a VS Code editor, specifically for @@ -7,77 +8,71 @@ import { SmellsCacheManager } from '../context/SmellsCacheManager'; */ export class LineSelectionManager { private decoration: vscode.TextEditorDecorationType | null = null; - - /** - * Constructs a new instance of the `LineSelectionManager`. - * - * @param smellsCacheManager - An instance of `SmellsCacheManager` used to retrieve cached smells for a file. - */ - public constructor(private smellsCacheManager: SmellsCacheManager) {} + private lastDecoratedLine: number | null = null; + + constructor(private smellsCacheManager: SmellsCacheManager) { + // Listen for smell cache being cleared for any file + this.smellsCacheManager.onSmellsUpdated((targetFilePath) => { + if (targetFilePath === 'all') { + this.removeLastComment(); + return; + } + + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor && activeEditor.document.uri.fsPath === targetFilePath) { + ecoOutput.appendLine( + `[LineSelect] Cache cleared for active file — removing comment`, + ); + this.removeLastComment(); + } + }); + } /** * Removes the last applied decoration from the editor, if any. - * - * This method ensures that only one decoration is applied at a time by disposing - * of the previous decoration before applying a new one. */ public removeLastComment(): void { if (this.decoration) { - ecoOutput.appendLine('Removing decoration'); + ecoOutput.appendLine('[LineSelect] Removing decoration'); this.decoration.dispose(); + this.decoration = null; } + this.lastDecoratedLine = null; } /** - * Adds a comment to the currently selected line in the editor, indicating the presence - * of code smells. If multiple smells are present, it displays the first smell and a count - * of additional smells. - * - * @param editor - The active `vscode.TextEditor` instance where the comment will be applied. - * - * @remarks - * - If no smells are cached for the file, or if the selection spans multiple lines, no comment is added. - * - The comment is displayed as a decoration appended to the end of the selected line. + * Adds a comment to the currently selected line in the editor. */ public commentLine(editor: vscode.TextEditor): void { - this.removeLastComment(); - - if (!editor) { - return; - } + if (!editor) return; const filePath = editor.document.fileName; const smells = this.smellsCacheManager.getCachedSmells(filePath); - if (!smells) { + this.removeLastComment(); // If cache is gone, clear any previous comment return; } const { selection } = editor; - - if (!selection.isSingleLine) { - return; - } + if (!selection.isSingleLine) return; const selectedLine = selection.start.line; - ecoOutput.appendLine(`selection: ${selectedLine}`); - const smellsAtLine = smells.filter((smell: Smell) => { - return smell.occurences[0].line === selectedLine + 1; - }); + if (this.lastDecoratedLine === selectedLine) return; - if (smellsAtLine.length === 0) { - return; - } + this.removeLastComment(); + this.lastDecoratedLine = selectedLine; + ecoOutput.appendLine(`[LineSelect] Decorating line ${selectedLine + 1}`); + + const smellsAtLine = smells.filter((smell) => + smell.occurences.some((occ) => occ.line === selectedLine + 1), + ); - let comment; + if (smellsAtLine.length === 0) return; + let comment = `🍂 Smell: ${smellsAtLine[0].symbol}`; if (smellsAtLine.length > 1) { - comment = `🍂 Smell: ${smellsAtLine[0].symbol} | (+${ - smellsAtLine.length - 1 - })`; - } else { - comment = `🍂 Smell: ${smellsAtLine[0].symbol}`; + comment += ` | (+${smellsAtLine.length - 1})`; } const themeColor = new vscode.ThemeColor('editorLineNumber.foreground'); @@ -91,18 +86,14 @@ export class LineSelectionManager { }, }); - const selectionLine: vscode.Range[] = []; - - // Calculate the range for the decoration based on the line's content. - const line_text = editor.document.lineAt(selectedLine).text; - const line_length = line_text.length; - const indexStart = line_length - line_text.trimStart().length; - const indexEnd = line_text.trimEnd().length + 1; - - selectionLine.push( - new vscode.Range(selectedLine, indexStart, selectedLine, indexEnd), + const lineText = editor.document.lineAt(selectedLine).text; + const range = new vscode.Range( + selectedLine, + 0, + selectedLine, + lineText.trimEnd().length + 1, ); - editor.setDecorations(this.decoration, selectionLine); + editor.setDecorations(this.decoration, [range]); } } From 6ac1a256d77cfae5c4ff42d2c808883b21c33a64 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 04:58:54 -0400 Subject: [PATCH 059/121] added detectsmellsfolder --- package.json | 2 +- src/commands/detectSmells.ts | 122 +++++++++++++++++++++++------------ src/extension.ts | 14 +++- 3 files changed, 96 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 38e609a..fec3839 100644 --- a/package.json +++ b/package.json @@ -248,7 +248,7 @@ }, { "command": "ecooptimizer.detectSmellsFolder", - "when": "viewItem == ecooptimizer.smellsView && viewItem == folder", + "when": "view == ecooptimizer.smellsView && viewItem == directory", "group": "inline" }, { diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 8139626..957798e 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -1,4 +1,6 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; import { fetchSmells } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { getEnabledSmells } from '../utils/smellsData'; @@ -6,80 +8,120 @@ import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; /** - * Detects code smells for a given file. - * Uses cached smells if available; otherwise, fetches from the backend. - * - * @param filePath - The VS Code file URI or string path of the file to analyze. - * @param smellsViewProvider - UI provider for updating tree view. - * @param smellsCacheManager - Manager for caching smells. + * Runs smell detection on a single file if valid. */ export async function detectSmellsFile( filePath: string, smellsViewProvider: SmellsViewProvider, smellsCacheManager: SmellsCacheManager, ): Promise { - // STEP 0: Check cache first + const shouldProceed = await precheckAndMarkQueued( + filePath, + smellsViewProvider, + smellsCacheManager, + ); + + if (!shouldProceed) return; + + const enabledSmells = getEnabledSmells(); + const enabledSmellsForBackend = Object.fromEntries( + Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), + ); + + try { + const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); + + if (status === 200) { + if (smells.length > 0) { + smellsViewProvider.setStatus(filePath, 'passed'); + await smellsCacheManager.setCachedSmells(filePath, smells); + smellsViewProvider.setSmells(filePath, smells); + } else { + smellsViewProvider.setStatus(filePath, 'no_issues'); + await smellsCacheManager.setCachedSmells(filePath, []); + smellsViewProvider.setSmells(filePath, []); + } + } else { + smellsViewProvider.setStatus(filePath, 'failed'); + vscode.window.showErrorMessage(`Analysis failed (status ${status}).`); + smellsViewProvider.setSmells(filePath, []); + } + } catch (error: any) { + smellsViewProvider.setStatus(filePath, 'failed'); + vscode.window.showErrorMessage(`Analysis failed: ${error.message}`); + smellsViewProvider.setSmells(filePath, []); + } +} + +/** + * Validates workspace state before initiating detection. + */ +async function precheckAndMarkQueued( + filePath: string, + smellsViewProvider: SmellsViewProvider, + smellsCacheManager: SmellsCacheManager, +): Promise { if (smellsCacheManager.hasCachedSmells(filePath)) { const cached = smellsCacheManager.getCachedSmells(filePath); vscode.window.showInformationMessage('Using cached smells for this file.'); if (cached && cached.length > 0) { smellsViewProvider.setStatus(filePath, 'passed'); - smellsViewProvider.setSmells(filePath, cached); // Render cached smells in the tree + smellsViewProvider.setSmells(filePath, cached); } else { smellsViewProvider.setStatus(filePath, 'no_issues'); - smellsViewProvider.setSmells(filePath, []); // Clear any existing smells + smellsViewProvider.setSmells(filePath, []); } - return; + return false; } - // STEP 1: Check if server is down if (serverStatus.getStatus() === ServerStatusType.DOWN) { vscode.window.showWarningMessage( 'Action blocked: Server is down and no cached smells exist for this file version.', ); smellsViewProvider.setStatus(filePath, 'server_down'); - smellsViewProvider.setSmells(filePath, []); // Clear any existing smells - return; + smellsViewProvider.setSmells(filePath, []); + return false; } - // STEP 2: Get enabled smells const enabledSmells = getEnabledSmells(); if (Object.keys(enabledSmells).length === 0) { vscode.window.showWarningMessage( 'No enabled smells found. Please configure enabled smells in the settings.', ); - smellsViewProvider.setSmells(filePath, []); // Clear any existing smells - return; + smellsViewProvider.setSmells(filePath, []); + return false; } - const enabledSmellsForBackend = Object.fromEntries( - Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), - ); - - // STEP 3: Queue analysis smellsViewProvider.setStatus(filePath, 'queued'); + return true; +} - try { - const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); +/** + * Detects smells in all Python files within the selected folder. + */ +export async function detectSmellsFolder( + folderPath: string, + smellsViewProvider: SmellsViewProvider, + smellsCacheManager: SmellsCacheManager, +): Promise { + const pythonFiles: string[] = []; - if (status === 200) { - if (smells.length > 0) { - smellsViewProvider.setStatus(filePath, 'passed'); - await smellsCacheManager.setCachedSmells(filePath, smells); - smellsViewProvider.setSmells(filePath, smells); // Render detected smells in the tree - } else { - smellsViewProvider.setStatus(filePath, 'no_issues'); - await smellsCacheManager.setCachedSmells(filePath, []); - smellsViewProvider.setSmells(filePath, []); // Clear any existing smells + function walk(dir: string): void { + const entries = fs.readdirSync(dir); + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + walk(fullPath); + } else if (stat.isFile() && fullPath.endsWith('.py')) { + pythonFiles.push(fullPath); } - } else { - smellsViewProvider.setStatus(filePath, 'failed'); - vscode.window.showErrorMessage(`Analysis failed (status ${status}).`); - smellsViewProvider.setSmells(filePath, []); // Clear any existing smells } - } catch (error: any) { - smellsViewProvider.setStatus(filePath, 'failed'); - vscode.window.showErrorMessage(`Analysis failed: ${error.message}`); - smellsViewProvider.setSmells(filePath, []); // Clear any existing smells + } + + walk(folderPath); + + for (const file of pythonFiles) { + await detectSmellsFile(file, smellsViewProvider, smellsCacheManager); } } diff --git a/src/extension.ts b/src/extension.ts index 22cfaf9..ae121ab 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,7 +16,7 @@ import { FilterViewProvider } from './providers/FilterViewProvider'; // Commands import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; -import { detectSmellsFile } from './commands/detectSmells'; +import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; import { registerFilterSmellCommands } from './commands/filterSmells'; // Listeners @@ -121,6 +121,18 @@ export function activate(context: vscode.ExtensionContext): void { vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); } }), + + vscode.commands.registerCommand( + 'ecooptimizer.detectSmellsFolder', + (folderItem) => { + const folderPath = folderItem?.resourceUri?.fsPath; + if (!folderPath) { + vscode.window.showWarningMessage('No folder selected.'); + return; + } + detectSmellsFolder(folderPath, smellsViewProvider, smellsCacheManager); + }, + ), ); // Register filter UI toggle/edit/select-all/deselect-all From 6225ce6c1d333d1c5f30eaf4a06c6cd4c336720c Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 05:30:18 -0400 Subject: [PATCH 060/121] clean --- package.json | 36 ++++++++++++++++++++++++++++- src/api/backend.ts | 1 + src/commands/detectSmells.ts | 6 ----- src/providers/SmellsViewProvider.ts | 15 ++++++++++-- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index fec3839..2bafddc 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,30 @@ "title": "Detect Smells", "icon": "$(search)", "category": "Eco" + }, + { + "command": "ecooptimizer.refactorAllSmellsOfType", + "title": "Refactor Smells By Type", + "icon": "$(tools)", + "category": "Eco" + }, + { + "command": "ecooptimizer.refactorSmell", + "title": "Refactor Smell", + "icon": "$(tools)", + "category": "Eco" + }, + { + "command": "ecooptimizer.acceptRefactoring", + "title": "Accept Refactoring", + "category": "Eco", + "icon": "$(check)" + }, + { + "command": "ecooptimizer.rejectRefactoring", + "title": "Reject Refactoring", + "category": "Eco", + "icon": "$(close)" } ], "menus": { @@ -253,7 +277,17 @@ }, { "command": "ecooptimizer.detectSmellsFile", - "when": "view == ecooptimizer.smellsView && viewItem == file", + "when": "view == ecooptimizer.smellsView && (viewItem == file || viewItem == file_with_smells)", + "group": "inline" + }, + { + "command": "ecooptimizer.refactorAllSmellsOfType", + "when": "view == ecooptimizer.smellsView && viewItem == file_with_smells", + "group": "inline" + }, + { + "command": "ecooptimizer.refactorSmell", + "when": "view == ecooptimizer.smellsView && viewItem == smell", "group": "inline" } ] diff --git a/src/api/backend.ts b/src/api/backend.ts index 531dfbf..7ce08aa 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -2,6 +2,7 @@ import { envConfig } from '../utils/envConfig'; import { serverStatus } from '../emitters/serverStatus'; import { ServerStatusType } from '../emitters/serverStatus'; import * as vscode from 'vscode'; +import { ecoOutput } from '../extension'; const BASE_URL = `http://${envConfig.SERVER_URL}`; // API URL for Python backend diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 957798e..99c1d56 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -39,17 +39,14 @@ export async function detectSmellsFile( } else { smellsViewProvider.setStatus(filePath, 'no_issues'); await smellsCacheManager.setCachedSmells(filePath, []); - smellsViewProvider.setSmells(filePath, []); } } else { smellsViewProvider.setStatus(filePath, 'failed'); vscode.window.showErrorMessage(`Analysis failed (status ${status}).`); - smellsViewProvider.setSmells(filePath, []); } } catch (error: any) { smellsViewProvider.setStatus(filePath, 'failed'); vscode.window.showErrorMessage(`Analysis failed: ${error.message}`); - smellsViewProvider.setSmells(filePath, []); } } @@ -69,7 +66,6 @@ async function precheckAndMarkQueued( smellsViewProvider.setSmells(filePath, cached); } else { smellsViewProvider.setStatus(filePath, 'no_issues'); - smellsViewProvider.setSmells(filePath, []); } return false; } @@ -79,7 +75,6 @@ async function precheckAndMarkQueued( 'Action blocked: Server is down and no cached smells exist for this file version.', ); smellsViewProvider.setStatus(filePath, 'server_down'); - smellsViewProvider.setSmells(filePath, []); return false; } @@ -88,7 +83,6 @@ async function precheckAndMarkQueued( vscode.window.showWarningMessage( 'No enabled smells found. Please configure enabled smells in the settings.', ); - smellsViewProvider.setSmells(filePath, []); return false; } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index e591d11..4a4ec6d 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -77,7 +77,10 @@ export class SmellsViewProvider } // If file node, show smells - if (element?.contextValue === 'file') { + if ( + element?.contextValue === 'file' || + element?.contextValue === 'file_with_smells' + ) { ecoOutput.appendLine(`Getting smells for file: ${element.fullPath}`); const smells = this.fileSmells.get(element.fullPath) ?? []; return smells.map((smell) => new SmellTreeItem(smell)); @@ -133,6 +136,14 @@ export class SmellsViewProvider item.iconPath = icon; item.tooltip = tooltip; + if ( + isFile && + this.fileSmells.has(filePath) && + this.fileSmells.get(filePath)!.length > 0 + ) { + item.contextValue = 'file_with_smells'; + } + if (status === 'outdated') { item.description = 'outdated'; } @@ -152,7 +163,7 @@ class TreeItem extends vscode.TreeItem { this.resourceUri = vscode.Uri.file(fullPath); this.contextValue = contextValue; - if (contextValue === 'file') { + if (contextValue === 'file' || contextValue === 'file_with_smells') { this.command = { title: 'Open File', command: 'vscode.open', From 646ece4d65104debecc13ba6df3dafe4e5c5f208 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 05:35:36 -0400 Subject: [PATCH 061/121] clean --- src/providers/SmellsViewProvider.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 4a4ec6d..154c697 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -120,22 +120,18 @@ export class SmellsViewProvider const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); const tooltip = isFile ? getStatusMessage(status) : undefined; - // Set collapsible state const collapsibleState = isFile ? this.fileSmells.has(filePath) && this.fileSmells.get(filePath)!.length > 0 - ? vscode.TreeItemCollapsibleState.Collapsed // Files with smells are collapsible - : vscode.TreeItemCollapsibleState.None // Files without smells are not collapsible - : vscode.TreeItemCollapsibleState.Collapsed; // Folders are always collapsible - - const item = new TreeItem( - label, - filePath, - collapsibleState, - isFile ? 'file' : 'directory', - ); + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + : vscode.TreeItemCollapsibleState.Collapsed; + + const baseContext = isFile ? 'file' : 'directory'; + const item = new TreeItem(label, filePath, collapsibleState, baseContext); item.iconPath = icon; item.tooltip = tooltip; + // Override contextValue if file has smells if ( isFile && this.fileSmells.has(filePath) && @@ -148,6 +144,13 @@ export class SmellsViewProvider item.description = 'outdated'; } + // ✅ Log the context value + ecoOutput.appendLine(`Created TreeItem: ${filePath}`); + ecoOutput.appendLine(` → Label: ${label}`); + ecoOutput.appendLine(` → isFile: ${isFile}`); + ecoOutput.appendLine(` → Context Value: ${item.contextValue}`); + ecoOutput.appendLine(` → Status: ${status}`); + return item; } } From ffcf5fa30ec2399d972dccd8e0feffa406a6946c Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 05:40:22 -0400 Subject: [PATCH 062/121] clean --- src/commands/refactorSmell.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 3200662..039f7b3 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -7,6 +7,7 @@ import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; +import { ecoOutput } from '../extension'; function normalizePath(filePath: string): string { const normalizedPath = filePath.toLowerCase(); // Normalize case for consistent Map keying @@ -33,7 +34,7 @@ export async function refactorSmell( vscode.window.showInformationMessage(`Refactoring code smell: ${smell.symbol}`); // Update UI to indicate the file is queued for analysis - smellsDataProvider.updateStatus(smell.path, 'queued'); + smellsDataProvider.setStatus(smell.path, 'queued'); try { // Set a context key to track that refactoring is in progress @@ -43,7 +44,7 @@ export async function refactorSmell( const refactoredData = await backendRefactorSmell(smell); // Log the response from the backend - ecoOutput.appendLine('Refactoring response:', refactoredData); + ecoOutput.appendLine(`Refactoring response: ${JSON.stringify(refactoredData)}`); // Update the refactoring details view with the target file, affected files, and energy saved refactoringDetailsViewProvider.updateRefactoringDetails( @@ -116,10 +117,10 @@ export async function acceptRefactoring( const targetSmell = refactoringDetailsViewProvider.targetSmell; const file = vscode.Uri.file(targetFile.original).fsPath; - ecoOutput.appendLine('Energy: %d, smell: %s', energySaved, targetSmell); + ecoOutput.appendLine(`Energy: ${energySaved}, smell: ${targetSmell}`); if (energySaved && targetSmell) { - ecoOutput.appendLine('Updating metrics for', file); + ecoOutput.appendLine(`Updating metrics for ${file}`); metricsDataProvider.updateMetrics(file, energySaved, targetSmell); } @@ -137,9 +138,9 @@ export async function acceptRefactoring( } // Mark the target file and affected files as outdated - smellsViewProvider.markFileAsOutdated(normalizePath(targetFile.original)); + smellsViewProvider.setStatus(normalizePath(targetFile.original), 'outdated'); for (const file of affectedFiles) { - smellsViewProvider.markFileAsOutdated(normalizePath(file.original)); + smellsViewProvider.setStatus(normalizePath(targetFile.original), 'outdated'); } // Reset the refactoring details view From 0da7aaa2132c519635d1c41f0112eb85b5378c5d Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 06:20:33 -0400 Subject: [PATCH 063/121] added refactoring --- package.json | 14 +++- src/commands/acceptRefactoring.ts | 68 ++++++++++++++++ src/commands/refactorSmell.ts | 117 ++-------------------------- src/commands/rejectRefactoring.ts | 12 +++ src/extension.ts | 96 +++++++++++------------ src/providers/SmellsViewProvider.ts | 2 +- src/utils/openDiffEditor.ts | 27 +++++++ 7 files changed, 173 insertions(+), 163 deletions(-) create mode 100644 src/commands/acceptRefactoring.ts create mode 100644 src/commands/rejectRefactoring.ts create mode 100644 src/utils/openDiffEditor.ts diff --git a/package.json b/package.json index 2bafddc..ff8c333 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "views": { "ecooptimizer": [ { - "id": "ecooptimizer.refactoringDetails", + "id": "ecooptimizer.refactorView", "name": "Refactoring Details", "icon": "assets/eco-icon.png" }, @@ -152,7 +152,7 @@ }, "viewsWelcome": [ { - "view": "ecooptimizer.refactoringDetails", + "view": "ecooptimizer.refactorView", "contents": "Refactoring is currently not in progress. Try selecting a smell in the Code Smells view to start refactoring.", "when": "!refactoringInProgress" }, @@ -289,6 +289,16 @@ "command": "ecooptimizer.refactorSmell", "when": "view == ecooptimizer.smellsView && viewItem == smell", "group": "inline" + }, + { + "command": "ecooptimizer.acceptRefactoring", + "when": "view == ecooptimizer.refactorView && refactoringInProgress", + "group": "navigation" + }, + { + "command": "ecooptimizer.rejectRefactoring", + "when": "view == ecooptimizer.refactorView && refactoringInProgress", + "group": "navigation" } ] }, diff --git a/src/commands/acceptRefactoring.ts b/src/commands/acceptRefactoring.ts new file mode 100644 index 0000000..332ecae --- /dev/null +++ b/src/commands/acceptRefactoring.ts @@ -0,0 +1,68 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { MetricsViewProvider } from '../providers/MetricsViewProvider'; +import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { ecoOutput } from '../extension'; + +function normalizePath(filePath: string): string { + return filePath.toLowerCase(); +} + +export async function acceptRefactoring( + refactoringDetailsViewProvider: RefactoringDetailsViewProvider, + metricsDataProvider: MetricsViewProvider, + smellsCacheManager: SmellsCacheManager, + smellsViewProvider: SmellsViewProvider, +): Promise { + const targetFile = refactoringDetailsViewProvider.targetFile; + const affectedFiles = refactoringDetailsViewProvider.affectedFiles; + + if (!targetFile || !affectedFiles) { + vscode.window.showErrorMessage('No refactoring data available.'); + return; + } + + try { + fs.copyFileSync(targetFile.refactored, targetFile.original); + for (const file of affectedFiles) { + fs.copyFileSync(file.refactored, file.original); + } + + const energySaved = refactoringDetailsViewProvider.energySaved; + const targetSmell = refactoringDetailsViewProvider.targetSmell; + const file = vscode.Uri.file(targetFile.original).fsPath; + + if (energySaved && targetSmell) { + ecoOutput.appendLine(`Updating metrics for ${file}`); + metricsDataProvider.updateMetrics(file, energySaved, targetSmell); + } + + vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); + + await smellsCacheManager.clearCachedSmellsForFile( + normalizePath(targetFile.original), + ); + for (const file of affectedFiles) { + await smellsCacheManager.clearCachedSmellsForFile( + normalizePath(file.original), + ); + } + + smellsViewProvider.setStatus(normalizePath(targetFile.original), 'outdated'); + for (const file of affectedFiles) { + smellsViewProvider.setStatus(normalizePath(file.original), 'outdated'); + } + + refactoringDetailsViewProvider.resetRefactoringDetails(); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + smellsViewProvider.refresh(); + } catch (error) { + console.error('Failed to accept refactoring:', error); + vscode.window.showErrorMessage( + 'Failed to accept refactoring. Please try again.', + ); + } +} diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 039f7b3..755ce7c 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,13 +1,10 @@ import * as vscode from 'vscode'; -import { basename } from 'path'; -import * as fs from 'fs'; import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { MetricsViewProvider } from '../providers/MetricsViewProvider'; import { ecoOutput } from '../extension'; +import { openDiffEditor } from '../utils/openDiffEditor'; function normalizePath(filePath: string): string { const normalizedPath = filePath.toLowerCase(); // Normalize case for consistent Map keying @@ -55,22 +52,13 @@ export async function refactorSmell( ); // Show a diff view for the target file - const targetFile = refactoredData.targetFile; - const fileName = basename(targetFile.original); - const originalUri = vscode.Uri.file(targetFile.original); - const refactoredUri = vscode.Uri.file(targetFile.refactored); - await vscode.commands.executeCommand( - 'vscode.diff', - originalUri, - refactoredUri, - `Refactoring Comparison (${fileName})`, - { - preview: false, // Ensure the diff editor is not in preview mode - }, + await openDiffEditor( + refactoredData.targetFile.original, + refactoredData.targetFile.refactored, ); // Focus on the Refactoring Details view - await vscode.commands.executeCommand('ecooptimizer.refactoringDetails.focus'); + await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); // Notify the user vscode.window.showInformationMessage( @@ -85,98 +73,3 @@ export async function refactorSmell( vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } - -/** - * Accepts the refactoring changes and saves the refactored files. - * Marks the modified files as outdated and clears their smell cache. - */ -export async function acceptRefactoring( - refactoringDetailsViewProvider: RefactoringDetailsViewProvider, - metricsDataProvider: MetricsViewProvider, - smellsCacheManager: SmellsCacheManager, - smellsViewProvider: SmellsViewProvider, -): Promise { - const targetFile = refactoringDetailsViewProvider.targetFile; - const affectedFiles = refactoringDetailsViewProvider.affectedFiles; - - if (!targetFile || !affectedFiles) { - vscode.window.showErrorMessage('No refactoring data available.'); - return; - } - - try { - // Save the refactored target file - fs.copyFileSync(targetFile.refactored, targetFile.original); - - // Save the refactored affected files - for (const file of affectedFiles) { - fs.copyFileSync(file.refactored, file.original); - } - - const energySaved = refactoringDetailsViewProvider.energySaved; - const targetSmell = refactoringDetailsViewProvider.targetSmell; - const file = vscode.Uri.file(targetFile.original).fsPath; - - ecoOutput.appendLine(`Energy: ${energySaved}, smell: ${targetSmell}`); - - if (energySaved && targetSmell) { - ecoOutput.appendLine(`Updating metrics for ${file}`); - metricsDataProvider.updateMetrics(file, energySaved, targetSmell); - } - - // Notify the user - vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); - - // Clear the smell cache for the target file and affected files - await smellsCacheManager.clearCachedSmellsForFile( - normalizePath(targetFile.original), - ); - for (const file of affectedFiles) { - await smellsCacheManager.clearCachedSmellsForFile( - normalizePath(file.original), - ); - } - - // Mark the target file and affected files as outdated - smellsViewProvider.setStatus(normalizePath(targetFile.original), 'outdated'); - for (const file of affectedFiles) { - smellsViewProvider.setStatus(normalizePath(targetFile.original), 'outdated'); - } - - // Reset the refactoring details view - refactoringDetailsViewProvider.resetRefactoringDetails(); - - // Close all diff editors - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - - // Set the context key to indicate refactoring is no longer in progress - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); - - // Refresh the UI to reflect the outdated status of the modified files - smellsViewProvider.refresh(); - } catch (error) { - console.error('Failed to accept refactoring:', error); - vscode.window.showErrorMessage( - 'Failed to accept refactoring. Please try again.', - ); - } -} - -/** - * Rejects the refactoring changes and keeps the original files. - */ -export async function rejectRefactoring( - refactoringDetailsViewProvider: RefactoringDetailsViewProvider, -): Promise { - // Notify the user - vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); - - // Reset the refactoring details view - refactoringDetailsViewProvider.resetRefactoringDetails(); - - // Close all diff editors - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - - // Set the context key to indicate refactoring is no longer in progress - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); -} diff --git a/src/commands/rejectRefactoring.ts b/src/commands/rejectRefactoring.ts new file mode 100644 index 0000000..f7a4369 --- /dev/null +++ b/src/commands/rejectRefactoring.ts @@ -0,0 +1,12 @@ +import * as vscode from 'vscode'; +import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; + +export async function rejectRefactoring( + refactoringDetailsViewProvider: RefactoringDetailsViewProvider, +): Promise { + vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); + + refactoringDetailsViewProvider.resetRefactoringDetails(); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); +} diff --git a/src/extension.ts b/src/extension.ts index ae121ab..5f839a0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,35 +3,36 @@ import * as vscode from 'vscode'; // Output channel for logging export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); -// Core utilities +// === Core Utilities === import { loadSmells } from './utils/smellsData'; import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; +import { openDiffEditor } from './utils/openDiffEditor'; -// Context & View Providers +// === Context & View Providers === import { SmellsCacheManager } from './context/SmellsCacheManager'; -import { SmellsViewProvider } from './providers/SmellsViewProvider'; +import { SmellsViewProvider, SmellTreeItem } from './providers/SmellsViewProvider'; import { MetricsViewProvider } from './providers/MetricsViewProvider'; import { FilterViewProvider } from './providers/FilterViewProvider'; +import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; -// Commands +// === Commands === import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; import { registerFilterSmellCommands } from './commands/filterSmells'; - -// Listeners -import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; +import { refactorSmell } from './commands/refactorSmell'; + +// === Listeners & UI === +import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { LineSelectionManager } from './ui/LineSelection'; -/** - * Activates the Eco-Optimizer extension and registers all necessary components. - */ export function activate(context: vscode.ExtensionContext): void { - // Load smell definitions and initialize context + // Load smell definitions loadSmells(); + // === Initialize Managers & Providers === const smellsCacheManager = new SmellsCacheManager(context); const smellsViewProvider = new SmellsViewProvider(context); const metricsViewProvider = new MetricsViewProvider(context); @@ -41,11 +42,12 @@ export function activate(context: vscode.ExtensionContext): void { smellsCacheManager, smellsViewProvider, ); + const refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); // Restore cached statuses initializeStatusesFromCache(context, smellsCacheManager, smellsViewProvider); - // === Tree Views === + // === Register Tree Views === context.subscriptions.push( vscode.window.createTreeView('ecooptimizer.smellsView', { treeDataProvider: smellsViewProvider, @@ -58,9 +60,12 @@ export function activate(context: vscode.ExtensionContext): void { treeDataProvider: filterSmellsProvider, showCollapseAll: true, }), + vscode.window.createTreeView('ecooptimizer.refactorView', { + treeDataProvider: refactoringDetailsViewProvider, + }), ); - // Link checkbox UI to filter logic + // Connect checkbox UI logic filterSmellsProvider.setTreeView( vscode.window.createTreeView('ecooptimizer.filterView', { treeDataProvider: filterSmellsProvider, @@ -68,7 +73,7 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // === Workspace Context Flag === + // Set workspace configuration context const workspaceConfigured = Boolean( context.workspaceState.get('workspaceConfiguredPath'), ); @@ -78,7 +83,7 @@ export function activate(context: vscode.ExtensionContext): void { workspaceConfigured, ); - // === Command Registration === + // === Register Commands === context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.configureWorkspace', async () => { await configureWorkspace(context); @@ -88,13 +93,11 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('ecooptimizer.resetConfiguration', async () => { const didReset = await resetConfiguration(context); - if (didReset) { smellsCacheManager.clearAllCachedSmells(); smellsViewProvider.clearAllStatuses(); smellsViewProvider.refresh(); metricsViewProvider.refresh(); - vscode.window.showInformationMessage( 'Workspace configuration has been reset. All analysis data has been cleared.', ); @@ -108,18 +111,12 @@ export function activate(context: vscode.ExtensionContext): void { }), vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileItem) => { - try { - const filePath = fileItem?.resourceUri?.fsPath; - if (!filePath) { - vscode.window.showWarningMessage( - 'No file selected or file path not found.', - ); - return; - } - detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); - } catch (error: any) { - vscode.window.showErrorMessage(`Error detecting smells: ${error.message}`); + const filePath = fileItem?.resourceUri?.fsPath; + if (!filePath) { + vscode.window.showWarningMessage('No file selected or file path not found.'); + return; } + detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); }), vscode.commands.registerCommand( @@ -133,12 +130,31 @@ export function activate(context: vscode.ExtensionContext): void { detectSmellsFolder(folderPath, smellsViewProvider, smellsCacheManager); }, ), + + vscode.commands.registerCommand( + 'ecooptimizer.refactorSmell', + (item: SmellTreeItem) => { + const smell = item?.smell; + if (!smell) { + vscode.window.showErrorMessage('No smell found for this item.'); + return; + } + refactorSmell(smellsViewProvider, refactoringDetailsViewProvider, smell); + }, + ), + + vscode.commands.registerCommand( + 'ecooptimizer.openDiffEditor', + (originalFilePath: string, refactoredFilePath: string) => { + openDiffEditor(originalFilePath, refactoredFilePath); + }, + ), ); - // Register filter UI toggle/edit/select-all/deselect-all + // Register filter-related commands registerFilterSmellCommands(context, filterSmellsProvider); - // === Workspace File Listener === + // === Watch for workspace changes === context.subscriptions.push( new WorkspaceModifiedListener( context, @@ -148,31 +164,15 @@ export function activate(context: vscode.ExtensionContext): void { ), ); - // Initialize the FileHighlighter for highlighting code smells. - // const fileHighlighter = FileHighlighter.getInstance(smellsCacheManager); - - // fileHighlighter.updateHighlightsForVisibleEditors(); - - // context.subscriptions.push( - // vscode.window.onDidChangeVisibleTextEditors((editors) => { - // editors.forEach((editor) => { - // fileHighlighter.highlightSmells(editor); - // }); - // }), - // ); - + // === Register Line Selection Listener === const lineSelectManager = new LineSelectionManager(smellsCacheManager); context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection((event) => { - console.log('Eco: Detected line selection event'); lineSelectManager.commentLine(event.textEditor); }), ); } -/** - * Called when the extension is deactivated. - */ export function deactivate(): void { - ecoOutput.appendLine('Deactivating Eco-Optimizer extension...\n'); + ecoOutput.appendLine('Deactivating Eco-Optimizer extension...'); } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 154c697..e7c5830 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -176,7 +176,7 @@ class TreeItem extends vscode.TreeItem { } } -class SmellTreeItem extends vscode.TreeItem { +export class SmellTreeItem extends vscode.TreeItem { constructor(public readonly smell: Smell) { const acronym = getAcronymByMessageId(smell.messageId) ?? smell.messageId; const lines = smell.occurences diff --git a/src/utils/openDiffEditor.ts b/src/utils/openDiffEditor.ts new file mode 100644 index 0000000..f31eb26 --- /dev/null +++ b/src/utils/openDiffEditor.ts @@ -0,0 +1,27 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +/** + * Opens a VS Code diff editor comparing the original and refactored file. + * + * @param originalFilePath - Path to the original file. + * @param refactoredFilePath - Path to the refactored version of the file. + */ +export async function openDiffEditor( + originalFilePath: string, + refactoredFilePath: string, +): Promise { + const fileName = path.basename(originalFilePath); + const originalUri = vscode.Uri.file(originalFilePath); + const refactoredUri = vscode.Uri.file(refactoredFilePath); + + await vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + refactoredUri, + `Refactoring Comparison (${fileName})`, + { + preview: false, + }, + ); +} From 30af34f20b2f3c394f59cc860d48e001648dc8bf Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 06:58:33 -0400 Subject: [PATCH 064/121] added refactoring --- src/api/backend.ts | 29 ++---- src/commands/configureWorkspace.ts | 89 +++++-------------- src/commands/refactorSmell.ts | 71 ++++++++++----- src/extension.ts | 7 +- .../RefactoringDetailsViewProvider.ts | 2 +- 5 files changed, 86 insertions(+), 112 deletions(-) diff --git a/src/api/backend.ts b/src/api/backend.ts index 7ce08aa..6621bd0 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -1,7 +1,6 @@ import { envConfig } from '../utils/envConfig'; import { serverStatus } from '../emitters/serverStatus'; import { ServerStatusType } from '../emitters/serverStatus'; -import * as vscode from 'vscode'; import { ecoOutput } from '../extension'; const BASE_URL = `http://${envConfig.SERVER_URL}`; // API URL for Python backend @@ -71,37 +70,25 @@ export async function fetchSmells( * Sends a request to the backend to refactor a specific smell. * * @param smell - The smell to refactor. + * @param workspacePath - The user-configured workspace root directory. * @returns A promise resolving to the refactored data or throwing an error if unsuccessful. */ export async function backendRefactorSmell( smell: Smell, + workspacePath: string, ): Promise { const url = `${BASE_URL}/refactor`; - // Extract the file path from the smell object - const filePath = smell.path; - - // Find the workspace folder containing the file - const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => - filePath.includes(folder.uri.fsPath), - ); - - if (!workspaceFolder) { - console.error('Eco: Error - Unable to determine workspace folder for', filePath); - throw new Error( - `Eco: Unable to find a matching workspace folder for file: ${filePath}`, - ); + if (!workspacePath) { + throw new Error('No workspace path provided for refactoring.'); } - const workspaceFolderPath = workspaceFolder.uri.fsPath; - ecoOutput.appendLine( - `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspaceFolderPath}"`, + `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspacePath}"`, ); - // Prepare the payload for the backend const payload = { - source_dir: workspaceFolderPath, + source_dir: workspacePath, smell, }; @@ -122,7 +109,7 @@ export async function backendRefactorSmell( const refactorResult = (await response.json()) as RefactoredData; return refactorResult; } catch (error: any) { - console.error('Eco: Unexpected error in refactorSmell:', error); + console.error('Eco: Unexpected error in backendRefactorSmell:', error); throw new Error(`Refactoring failed: ${error.message}`); } -} \ No newline at end of file +} diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index 62d88c9..9d2afef 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -3,70 +3,14 @@ import * as path from 'path'; import * as fs from 'fs'; /** - * Prompts the user to configure a workspace by selecting either a Python file or folder. + * Prompts the user to configure a workspace by selecting a folder containing Python files. * Updates the workspace state accordingly and refreshes the tree view to reflect the changes. * * @param context - The extension context for managing workspace state. */ export async function configureWorkspace(context: vscode.ExtensionContext) { - const choice = await vscode.window.showQuickPick( - ['Configure a Python File', 'Configure a Python Folder'], - { placeHolder: 'Choose whether to configure a Python file or folder.' }, - ); - - if (!choice) return; - - if (choice === 'Configure a Python File') { - await configurePythonFile(context); - } else { - await configurePythonFolder(context); - } -} - -/** - * Configures the workspace using a selected Python file. - * Prompts the user to select a Python file from open editors or the workspace. - * - * @param context - The extension context for managing workspace state. - * @param smellsViewProvider - The provider for the smells view. - * @param metricsViewProvider - The provider for the metrics view. - */ -async function configurePythonFile(context: vscode.ExtensionContext) { - // Get Python files from open editors - const openEditorFiles = vscode.window.tabGroups.activeTabGroup.tabs - .map((tab) => (tab.input as any)?.uri?.fsPath) - .filter((filePath) => filePath && filePath.endsWith('.py')); - - // Get Python files from the workspace - const workspaceFiles = await vscode.workspace.findFiles( - '**/*.py', - '**/node_modules/**', - ); - const workspaceFilePaths = workspaceFiles.map((uri) => uri.fsPath); - - // Combine and deduplicate file paths - const allPythonFiles = Array.from( - new Set([...openEditorFiles, ...workspaceFilePaths]), - ); - - if (allPythonFiles.length === 0) { - vscode.window.showErrorMessage( - 'No Python files found in open editors or workspace.', - ); - return; - } - - // Prompt the user to select a Python file - const selectedFile = await vscode.window.showQuickPick(allPythonFiles, { - placeHolder: 'Select a Python file to use as your workspace.', - }); - - if (selectedFile) { - await updateWorkspace(context, selectedFile); - vscode.window.showInformationMessage( - `Workspace configured for file: ${path.basename(selectedFile)}`, - ); - } + // Directly configure a Python folder (removed the file option) + await configurePythonFolder(context); } /** @@ -139,20 +83,33 @@ async function configurePythonFolder(context: vscode.ExtensionContext) { if (validPythonFolders.length === 0) { vscode.window.showErrorMessage( - 'No valid Python folders found in your workspace.', + 'No valid Python folders found in your workspace. A valid folder must contain Python files (*.py) or an __init__.py file.', ); return; } - // Prompt the user to select a Python folder - const selectedFolder = await vscode.window.showQuickPick(validPythonFolders, { - placeHolder: 'Select a Python folder to use as your workspace.', - }); + // Show folder selection dialog + const selectedFolder = await vscode.window.showQuickPick( + validPythonFolders.map((folder) => ({ + label: path.basename(folder), + description: folder, + detail: `Contains Python files: ${fs + .readdirSync(folder) + .filter((file) => file.endsWith('.py') || file === '__init__.py') + .join(', ')}`, + folderPath: folder, + })), + { + placeHolder: 'Select a Python folder to use as your workspace', + matchOnDescription: true, + matchOnDetail: true, + }, + ); if (selectedFolder) { - await updateWorkspace(context, selectedFolder); + await updateWorkspace(context, selectedFolder.folderPath); vscode.window.showInformationMessage( - `Workspace configured for folder: ${path.basename(selectedFolder)}`, + `Workspace configured for folder: ${path.basename(selectedFolder.folderPath)}`, ); } } diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 755ce7c..5ce356e 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,66 +1,92 @@ import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { ecoOutput } from '../extension'; import { openDiffEditor } from '../utils/openDiffEditor'; +import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; -function normalizePath(filePath: string): string { - const normalizedPath = filePath.toLowerCase(); // Normalize case for consistent Map keying - return normalizedPath; +/** + * Recursively collects all `.py` files in the given directory. + */ +function getAllPythonFiles(dir: string): string[] { + const result: string[] = []; + + const walk = (current: string) => { + const entries = fs.readdirSync(current); + for (const entry of entries) { + const fullPath = path.join(current, entry); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + walk(fullPath); + } else if (stat.isFile() && fullPath.endsWith('.py')) { + result.push(fullPath); + } + } + }; + + walk(dir); + return result; } /** * Handles the refactoring of a specific smell. - * - * @param treeDataProvider - The tree data provider for updating the UI. - * @param refactoringDetailsViewProvider - The refactoring details view provider. - * @param smell - The smell to refactor. */ export async function refactorSmell( - smellsDataProvider: SmellsViewProvider, + smellsViewProvider: SmellsViewProvider, refactoringDetailsViewProvider: RefactoringDetailsViewProvider, smell: Smell, + context: vscode.ExtensionContext, ): Promise { - if (!smell) { - vscode.window.showErrorMessage('Error: Invalid smell.'); + vscode.window.showInformationMessage(`Refactoring code smell: ${smell.symbol}`); + + const workspacePath = context.workspaceState.get( + 'workspaceConfiguredPath', + ); + if (!workspacePath) { + vscode.window.showErrorMessage('No workspace configured.'); return; } - vscode.window.showInformationMessage(`Refactoring code smell: ${smell.symbol}`); + // Step 1: Mark every Python file in the workspace as "refactoring" + const allPythonFiles = getAllPythonFiles(workspacePath); + for (const filePath of allPythonFiles) { + smellsViewProvider.setStatus(filePath, 'refactoring'); + } - // Update UI to indicate the file is queued for analysis - smellsDataProvider.setStatus(smell.path, 'queued'); + // Step 2: Check if the server is down (can overwrite to "server_down" if needed) + if (serverStatus.getStatus() === ServerStatusType.DOWN) { + vscode.window.showWarningMessage( + 'Action blocked: Server is down and no cached smells exist for this file version.', + ); + smellsViewProvider.setStatus(smell.path, 'server_down'); + return; + } try { - // Set a context key to track that refactoring is in progress vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); - // Call the backend to refactor the smell - const refactoredData = await backendRefactorSmell(smell); + const refactoredData = await backendRefactorSmell(smell, workspacePath); - // Log the response from the backend ecoOutput.appendLine(`Refactoring response: ${JSON.stringify(refactoredData)}`); - // Update the refactoring details view with the target file, affected files, and energy saved refactoringDetailsViewProvider.updateRefactoringDetails( smell.symbol, refactoredData.targetFile, refactoredData.affectedFiles, - refactoredData.energySaved, // Pass the energy saved value + refactoredData.energySaved, ); - // Show a diff view for the target file await openDiffEditor( refactoredData.targetFile.original, refactoredData.targetFile.refactored, ); - // Focus on the Refactoring Details view await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); - // Notify the user vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); @@ -68,7 +94,6 @@ export async function refactorSmell( console.error('Refactoring failed:', error.message); vscode.window.showErrorMessage(`Refactoring failed: ${error.message}`); - // Reset the refactoring details view on failure refactoringDetailsViewProvider.resetRefactoringDetails(); vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } diff --git a/src/extension.ts b/src/extension.ts index 5f839a0..b626ac9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -139,7 +139,12 @@ export function activate(context: vscode.ExtensionContext): void { vscode.window.showErrorMessage('No smell found for this item.'); return; } - refactorSmell(smellsViewProvider, refactoringDetailsViewProvider, smell); + refactorSmell( + smellsViewProvider, + refactoringDetailsViewProvider, + smell, + context, + ); }, ), diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 03058c8..a06b7c8 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -60,7 +60,7 @@ export class RefactoringDetailsViewProvider 'Target File', // Description targetFile.original, targetFile.refactored, - true, // This is a parent item (collapsible) + affectedFiles.length > 0, // This is a parent item (collapsible) ), ); From 92fc62ab8ee384458618eaac6795c2222ad17aba Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 07:45:41 -0400 Subject: [PATCH 065/121] added back metrics --- package.json | 59 +++++++++++++++++--------- src/commands/refactorSmell.ts | 5 +++ src/commands/rejectRefactoring.ts | 9 ++-- src/extension.ts | 68 ++++++++++++++++++++++++++++++ src/utils/openDiffEditor.ts | 36 ++++++++++++---- src/utils/refactorActionButtons.ts | 46 ++++++++++++++++++++ 6 files changed, 192 insertions(+), 31 deletions(-) create mode 100644 src/utils/refactorActionButtons.ts diff --git a/package.json b/package.json index ff8c333..da577d6 100644 --- a/package.json +++ b/package.json @@ -239,66 +239,87 @@ "title": "Reject Refactoring", "category": "Eco", "icon": "$(close)" + }, + { + "command": "ecooptimizer.exportMetricsData", + "title": "Export Metrics Data as JSON", + "category": "Eco" + }, + { + "command": "ecooptimizer.clearMetricsData", + "title": "Clear Metrics Data", + "category": "Eco" + }, + { + "command": "ecooptimizer.metricsView.refresh", + "title": "Refresh Metrics Data", + "icon": "$(sync)", + "category": "Eco" } ], "menus": { "view/title": [ { "command": "ecooptimizer.resetConfiguration", - "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured", + "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured && !refactoringInProgress", "group": "resource" }, { "command": "ecooptimizer.wipeWorkCache", - "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured", + "when": "view == ecooptimizer.smellsView && workspaceState.workspaceConfigured && !refactoringInProgress", "group": "resource" }, { "command": "ecooptimizer.selectAllFilterSmells", - "when": "view == ecooptimizer.filterView", + "when": "view == ecooptimizer.filterView && !refactoringInProgress", "group": "resource" }, { "command": "ecooptimizer.deselectAllFilterSmells", - "when": "view == ecooptimizer.filterView", + "when": "view == ecooptimizer.filterView && !refactoringInProgress", + "group": "resource" + }, + { + "command": "ecooptimizer.exportMetricsData", + "when": "view == ecooptimizer.metricsView", "group": "resource" + }, + { + "command": "ecooptimizer.clearMetricsData", + "when": "view == ecooptimizer.metricsView", + "group": "resource" + }, + { + "command": "ecooptimizer.metricsView.refresh", + "when": "view == ecooptimizer.metricsView", + "group": "navigation" } ], "view/item/context": [ { "command": "ecooptimizer.editSmellFilterOption", - "when": "viewItem == smellOption", + "when": "viewItem == smellOption && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.detectSmellsFolder", - "when": "view == ecooptimizer.smellsView && viewItem == directory", + "when": "view == ecooptimizer.smellsView && viewItem == directory && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.detectSmellsFile", - "when": "view == ecooptimizer.smellsView && (viewItem == file || viewItem == file_with_smells)", + "when": "view == ecooptimizer.smellsView && (viewItem == file || viewItem == file_with_smells) && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.refactorAllSmellsOfType", - "when": "view == ecooptimizer.smellsView && viewItem == file_with_smells", + "when": "view == ecooptimizer.smellsView && viewItem == file_with_smells && !refactoringInProgress", "group": "inline" }, { "command": "ecooptimizer.refactorSmell", - "when": "view == ecooptimizer.smellsView && viewItem == smell", + "when": "view == ecooptimizer.smellsView && viewItem == smell && !refactoringInProgress", "group": "inline" - }, - { - "command": "ecooptimizer.acceptRefactoring", - "when": "view == ecooptimizer.refactorView && refactoringInProgress", - "group": "navigation" - }, - { - "command": "ecooptimizer.rejectRefactoring", - "when": "view == ecooptimizer.refactorView && refactoringInProgress", - "group": "navigation" } ] }, diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 5ce356e..2e3aa81 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -8,6 +8,7 @@ import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsV import { ecoOutput } from '../extension'; import { openDiffEditor } from '../utils/openDiffEditor'; import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; +import { showRefactorActionButtons } from '../utils/refactorActionButtons'; // ← add this at the top /** * Recursively collects all `.py` files in the given directory. @@ -66,6 +67,8 @@ export async function refactorSmell( return; } + smellsViewProvider.setStatus(smell.path, 'queue'); + try { vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); @@ -87,6 +90,8 @@ export async function refactorSmell( await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); + showRefactorActionButtons(context); + vscode.window.showInformationMessage( `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); diff --git a/src/commands/rejectRefactoring.ts b/src/commands/rejectRefactoring.ts index f7a4369..ab48bb1 100644 --- a/src/commands/rejectRefactoring.ts +++ b/src/commands/rejectRefactoring.ts @@ -1,12 +1,15 @@ import * as vscode from 'vscode'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; - +import { closeAllRefactorDiffEditors } from '../utils/openDiffEditor'; +import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; export async function rejectRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, + context: vscode.ExtensionContext, ): Promise { vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); + // Clear state + UI refactoringDetailsViewProvider.resetRefactoringDetails(); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + await closeAllRefactorDiffEditors(); + hideRefactorActionButtons(context); } diff --git a/src/extension.ts b/src/extension.ts index b626ac9..a0ec254 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,8 +27,42 @@ import { refactorSmell } from './commands/refactorSmell'; // === Listeners & UI === import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { LineSelectionManager } from './ui/LineSelection'; +import { envConfig } from './utils/envConfig'; +import { exportMetricsData } from './commands/exportMetricsData'; export function activate(context: vscode.ExtensionContext): void { + const acceptRefactoringItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 100, + ); + acceptRefactoringItem.text = '$(check) Accept Refactoring'; + acceptRefactoringItem.command = 'ecooptimizer.acceptRefactoring'; + acceptRefactoringItem.tooltip = 'Accept and apply the suggested refactoring'; + acceptRefactoringItem.hide(); // Hidden by default + + const rejectRefactoringItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 99, + ); + rejectRefactoringItem.text = '$(x) Reject Refactoring'; + rejectRefactoringItem.command = 'ecooptimizer.rejectRefactoring'; + rejectRefactoringItem.tooltip = 'Reject the suggested refactoring'; + rejectRefactoringItem.hide(); // Hidden by default + + context.subscriptions.push(acceptRefactoringItem, rejectRefactoringItem); + + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); // initially + + vscode.commands.registerCommand('ecooptimizer.showRefactorStatusBar', () => { + acceptRefactoringItem.show(); + rejectRefactoringItem.show(); + }); + + vscode.commands.registerCommand('ecooptimizer.hideRefactorStatusBar', () => { + acceptRefactoringItem.hide(); + rejectRefactoringItem.hide(); + }); + // Load smell definitions loadSmells(); @@ -176,6 +210,40 @@ export function activate(context: vscode.ExtensionContext): void { lineSelectManager.commentLine(event.textEditor); }), ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.exportMetricsData', () => + exportMetricsData(context), + ), + ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.metricsView.refresh', () => { + metricsViewProvider.refresh(); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.clearMetricsData', () => { + vscode.window + .showWarningMessage( + 'Are you sure you want to clear the metrics data? This action is irreversible, and the data will be permanently lost unless exported.', + { modal: true }, + 'Yes', + 'No', + ) + .then((selection) => { + if (selection === 'Yes') { + context.workspaceState.update( + envConfig.WORKSPACE_METRICS_DATA!, + undefined, + ); + vscode.window.showInformationMessage('Metrics data has been cleared.'); + } + }); + metricsViewProvider.refresh(); + }), + ); } export function deactivate(): void { diff --git a/src/utils/openDiffEditor.ts b/src/utils/openDiffEditor.ts index f31eb26..f73f424 100644 --- a/src/utils/openDiffEditor.ts +++ b/src/utils/openDiffEditor.ts @@ -1,12 +1,8 @@ import * as vscode from 'vscode'; import * as path from 'path'; -/** - * Opens a VS Code diff editor comparing the original and refactored file. - * - * @param originalFilePath - Path to the original file. - * @param refactoredFilePath - Path to the refactored version of the file. - */ +const openedRefactorDiffs: [vscode.Uri, vscode.Uri][] = []; + export async function openDiffEditor( originalFilePath: string, refactoredFilePath: string, @@ -15,13 +11,35 @@ export async function openDiffEditor( const originalUri = vscode.Uri.file(originalFilePath); const refactoredUri = vscode.Uri.file(refactoredFilePath); + // Store this diff pair for later cleanup + openedRefactorDiffs.push([originalUri, refactoredUri]); + await vscode.commands.executeCommand( 'vscode.diff', originalUri, refactoredUri, `Refactoring Comparison (${fileName})`, - { - preview: false, - }, + { preview: false }, ); } + +// Utility to close all tracked diff editors +export async function closeAllRefactorDiffEditors(): Promise { + const visibleEditors = vscode.window.visibleTextEditors; + + for (const editor of visibleEditors) { + const uri = editor.document.uri; + const isRefactorDiff = openedRefactorDiffs.some( + ([original, refactored]) => + uri.toString() === original.toString() || + uri.toString() === refactored.toString(), + ); + + if (isRefactorDiff) { + await vscode.window.showTextDocument(uri, { preview: false }); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + } + } + + openedRefactorDiffs.length = 0; // clear tracked diffs +} diff --git a/src/utils/refactorActionButtons.ts b/src/utils/refactorActionButtons.ts new file mode 100644 index 0000000..21630b7 --- /dev/null +++ b/src/utils/refactorActionButtons.ts @@ -0,0 +1,46 @@ +import * as vscode from 'vscode'; + +export function showRefactorActionButtons(context: vscode.ExtensionContext) { + const acceptButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 0, + ); + acceptButton.text = '$(check) Accept Refactoring'; + acceptButton.command = 'ecooptimizer.acceptRefactoring'; + acceptButton.color = 'lightgreen'; + acceptButton.tooltip = 'Apply the suggested refactoring'; + + const rejectButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 1, + ); + rejectButton.text = '$(x) Reject Refactoring'; + rejectButton.command = 'ecooptimizer.rejectRefactoring'; + rejectButton.color = 'red'; + rejectButton.tooltip = 'Discard the suggested refactoring'; + + context.subscriptions.push(acceptButton, rejectButton); + + // Show them only when refactoring is active + vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); + + acceptButton.show(); + rejectButton.show(); +} + +/** + * Hides the refactor action buttons from the status bar. + */ +export function hideRefactorActionButtons(context: vscode.ExtensionContext) { + const acceptButton = context.workspaceState.get( + 'ecooptimizer.refactorAcceptButton', + ); + const rejectButton = context.workspaceState.get( + 'ecooptimizer.refactorRejectButton', + ); + + acceptButton?.hide(); + rejectButton?.hide(); + + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); +} From 005e951d4831b42f78ba28228ca575df2feb8d3b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 08:43:34 -0400 Subject: [PATCH 066/121] added refactoring --- data/smells.json | 10 +- src/commands/acceptRefactoring.ts | 8 +- src/commands/refactorSmell.ts | 4 +- src/extension.ts | 24 ++++- .../RefactoringDetailsViewProvider.ts | 97 +++++++++++++------ src/utils/smellsData.ts | 25 +++++ 6 files changed, 130 insertions(+), 38 deletions(-) diff --git a/data/smells.json b/data/smells.json index df7c5bc..4c7e3df 100644 --- a/data/smells.json +++ b/data/smells.json @@ -4,6 +4,7 @@ "name": "Use A Generator (UGEN)", "acronym": "UGEN", "enabled": true, + "smell_description": "Using generators instead of lists reduces memory consumption and avoids unnecessary allocations, leading to more efficient CPU and energy use.", "analyzer_options": {} }, "too-many-arguments": { @@ -11,6 +12,7 @@ "name": "Too Many Arguments (LPL)", "acronym": "LPL", "enabled": true, + "smell_description": "Functions with many arguments are harder to optimize and often require more memory and call overhead, increasing CPU load and energy usage.", "analyzer_options": { "max_args": { "label": "Number of Arguments", @@ -24,6 +26,7 @@ "name": "No Self Use (NSU)", "acronym": "NSU", "enabled": true, + "smell_description": "Methods that don't use 'self' can be static, reducing object overhead and avoiding unnecessary memory binding at runtime.", "analyzer_options": {} }, "long-lambda-expression": { @@ -31,6 +34,7 @@ "name": "Long Lambda Expression (LLE)", "acronym": "LLE", "enabled": true, + "smell_description": "Complex lambdas are harder for the interpreter to optimize and may lead to repeated evaluations, which can increase CPU usage and energy draw.", "analyzer_options": { "threshold_length": { "label": "Lambda Length", @@ -49,6 +53,7 @@ "name": "Long Message Chain (LMC)", "acronym": "LMC", "enabled": true, + "smell_description": "Deeply nested calls create performance bottlenecks due to increased dereferencing and lookup time, which adds to CPU cycles and energy usage.", "analyzer_options": { "threshold": { "label": "Threshold", @@ -62,6 +67,7 @@ "name": "Long Element Chain (LEC)", "acronym": "LEC", "enabled": true, + "smell_description": "Chained element access can be inefficient in large structures, increasing access time and CPU effort, thereby consuming more energy.", "analyzer_options": { "threshold": { "label": "Threshold", @@ -75,6 +81,7 @@ "name": "Cached Repeated Calls (CRC)", "acronym": "CRC", "enabled": true, + "smell_description": "Failing to cache repeated expensive calls leads to redundant computation, which wastes CPU cycles and drains energy needlessly.", "analyzer_options": { "threshold": { "label": "Cache Threshold", @@ -88,6 +95,7 @@ "name": "String Concatenation in Loops (SCL)", "acronym": "SCL", "enabled": true, + "smell_description": "String concatenation in loops creates new objects each time, increasing memory churn and CPU workload, which leads to higher energy consumption.", "analyzer_options": {} } -} \ No newline at end of file +} diff --git a/src/commands/acceptRefactoring.ts b/src/commands/acceptRefactoring.ts index 332ecae..bc4ce1f 100644 --- a/src/commands/acceptRefactoring.ts +++ b/src/commands/acceptRefactoring.ts @@ -5,6 +5,7 @@ import { MetricsViewProvider } from '../providers/MetricsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { ecoOutput } from '../extension'; +import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; function normalizePath(filePath: string): string { return filePath.toLowerCase(); @@ -15,6 +16,7 @@ export async function acceptRefactoring( metricsDataProvider: MetricsViewProvider, smellsCacheManager: SmellsCacheManager, smellsViewProvider: SmellsViewProvider, + context: vscode.ExtensionContext, ): Promise { const targetFile = refactoringDetailsViewProvider.targetFile; const affectedFiles = refactoringDetailsViewProvider.affectedFiles; @@ -31,7 +33,7 @@ export async function acceptRefactoring( } const energySaved = refactoringDetailsViewProvider.energySaved; - const targetSmell = refactoringDetailsViewProvider.targetSmell; + const targetSmell = refactoringDetailsViewProvider.targetSmell?.symbol; const file = vscode.Uri.file(targetFile.original).fsPath; if (energySaved && targetSmell) { @@ -57,7 +59,9 @@ export async function acceptRefactoring( refactoringDetailsViewProvider.resetRefactoringDetails(); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + + hideRefactorActionButtons(context); + smellsViewProvider.refresh(); } catch (error) { console.error('Failed to accept refactoring:', error); diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 2e3aa81..3a8b2f3 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -67,7 +67,7 @@ export async function refactorSmell( return; } - smellsViewProvider.setStatus(smell.path, 'queue'); + smellsViewProvider.setStatus(smell.path, 'queued'); try { vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); @@ -77,7 +77,7 @@ export async function refactorSmell( ecoOutput.appendLine(`Refactoring response: ${JSON.stringify(refactoredData)}`); refactoringDetailsViewProvider.updateRefactoringDetails( - smell.symbol, + smell, refactoredData.targetFile, refactoredData.affectedFiles, refactoredData.energySaved, diff --git a/src/extension.ts b/src/extension.ts index a0ec254..b08fc43 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,6 +29,8 @@ import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener import { LineSelectionManager } from './ui/LineSelection'; import { envConfig } from './utils/envConfig'; import { exportMetricsData } from './commands/exportMetricsData'; +import { acceptRefactoring } from './commands/acceptRefactoring'; +import { rejectRefactoring } from './commands/rejectRefactoring'; export function activate(context: vscode.ExtensionContext): void { const acceptRefactoringItem = vscode.window.createStatusBarItem( @@ -211,11 +213,23 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.exportMetricsData', () => - exportMetricsData(context), - ), - ); + vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', async () => { + await acceptRefactoring( + refactoringDetailsViewProvider, + metricsViewProvider, + smellsCacheManager, + smellsViewProvider, + context, + ); + }), + vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', async () => { + await rejectRefactoring(refactoringDetailsViewProvider, context); + }), + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.exportMetricsData', () => + exportMetricsData(context), + ), + ); context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.metricsView.refresh', () => { diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index a06b7c8..e477990 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import * as path from 'path'; +import { getDescriptionByMessageId, getNameByMessageId } from '../utils/smellsData'; export class RefactoringDetailsViewProvider implements vscode.TreeDataProvider @@ -12,8 +13,8 @@ export class RefactoringDetailsViewProvider private refactoringDetails: RefactoringDetailItem[] = []; public targetFile: { original: string; refactored: string } | undefined; public affectedFiles: { original: string; refactored: string }[] = []; - public energySaved: number | undefined; // Add energySaved as a class property - public targetSmell: string | undefined; + public energySaved: number | undefined; + public targetSmell: Smell | undefined; constructor() { this.resetRefactoringDetails(); @@ -21,12 +22,13 @@ export class RefactoringDetailsViewProvider /** * Updates the refactoring details with the given target file, affected files, and energy saved. + * @param targetSmell - The smell being refactored. * @param targetFile - The target file (original and refactored paths). * @param affectedFiles - The list of affected files (original and refactored paths). * @param energySaved - The amount of energy saved in kg CO2. */ updateRefactoringDetails( - targetSmell: string, + targetSmell: Smell, targetFile: { original: string; refactored: string }, affectedFiles: { original: string; refactored: string }[], energySaved: number | undefined, @@ -39,16 +41,33 @@ export class RefactoringDetailsViewProvider // Clear the existing refactoring details this.refactoringDetails = []; - // Add energy saved as the first item + // Add the smell being refactored as the first item + if (targetSmell) { + const smellName = + getNameByMessageId(targetSmell.messageId) || targetSmell.messageId; + this.refactoringDetails.push( + new RefactoringDetailItem( + `Refactoring: ${smellName}`, + '', // Empty description since we'll show it as a child + '', + '', + true, // Make it collapsible to show description as child + false, + true, // isSmellItem + ), + ); + } + + // Add energy saved as the second item if (energySaved) { this.refactoringDetails.push( new RefactoringDetailItem( - `Energy Saved: ${energySaved} kg CO2`, // Label - '', // No description - '', // No file path - '', // No file path - false, // Not collapsible - true, // Special item for energy saved + `Energy Saved: ${energySaved} kg CO2`, + '', + '', + '', + false, + true, // isEnergySaved ), ); } @@ -56,18 +75,15 @@ export class RefactoringDetailsViewProvider // Add the target file this.refactoringDetails.push( new RefactoringDetailItem( - path.basename(targetFile.original), // File name as label - 'Target File', // Description + path.basename(targetFile.original), + 'Target File', targetFile.original, targetFile.refactored, - affectedFiles.length > 0, // This is a parent item (collapsible) + affectedFiles.length > 0, ), ); - // Do not add affected files to refactoringDetails here - // They will be added dynamically in getChildren when the parent item is expanded - - this._onDidChangeTreeData.fire(undefined); // Refresh the view + this._onDidChangeTreeData.fire(undefined); } /** @@ -79,7 +95,7 @@ export class RefactoringDetailsViewProvider this.targetSmell = undefined; this.energySaved = undefined; this.refactoringDetails = []; - this._onDidChangeTreeData.fire(undefined); // Refresh the view + this._onDidChangeTreeData.fire(undefined); } getTreeItem(element: RefactoringDetailItem): vscode.TreeItem { @@ -88,22 +104,40 @@ export class RefactoringDetailsViewProvider getChildren(element?: RefactoringDetailItem): RefactoringDetailItem[] { if (element) { + // If this is the smell parent item, return the description as child + if (element.isSmellItem && this.targetSmell) { + const smellDescription = + getDescriptionByMessageId(this.targetSmell.messageId) || + this.targetSmell.messageId; + return [ + new RefactoringDetailItem( + '', + smellDescription, + '', + '', + false, // Not collapsible + false, + false, + 'info', // New parameter for description type + ), + ]; + } + // If this is the parent item (Target File), return the affected files as children if (element.isParent) { return this.affectedFiles.map( (file) => new RefactoringDetailItem( - path.basename(file.original), // File name as label - 'Affected File', // Description + path.basename(file.original), + 'Affected File', file.original, file.refactored, - false, // This is a child item (not collapsible) + false, ), ); } - return []; // No nested items for child items + return []; } - // If no element is provided, return the top-level items (Energy Saved and Target File) return this.refactoringDetails; } } @@ -116,26 +150,33 @@ class RefactoringDetailItem extends vscode.TreeItem { public readonly refactoredFilePath: string, public readonly isParent: boolean = false, public readonly isEnergySaved: boolean = false, + public readonly isSmellItem: boolean = false, + public readonly itemType?: 'info' | 'file', // New parameter to distinguish types ) { super( label, - isParent + isParent || isSmellItem // Make smell items collapsible too ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, ); this.description = description; - // Customize the icon for the Energy Saved item + // Custom icons based on type if (isEnergySaved) { this.iconPath = new vscode.ThemeIcon( - 'lightbulb', // Use a lightbulb icon for energy saved + 'lightbulb', new vscode.ThemeColor('charts.yellow'), ); this.tooltip = 'This is the amount of energy saved by refactoring.'; + } else if (isSmellItem) { + this.iconPath = new vscode.ThemeIcon( + 'symbol-class', + new vscode.ThemeColor('charts.'), + ); } - // Add a command to open the diff editor for file items (not energy saved) - if (!isEnergySaved) { + // Add commands where appropriate + if (!isEnergySaved && !isSmellItem && itemType !== 'info' && originalFilePath) { this.command = { command: 'ecooptimizer.openDiffEditor', title: 'Open Diff Editor', diff --git a/src/utils/smellsData.ts b/src/utils/smellsData.ts index 251399b..8621060 100644 --- a/src/utils/smellsData.ts +++ b/src/utils/smellsData.ts @@ -11,6 +11,7 @@ export interface FilterSmellConfig { name: string; message_id: string; acronym: string; + smell_description: string; enabled: boolean; analyzer_options?: Record< string, @@ -133,3 +134,27 @@ export function getAcronymByMessageId(messageId: string): string | undefined { ); return match?.acronym; } + +/** + * Returns the full name for a given message ID. + * @param messageId - The message ID to look up (e.g., "R0913"). + * @returns The full name (e.g., "Long Parameter List") or undefined if not found. + */ +export function getNameByMessageId(messageId: string): string | undefined { + const match = Object.values(filterSmells).find( + (smell) => smell.message_id === messageId, + ); + return match?.name; +} + +/** + * Returns the description for a given message ID. + * @param messageId - The message ID to look up (e.g., "R0913"). + * @returns The description or undefined if not found. + */ +export function getDescriptionByMessageId(messageId: string): string | undefined { + const match = Object.values(filterSmells).find( + (smell) => smell.message_id === messageId, + ); + return match?.smell_description; // This assumes your FilterSmellConfig has a description field +} From 21c3a98aee531f8ba8cd0a63f2d328a4bf38cfcf Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 08:58:01 -0400 Subject: [PATCH 067/121] clean --- src/extension.ts | 107 +++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index b08fc43..f361792 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,12 +1,14 @@ import * as vscode from 'vscode'; -// Output channel for logging +// === Output Channel === export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); // === Core Utilities === import { loadSmells } from './utils/smellsData'; import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; import { openDiffEditor } from './utils/openDiffEditor'; +import { envConfig } from './utils/envConfig'; +import { checkServerStatus } from './api/backend'; // === Context & View Providers === import { SmellsCacheManager } from './context/SmellsCacheManager'; @@ -23,16 +25,16 @@ import { registerFilterSmellCommands } from './commands/filterSmells'; import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; import { refactorSmell } from './commands/refactorSmell'; +import { acceptRefactoring } from './commands/acceptRefactoring'; +import { rejectRefactoring } from './commands/rejectRefactoring'; +import { exportMetricsData } from './commands/exportMetricsData'; // === Listeners & UI === import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { LineSelectionManager } from './ui/LineSelection'; -import { envConfig } from './utils/envConfig'; -import { exportMetricsData } from './commands/exportMetricsData'; -import { acceptRefactoring } from './commands/acceptRefactoring'; -import { rejectRefactoring } from './commands/rejectRefactoring'; export function activate(context: vscode.ExtensionContext): void { + // === Status Bar Buttons for Refactoring === const acceptRefactoringItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, 100, @@ -40,7 +42,7 @@ export function activate(context: vscode.ExtensionContext): void { acceptRefactoringItem.text = '$(check) Accept Refactoring'; acceptRefactoringItem.command = 'ecooptimizer.acceptRefactoring'; acceptRefactoringItem.tooltip = 'Accept and apply the suggested refactoring'; - acceptRefactoringItem.hide(); // Hidden by default + acceptRefactoringItem.hide(); const rejectRefactoringItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, @@ -49,11 +51,11 @@ export function activate(context: vscode.ExtensionContext): void { rejectRefactoringItem.text = '$(x) Reject Refactoring'; rejectRefactoringItem.command = 'ecooptimizer.rejectRefactoring'; rejectRefactoringItem.tooltip = 'Reject the suggested refactoring'; - rejectRefactoringItem.hide(); // Hidden by default + rejectRefactoringItem.hide(); context.subscriptions.push(acceptRefactoringItem, rejectRefactoringItem); - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); // initially + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); vscode.commands.registerCommand('ecooptimizer.showRefactorStatusBar', () => { acceptRefactoringItem.show(); @@ -65,9 +67,13 @@ export function activate(context: vscode.ExtensionContext): void { rejectRefactoringItem.hide(); }); - // Load smell definitions + // === Load Core Data === loadSmells(); + // === Start periodic backend status checks === + checkServerStatus(); + setInterval(checkServerStatus, 10000); + // === Initialize Managers & Providers === const smellsCacheManager = new SmellsCacheManager(context); const smellsViewProvider = new SmellsViewProvider(context); @@ -80,7 +86,6 @@ export function activate(context: vscode.ExtensionContext): void { ); const refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); - // Restore cached statuses initializeStatusesFromCache(context, smellsCacheManager, smellsViewProvider); // === Register Tree Views === @@ -101,7 +106,6 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // Connect checkbox UI logic filterSmellsProvider.setTreeView( vscode.window.createTreeView('ecooptimizer.filterView', { treeDataProvider: filterSmellsProvider, @@ -109,7 +113,6 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - // Set workspace configuration context const workspaceConfigured = Boolean( context.workspaceState.get('workspaceConfiguredPath'), ); @@ -184,60 +187,35 @@ export function activate(context: vscode.ExtensionContext): void { }, ), + vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', async () => { + await acceptRefactoring( + refactoringDetailsViewProvider, + metricsViewProvider, + smellsCacheManager, + smellsViewProvider, + context, + ); + }), + + vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', async () => { + await rejectRefactoring(refactoringDetailsViewProvider, context); + }), + vscode.commands.registerCommand( 'ecooptimizer.openDiffEditor', (originalFilePath: string, refactoredFilePath: string) => { openDiffEditor(originalFilePath, refactoredFilePath); }, ), - ); - - // Register filter-related commands - registerFilterSmellCommands(context, filterSmellsProvider); - - // === Watch for workspace changes === - context.subscriptions.push( - new WorkspaceModifiedListener( - context, - smellsCacheManager, - smellsViewProvider, - metricsViewProvider, - ), - ); - - // === Register Line Selection Listener === - const lineSelectManager = new LineSelectionManager(smellsCacheManager); - context.subscriptions.push( - vscode.window.onDidChangeTextEditorSelection((event) => { - lineSelectManager.commentLine(event.textEditor); - }), - ); - vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', async () => { - await acceptRefactoring( - refactoringDetailsViewProvider, - metricsViewProvider, - smellsCacheManager, - smellsViewProvider, - context, - ); - }), - vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', async () => { - await rejectRefactoring(refactoringDetailsViewProvider, context); + vscode.commands.registerCommand('ecooptimizer.exportMetricsData', () => { + exportMetricsData(context); }), - context.subscriptions.push( - vscode.commands.registerCommand('ecooptimizer.exportMetricsData', () => - exportMetricsData(context), - ), - ); - context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.metricsView.refresh', () => { metricsViewProvider.refresh(); }), - ); - context.subscriptions.push( vscode.commands.registerCommand('ecooptimizer.clearMetricsData', () => { vscode.window .showWarningMessage( @@ -254,8 +232,29 @@ export function activate(context: vscode.ExtensionContext): void { ); vscode.window.showInformationMessage('Metrics data has been cleared.'); } + metricsViewProvider.refresh(); }); - metricsViewProvider.refresh(); + }), + ); + + // === Register Filter UI Commands === + registerFilterSmellCommands(context, filterSmellsProvider); + + // === Workspace File Listener === + context.subscriptions.push( + new WorkspaceModifiedListener( + context, + smellsCacheManager, + smellsViewProvider, + metricsViewProvider, + ), + ); + + // === Line Selection === + const lineSelectManager = new LineSelectionManager(smellsCacheManager); + context.subscriptions.push( + vscode.window.onDidChangeTextEditorSelection((event) => { + lineSelectManager.commentLine(event.textEditor); }), ); } From 3ba20c2d44c4dd0aa7fd7d43a0b7e080d21b3d35 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 09:02:22 -0400 Subject: [PATCH 068/121] clean --- src/extension.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index f361792..51d941d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -34,6 +34,8 @@ import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener import { LineSelectionManager } from './ui/LineSelection'; export function activate(context: vscode.ExtensionContext): void { + ecoOutput.appendLine('Initializing Eco-Optimizer extension...'); + // === Status Bar Buttons for Refactoring === const acceptRefactoringItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, @@ -138,7 +140,7 @@ export function activate(context: vscode.ExtensionContext): void { smellsViewProvider.refresh(); metricsViewProvider.refresh(); vscode.window.showInformationMessage( - 'Workspace configuration has been reset. All analysis data has been cleared.', + 'Workspace configuration and analysis data have been reset.', ); } }), @@ -152,7 +154,7 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileItem) => { const filePath = fileItem?.resourceUri?.fsPath; if (!filePath) { - vscode.window.showWarningMessage('No file selected or file path not found.'); + vscode.window.showWarningMessage('Please select a file to analyze.'); return; } detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); @@ -163,7 +165,7 @@ export function activate(context: vscode.ExtensionContext): void { (folderItem) => { const folderPath = folderItem?.resourceUri?.fsPath; if (!folderPath) { - vscode.window.showWarningMessage('No folder selected.'); + vscode.window.showWarningMessage('Please select a folder to analyze.'); return; } detectSmellsFolder(folderPath, smellsViewProvider, smellsCacheManager); @@ -175,7 +177,7 @@ export function activate(context: vscode.ExtensionContext): void { (item: SmellTreeItem) => { const smell = item?.smell; if (!smell) { - vscode.window.showErrorMessage('No smell found for this item.'); + vscode.window.showErrorMessage('No code smell detected for this item.'); return; } refactorSmell( @@ -219,20 +221,20 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('ecooptimizer.clearMetricsData', () => { vscode.window .showWarningMessage( - 'Are you sure you want to clear the metrics data? This action is irreversible, and the data will be permanently lost unless exported.', + 'Clear all metrics data? This cannot be undone unless you have exported it.', { modal: true }, - 'Yes', - 'No', + 'Clear', + 'Cancel', ) .then((selection) => { - if (selection === 'Yes') { + if (selection === 'Clear') { context.workspaceState.update( envConfig.WORKSPACE_METRICS_DATA!, undefined, ); - vscode.window.showInformationMessage('Metrics data has been cleared.'); + metricsViewProvider.refresh(); + vscode.window.showInformationMessage('Metrics data cleared.'); } - metricsViewProvider.refresh(); }); }), ); @@ -257,8 +259,10 @@ export function activate(context: vscode.ExtensionContext): void { lineSelectManager.commentLine(event.textEditor); }), ); + + ecoOutput.appendLine('Eco-Optimizer extension activated successfully'); } export function deactivate(): void { - ecoOutput.appendLine('Deactivating Eco-Optimizer extension...'); + ecoOutput.appendLine('Extension deactivated'); } From d5c66b0188a2ba3fa4c3c3fb5231ac1b0940987e Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 09:04:50 -0400 Subject: [PATCH 069/121] clean --- src/global.d.ts | 77 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/global.d.ts b/src/global.d.ts index 10faf27..7b026dd 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,53 +1,94 @@ export {}; -// global.d.ts -export {}; - +/** + * Global type declarations for the Eco-Optimizer extension. + * These interfaces define the core data structures used throughout the application. + */ declare global { - // Define your global types here + /** + * Represents a specific location in source code where a smell occurs. + * Uses VS Code-style line/column numbering (1-based). + */ export interface Occurrence { + /** The starting line number (1-based) */ line: number; + /** The ending line number (1-based, optional) */ endLine?: number; + /** The starting column number (1-based) */ column: number; + /** The ending column number (1-based, optional) */ endColumn?: number; } + /** + * Additional context-specific information about a code smell. + * The fields vary depending on the smell type. + */ export interface AdditionalInfo { - // CRC + // Fields for Cached Repeated Calls (CRC) smell: + /** Number of repetitions found (for CRC smells) */ repetitions?: number; + /** The call string that's being repeated (for CRC smells) */ callString?: string; - // SCL + + // Fields for String Concatenation in Loop (SCL) smell: + /** The target variable being concatenated (for SCL smells) */ concatTarget?: string; + /** The line number where the inner loop occurs (for SCL smells) */ innerLoopLine?: number; } + /** + * Represents a detected code smell with all its metadata. + * This is the core data structure for analysis results. + */ export interface Smell { - type: string; // Type of the smell (e.g., "performance", "convention") - symbol: string; // Symbolic identifier for the smell (e.g., "cached-repeated-calls") - message: string; // Detailed description of the smell - messageId: string; // Unique ID for the smell - confidence: string; // Confidence level (e.g., "HIGH", "MEDIUM") - path: string; // Optional: absolute file path - module: string; // Optional: Module name - obj?: string; // Optional: Object name associated with the smell (if applicable) - occurences: Occurrence[]; // Optional: List of occurrences for repeated calls + /** Category of the smell (e.g., "performance", "convention") */ + type: string; + /** Unique identifier for the smell type (e.g., "cached-repeated-calls") */ + symbol: string; + /** Human-readable description of the smell */ + message: string; + /** Unique message ID for specific smell variations */ + messageId: string; + /** Confidence level in detection ("HIGH", "MEDIUM", "LOW") */ + confidence: string; + /** Absolute path to the file containing the smell */ + path: string; + /** Module or namespace where the smell was found */ + module: string; + /** Specific object/function name (when applicable) */ + obj?: string; + /** All detected locations of this smell in the code */ + occurences: Occurrence[]; + /** Type-specific additional information about the smell */ additionalInfo: AdditionalInfo; - id?: string; // Add this line to include the unique ID + /** Unique identifier for this specific smell instance */ + id?: string; } /** - * Represents the response from the `/refactor` endpoint. + * Represents the response from the backend refactoring service. + * Contains all necessary information to present and apply refactorings. */ export interface RefactoredData { + /** Temporary directory containing all refactored files */ tempDir: string; + /** The main file that was refactored */ targetFile: { + /** Path to the original version */ original: string; + /** Path to the refactored version */ refactored: string; }; + /** Estimated energy savings in joules (optional) */ energySaved?: number; + /** Any additional files affected by the refactoring */ affectedFiles: { + /** Path to the original version */ original: string; + /** Path to the refactored version */ refactored: string; }[]; } -} +} \ No newline at end of file From 6f57800e7d646f4ae9d6bf3a7fae364411a8aa25 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 09:12:04 -0400 Subject: [PATCH 070/121] clean --- media/script.js | 122 ------------------------------ media/style.css | 42 ---------- media/vscode.css | 91 ---------------------- media/webview.html | 30 -------- src/utils/TreeStructureBuilder.ts | 65 +++++++++++++--- 5 files changed, 53 insertions(+), 297 deletions(-) delete mode 100644 media/script.js delete mode 100644 media/style.css delete mode 100644 media/vscode.css delete mode 100644 media/webview.html diff --git a/media/script.js b/media/script.js deleted file mode 100644 index ae26b65..0000000 --- a/media/script.js +++ /dev/null @@ -1,122 +0,0 @@ -const vscode = acquireVsCodeApi(); - -function updateWebView(data, sep) { - // Hide "No refactoring in progress" message - document.getElementById('no-data').style.display = 'none'; - document.getElementById('container').style.display = 'block'; - - // Update Energy Saved - document.getElementById( - 'energy' - ).textContent = `Carbon Saved: ${data.energySaved.toExponential(3)} kg CO2`; - - // Populate Target File - const targetFile = data.targetFile; - const targetFileList = document.getElementById('target-file-list'); - targetFileList.innerHTML = ''; - const li = document.createElement('li'); - - const relFile = findRelPath(targetFile.refactored, sep); - if (relFile.length === 0) { - relFile = targetFile.original; - } - li.textContent = relFile; - - li.classList.add('clickable'); - li.onclick = () => { - vscode.postMessage({ - command: 'selectFile', - original: targetFile.original, - refactored: targetFile.refactored - }); - }; - targetFileList.appendChild(li); - - // Populate Other Modified Files - const affectedFileList = document.getElementById('affected-file-list'); - affectedFileList.innerHTML = ''; - if (data.affectedFiles.length === 0) { - document.getElementById('other-files-head').style.display = 'none'; - } - data.affectedFiles.forEach((file) => { - const li = document.createElement('li'); - const relFile = findRelPath(file.refactored, sep); - - if (relFile.length === 0) { - relFile = file.original; - } - - li.textContent = relFile; - li.classList.add('clickable'); - li.onclick = () => { - vscode.postMessage({ - command: 'selectFile', - original: file.original, - refactored: file.refactored - }); - }; - affectedFileList.appendChild(li); - }); - - // Save state in the webview - vscode.setState(data); -} - -// Function to clear the UI (for when refactoring is done) -function clearWebview() { - document.getElementById('energy').textContent = 'Carbon Saved: --'; - document.getElementById('target-file-list').innerHTML = ''; - document.getElementById('affected-file-list').innerHTML = ''; - - document.getElementById('no-data').style.display = 'block'; - document.getElementById('container').style.display = 'none'; - vscode.setState(null); // Clear state -} - -// Restore state when webview loads -window.addEventListener('DOMContentLoaded', () => { - const savedState = vscode.getState(); - if (savedState) { - updateWebView(savedState); - } -}); - -// Listen for extension messages -window.addEventListener('message', (event) => { - if (event.data.command === 'update') { - updateWebView(event.data.data, event.data.sep); - } else if (event.data.command === 'clear') { - clearWebview(); - } else if (event.data.command === 'pause') { - document.getElementById('no-data').style.display = 'block'; - document.getElementById('container').style.display = 'none'; - } -}); - -// Button click handlers -document.getElementById('accept-btn').addEventListener('click', () => { - vscode.postMessage({ command: 'accept' }); - clearWebview(); -}); - -document.getElementById('reject-btn').addEventListener('click', () => { - vscode.postMessage({ command: 'reject' }); - clearWebview(); -}); - -function findRelPath(filePath, sep) { - // Split the path using the separator - const parts = filePath.split(sep); - - // Find the index of the part containing the 'ecooptimizer-' substring - const index = parts.findIndex((part) => part.includes('ecooptimizer-')); - - // If a matching part is found, return the joined list of items after it - if (index !== -1) { - // Slice the array from the next index and join them with the separator - return parts.slice(index + 1).join(sep); - } - - // Return an empty string if no match is found - return ''; -} diff --git a/media/style.css b/media/style.css deleted file mode 100644 index 2ce8d4e..0000000 --- a/media/style.css +++ /dev/null @@ -1,42 +0,0 @@ -body { - font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', - Arial, sans-serif; - padding: 10px; -} - -/* .body-text { - font-size: medium; -} */ -.clickable { - cursor: pointer; - padding: 5px; - transition: background-color 0.2s ease-in-out; -} -.clickable:hover { - background-color: rgba(87, 82, 82, 0.1); -} -#container { - display: none; /* Initially hidden until data is received */ -} -#no-data { - /* font-size: 14px; */ - text-align: center; -} -#buttons { - position: absolute; - display: flex; - justify-content: space-between; - margin: 0 5px; - bottom: 10px; -} -ul { - overflow-y: auto; - padding: 0; - list-style-type: square; -} - -button { - width: 45vw; - height: 40px; - border-radius: 2px; -} diff --git a/media/vscode.css b/media/vscode.css deleted file mode 100644 index 12d43b9..0000000 --- a/media/vscode.css +++ /dev/null @@ -1,91 +0,0 @@ -:root { - --container-paddding: 20px; - --input-padding-vertical: 6px; - --input-padding-horizontal: 4px; - --input-margin-vertical: 4px; - --input-margin-horizontal: 0; -} - -body { - padding: 0 var(--container-paddding); - color: var(--vscode-foreground); - font-size: var(--vscode-font-size); - font-weight: var(--vscode-font-weight); - font-family: var(--vscode-font-family); - background-color: var(--vscode-editor-background); -} - -ol, -ul { - padding-left: var(--container-paddding); -} - -body > *, -form > * { - margin-block-start: var(--input-margin-vertical); - margin-block-end: var(--input-margin-vertical); -} - -*:focus { - outline-color: var(--vscode-focusBorder) !important; -} - -a { - color: var(--vscode-textLink-foreground); -} - -a:hover, -a:active { - color: var(--vscode-textLink-activeForeground); -} - -code { - font-size: var(--vscode-editor-font-size); - font-family: var(--vscode-editor-font-family); -} - -button { - border: none; - padding: var(--input-padding-vertical) var(--input-padding-horizontal); - width: 100%; - text-align: center; - outline: 1px solid transparent; - outline-offset: 2px !important; - color: var(--vscode-button-foreground); - background: var(--vscode-button-background); -} - -button:hover { - cursor: pointer; - background: var(--vscode-button-hoverBackground); -} - -button:focus { - outline-color: var(--vscode-focusBorder); -} - -button.secondary { - color: var(--vscode-button-secondaryForeground); - background: var(--vscode-button-secondaryBackground); -} - -button.secondary:hover { - background: var(--vscode-button-secondaryHoverBackground); -} - -input:not([type='checkbox']), -textarea { - display: block; - width: 100%; - border: none; - font-family: var(--vscode-font-family); - padding: var(--input-padding-vertical) var(--input-padding-horizontal); - color: var(--vscode-input-foreground); - outline-color: var(--vscode-input-border); - background-color: var(--vscode-input-background); -} - -input::placeholder, -textarea::placeholder { - color: var(--vscode-input-placeholderForeground); -} diff --git a/media/webview.html b/media/webview.html deleted file mode 100644 index 4a1b2f6..0000000 --- a/media/webview.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - Refactoring Navigator - - - -

Nothing to see here. If you are currently refactoring a file, make sure the diff view is selected. -

-
-

Refactoring Summary

-

Carbon Saved: --

-

Target File

-
    -

    Other Modified Files

    -
      -
      - - -
      -
      - - - - \ No newline at end of file diff --git a/src/utils/TreeStructureBuilder.ts b/src/utils/TreeStructureBuilder.ts index 9206725..b6a8404 100644 --- a/src/utils/TreeStructureBuilder.ts +++ b/src/utils/TreeStructureBuilder.ts @@ -2,73 +2,114 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; +/** + * Options for configuring tree node appearance and behavior in the VS Code UI + */ export interface TreeNodeOptions { + /** Determines context menu commands and visibility rules */ contextValue: string; + /** Optional icon from VS Code's icon set */ icon?: vscode.ThemeIcon; + /** Tooltip text shown on hover */ tooltip?: string; + /** Command to execute when node is clicked */ command?: vscode.Command; } +/** + * Represents a node in the file system tree structure + */ export interface TreeNode { + /** Display name in the tree view */ label: string; + /** Absolute filesystem path */ fullPath: string; + /** Whether this represents a file (true) or directory (false) */ isFile: boolean; + /** Additional UI/behavior configuration */ options?: TreeNodeOptions; } /** - * Recursively builds a tree structure of Python files and valid folders. + * Builds a hierarchical tree structure of Python files and directories containing Python files + * @param rootPath - The absolute path to start building the tree from + * @returns Array of TreeNode objects representing the directory structure */ export function buildPythonTree(rootPath: string): TreeNode[] { const nodes: TreeNode[] = []; try { const entries = fs.readdirSync(rootPath); + for (const entry of entries) { const fullPath = path.join(rootPath, entry); const stat = fs.statSync(fullPath); - if (stat.isDirectory() && containsPythonFiles(fullPath)) { - nodes.push({ - label: entry, - fullPath, - isFile: false, - }); + if (stat.isDirectory()) { + // Only include directories that contain Python files + if (containsPythonFiles(fullPath)) { + nodes.push({ + label: entry, + fullPath, + isFile: false, + options: { + contextValue: 'folder', + icon: vscode.ThemeIcon.Folder, + }, + }); + } } else if (stat.isFile() && entry.endsWith('.py')) { nodes.push({ label: entry, fullPath, isFile: true, + options: { + contextValue: 'file', + icon: vscode.ThemeIcon.File, + }, }); } } } catch (err) { - console.error(`Error reading directory ${rootPath}:`, err); + vscode.window.showErrorMessage(`Failed to read directory: ${rootPath}`); + console.error(`Directory read error: ${err}`); } - return nodes; + return nodes.sort((a, b) => { + // Directories first, then alphabetical + if (!a.isFile && b.isFile) return -1; + if (a.isFile && !b.isFile) return 1; + return a.label.localeCompare(b.label); + }); } /** - * Checks if a folder (or its subfolders) contains any .py files. + * Recursively checks if a directory contains any Python files + * @param folderPath - Absolute path to the directory to check + * @returns True if any .py files exist in this directory or subdirectories */ function containsPythonFiles(folderPath: string): boolean { try { const entries = fs.readdirSync(folderPath); + for (const entry of entries) { const fullPath = path.join(folderPath, entry); const stat = fs.statSync(fullPath); if (stat.isFile() && entry.endsWith('.py')) { return true; - } else if (stat.isDirectory()) { + } + + if (stat.isDirectory()) { + // Short-circuit if any subdirectory contains Python files if (containsPythonFiles(fullPath)) { return true; } } } } catch (err) { - console.error(`Error checking folder ${folderPath}:`, err); + vscode.window.showErrorMessage(`Failed to scan directory: ${folderPath}`); + console.error(`Directory scan error: ${err}`); } return false; From da5d4d93da8c58ea4fa7e3dfaab91a6f6502563a Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 10:46:53 -0400 Subject: [PATCH 071/121] clean --- src/commands/refactorSmell.ts | 137 ++++++++++----- src/commands/rejectRefactoring.ts | 7 +- src/extension.ts | 21 ++- .../RefactoringDetailsViewProvider.ts | 157 ++++++++++-------- src/utils/openDiffEditor.ts | 45 ----- src/utils/trackedDiffEditors.ts | 32 ++++ 6 files changed, 236 insertions(+), 163 deletions(-) delete mode 100644 src/utils/openDiffEditor.ts create mode 100644 src/utils/trackedDiffEditors.ts diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 3a8b2f3..8432e91 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -6,35 +6,52 @@ import { backendRefactorSmell } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { ecoOutput } from '../extension'; -import { openDiffEditor } from '../utils/openDiffEditor'; import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; -import { showRefactorActionButtons } from '../utils/refactorActionButtons'; // ← add this at the top +import { showRefactorActionButtons } from '../utils/refactorActionButtons'; +import { registerDiffEditor } from '../utils/trackedDiffEditors'; /** - * Recursively collects all `.py` files in the given directory. + * Recursively collects all Python files (.py) in a directory and its subdirectories + * @param dir - The root directory path to search from + * @returns Array of absolute file paths to all Python files found */ function getAllPythonFiles(dir: string): string[] { - const result: string[] = []; - - const walk = (current: string) => { - const entries = fs.readdirSync(current); - for (const entry of entries) { - const fullPath = path.join(current, entry); - const stat = fs.statSync(fullPath); - if (stat.isDirectory()) { - walk(fullPath); - } else if (stat.isFile() && fullPath.endsWith('.py')) { - result.push(fullPath); + const pythonFiles: string[] = []; + + /** + * Recursive directory walker function + * @param currentDir - Current directory being processed + */ + const walkDirectory = (currentDir: string) => { + try { + const entries = fs.readdirSync(currentDir); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + walkDirectory(fullPath); + } else if (stat.isFile() && fullPath.endsWith('.py')) { + pythonFiles.push(fullPath); + } } + } catch (error) { + ecoOutput.appendLine(`Error scanning directory ${currentDir}: ${error}`); + console.error(`Directory scan error: ${error}`); } }; - walk(dir); - return result; + walkDirectory(dir); + return pythonFiles; } /** - * Handles the refactoring of a specific smell. + * Handles the complete refactoring workflow for a detected code smell + * @param smellsViewProvider - Reference to the smells view provider + * @param refactoringDetailsViewProvider - Reference to the refactoring details provider + * @param smell - The smell object to refactor + * @param context - VS Code extension context */ export async function refactorSmell( smellsViewProvider: SmellsViewProvider, @@ -42,40 +59,54 @@ export async function refactorSmell( smell: Smell, context: vscode.ExtensionContext, ): Promise { - vscode.window.showInformationMessage(`Refactoring code smell: ${smell.symbol}`); + // Notify user about refactoring start + ecoOutput.appendLine(`Starting refactoring for smell: ${smell.symbol}`); + vscode.window.showInformationMessage(`Refactoring ${smell.symbol} smell...`); + // Verify workspace configuration const workspacePath = context.workspaceState.get( 'workspaceConfiguredPath', ); if (!workspacePath) { - vscode.window.showErrorMessage('No workspace configured.'); + const errorMsg = 'No workspace configured. Please set up workspace first.'; + ecoOutput.appendLine(errorMsg); + vscode.window.showErrorMessage(errorMsg); return; } - // Step 1: Mark every Python file in the workspace as "refactoring" - const allPythonFiles = getAllPythonFiles(workspacePath); - for (const filePath of allPythonFiles) { - smellsViewProvider.setStatus(filePath, 'refactoring'); + // Mark all Python files as being refactored + try { + const allPythonFiles = getAllPythonFiles(workspacePath); + allPythonFiles.forEach((filePath) => { + smellsViewProvider.setStatus(filePath, 'refactoring'); + }); + } catch (error) { + ecoOutput.appendLine(`Error marking files for refactoring: ${error}`); } - // Step 2: Check if the server is down (can overwrite to "server_down" if needed) + // Check backend server status if (serverStatus.getStatus() === ServerStatusType.DOWN) { - vscode.window.showWarningMessage( - 'Action blocked: Server is down and no cached smells exist for this file version.', - ); + const warningMsg = + 'Server unavailable - cannot refactor without backend connection'; + ecoOutput.appendLine(warningMsg); + vscode.window.showWarningMessage(warningMsg); smellsViewProvider.setStatus(smell.path, 'server_down'); return; } + // Begin refactoring process smellsViewProvider.setStatus(smell.path, 'queued'); + vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); try { - vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); - + // Step 1: Send refactoring request to backend const refactoredData = await backendRefactorSmell(smell, workspacePath); + ecoOutput.appendLine(`Refactoring completed for ${smell.path}`); + ecoOutput.appendLine( + `Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, + ); - ecoOutput.appendLine(`Refactoring response: ${JSON.stringify(refactoredData)}`); - + // Step 2: Update UI with refactoring results refactoringDetailsViewProvider.updateRefactoringDetails( smell, refactoredData.targetFile, @@ -83,23 +114,45 @@ export async function refactorSmell( refactoredData.energySaved, ); - await openDiffEditor( - refactoredData.targetFile.original, - refactoredData.targetFile.refactored, + // Step 3: Show diff editor comparison + const targetFile = refactoredData.targetFile; + const fileName = path.basename(targetFile.original); + const originalUri = vscode.Uri.file(targetFile.original); + const refactoredUri = vscode.Uri.file(targetFile.refactored); + await vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + refactoredUri, + `Refactoring Comparison (${fileName})`, + { + preview: false, // Ensure the diff editor is not in preview mode + }, ); + registerDiffEditor(originalUri, refactoredUri); + // Step 4: Focus refactoring view and show action buttons await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); - showRefactorActionButtons(context); - vscode.window.showInformationMessage( - `Refactoring successful! Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, - ); - } catch (error: any) { - console.error('Refactoring failed:', error.message); - vscode.window.showErrorMessage(`Refactoring failed: ${error.message}`); - + // Step 5: Notify user of success + const successMsg = `Refactoring successful! Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`; + ecoOutput.appendLine(successMsg); + vscode.window.showInformationMessage(successMsg); + } catch (error) { + // Handle refactoring failures + const errorMsg = `Refactoring failed: ${error instanceof Error ? error.message : String(error)}`; + ecoOutput.appendLine(errorMsg); + console.error('Refactoring error:', error); + vscode.window.showErrorMessage(errorMsg); + + // Reset UI state refactoringDetailsViewProvider.resetRefactoringDetails(); vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + + // Update file status + smellsViewProvider.setStatus(smell.path, 'failed'); + } finally { + // Ensure context is reset even if errors occur + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } diff --git a/src/commands/rejectRefactoring.ts b/src/commands/rejectRefactoring.ts index ab48bb1..f81c2e3 100644 --- a/src/commands/rejectRefactoring.ts +++ b/src/commands/rejectRefactoring.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; -import { closeAllRefactorDiffEditors } from '../utils/openDiffEditor'; import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; +import { closeAllTrackedDiffEditors } from '../utils/trackedDiffEditors'; + export async function rejectRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, context: vscode.ExtensionContext, @@ -10,6 +11,8 @@ export async function rejectRefactoring( // Clear state + UI refactoringDetailsViewProvider.resetRefactoringDetails(); - await closeAllRefactorDiffEditors(); hideRefactorActionButtons(context); + + // Close any tracked diff editors + await closeAllTrackedDiffEditors(); } diff --git a/src/extension.ts b/src/extension.ts index 51d941d..be0b65e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import path from 'path'; // === Output Channel === export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); @@ -6,7 +7,6 @@ export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); // === Core Utilities === import { loadSmells } from './utils/smellsData'; import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; -import { openDiffEditor } from './utils/openDiffEditor'; import { envConfig } from './utils/envConfig'; import { checkServerStatus } from './api/backend'; @@ -32,6 +32,7 @@ import { exportMetricsData } from './commands/exportMetricsData'; // === Listeners & UI === import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { LineSelectionManager } from './ui/LineSelection'; +import { registerDiffEditor } from './utils/trackedDiffEditors'; export function activate(context: vscode.ExtensionContext): void { ecoOutput.appendLine('Initializing Eco-Optimizer extension...'); @@ -206,7 +207,23 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand( 'ecooptimizer.openDiffEditor', (originalFilePath: string, refactoredFilePath: string) => { - openDiffEditor(originalFilePath, refactoredFilePath); + // Get the file name for the diff editor title + const fileName = path.basename(originalFilePath); + + // Show the diff editor with the updated title + const originalUri = vscode.Uri.file(originalFilePath); + const refactoredUri = vscode.Uri.file(refactoredFilePath); + vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + refactoredUri, + `Refactoring Comparison (${fileName})`, + { + preview: false, + }, + ); + + registerDiffEditor(originalUri, refactoredUri); }, ), diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index e477990..22ab02e 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -2,14 +2,20 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { getDescriptionByMessageId, getNameByMessageId } from '../utils/smellsData'; +/** + * Provides a tree view that displays detailed information about ongoing refactoring operations. + * Shows the target smell, affected files, and estimated energy savings. + */ export class RefactoringDetailsViewProvider implements vscode.TreeDataProvider { + // Event emitter for tree data changes private _onDidChangeTreeData = new vscode.EventEmitter< RefactoringDetailItem | undefined >(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + // State properties private refactoringDetails: RefactoringDetailItem[] = []; public targetFile: { original: string; refactored: string } | undefined; public affectedFiles: { original: string; refactored: string }[] = []; @@ -21,11 +27,11 @@ export class RefactoringDetailsViewProvider } /** - * Updates the refactoring details with the given target file, affected files, and energy saved. - * @param targetSmell - The smell being refactored. - * @param targetFile - The target file (original and refactored paths). - * @param affectedFiles - The list of affected files (original and refactored paths). - * @param energySaved - The amount of energy saved in kg CO2. + * Updates the view with new refactoring details + * @param targetSmell - The code smell being refactored + * @param targetFile - Paths to original and refactored target files + * @param affectedFiles - Additional files impacted by the refactoring + * @param energySaved - Estimated energy savings in kg CO2 */ updateRefactoringDetails( targetSmell: Smell, @@ -37,57 +43,57 @@ export class RefactoringDetailsViewProvider this.targetFile = targetFile; this.affectedFiles = affectedFiles; this.energySaved = energySaved; - - // Clear the existing refactoring details this.refactoringDetails = []; - // Add the smell being refactored as the first item + // Add smell information if (targetSmell) { const smellName = getNameByMessageId(targetSmell.messageId) || targetSmell.messageId; this.refactoringDetails.push( new RefactoringDetailItem( `Refactoring: ${smellName}`, - '', // Empty description since we'll show it as a child '', '', - true, // Make it collapsible to show description as child + '', + true, false, - true, // isSmellItem + true, ), ); } - // Add energy saved as the second item - if (energySaved) { + // Add energy savings + if (energySaved !== undefined) { this.refactoringDetails.push( new RefactoringDetailItem( - `Energy Saved: ${energySaved} kg CO2`, - '', + `Estimated Savings: ${energySaved} kg CO2`, + 'Based on energy impact analysis', '', '', false, - true, // isEnergySaved + true, ), ); } - // Add the target file - this.refactoringDetails.push( - new RefactoringDetailItem( - path.basename(targetFile.original), - 'Target File', - targetFile.original, - targetFile.refactored, - affectedFiles.length > 0, - ), - ); + // Add target file + if (targetFile) { + this.refactoringDetails.push( + new RefactoringDetailItem( + `${path.basename(targetFile.original)}`, + 'Main refactored file', + targetFile.original, + targetFile.refactored, + affectedFiles.length > 0, + ), + ); + } this._onDidChangeTreeData.fire(undefined); } /** - * Resets the refactoring details to indicate no refactoring is in progress. + * Resets the view to its initial state */ resetRefactoringDetails(): void { this.targetFile = undefined; @@ -98,90 +104,97 @@ export class RefactoringDetailsViewProvider this._onDidChangeTreeData.fire(undefined); } + // VS Code TreeDataProvider implementation getTreeItem(element: RefactoringDetailItem): vscode.TreeItem { return element; } getChildren(element?: RefactoringDetailItem): RefactoringDetailItem[] { - if (element) { - // If this is the smell parent item, return the description as child - if (element.isSmellItem && this.targetSmell) { - const smellDescription = - getDescriptionByMessageId(this.targetSmell.messageId) || - this.targetSmell.messageId; - return [ + if (!element) { + return this.refactoringDetails; + } + + // Handle smell description expansion + if (element.isSmellItem && this.targetSmell) { + const description = + getDescriptionByMessageId(this.targetSmell.messageId) || + this.targetSmell.message; + return [ + new RefactoringDetailItem( + '', + description, + '', + '', + false, + false, + false, + 'info', + ), + ]; + } + + // Handle affected files expansion + if (element.isParent && this.affectedFiles.length > 0) { + return this.affectedFiles.map( + (file) => new RefactoringDetailItem( - '', - smellDescription, - '', - '', - false, // Not collapsible - false, - false, - 'info', // New parameter for description type + path.basename(file.original), + 'Affected file', + file.original, + file.refactored, ), - ]; - } - - // If this is the parent item (Target File), return the affected files as children - if (element.isParent) { - return this.affectedFiles.map( - (file) => - new RefactoringDetailItem( - path.basename(file.original), - 'Affected File', - file.original, - file.refactored, - false, - ), - ); - } - return []; + ); } - return this.refactoringDetails; + + return []; } } +/** + * Represents an item in the refactoring details tree view + */ class RefactoringDetailItem extends vscode.TreeItem { constructor( - label: string, - description: string, + public readonly label: string, + public readonly description: string, public readonly originalFilePath: string, public readonly refactoredFilePath: string, public readonly isParent: boolean = false, public readonly isEnergySaved: boolean = false, public readonly isSmellItem: boolean = false, - public readonly itemType?: 'info' | 'file', // New parameter to distinguish types + public readonly itemType: 'info' | 'file' | 'none' = 'none', ) { super( label, - isParent || isSmellItem // Make smell items collapsible too + isParent || isSmellItem ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, ); - this.description = description; - // Custom icons based on type + // Configure item based on type if (isEnergySaved) { this.iconPath = new vscode.ThemeIcon( 'lightbulb', new vscode.ThemeColor('charts.yellow'), ); - this.tooltip = 'This is the amount of energy saved by refactoring.'; + this.tooltip = 'Estimated energy savings from this refactoring'; } else if (isSmellItem) { this.iconPath = new vscode.ThemeIcon( - 'symbol-class', - new vscode.ThemeColor('charts.'), + 'warning', + new vscode.ThemeColor('charts.orange'), ); + } else if (itemType === 'info') { + this.iconPath = new vscode.ThemeIcon('info'); } - // Add commands where appropriate - if (!isEnergySaved && !isSmellItem && itemType !== 'info' && originalFilePath) { + // Make files clickable to open diffs + if (originalFilePath && refactoredFilePath && itemType !== 'info') { this.command = { command: 'ecooptimizer.openDiffEditor', - title: 'Open Diff Editor', + title: 'Compare Changes', arguments: [originalFilePath, refactoredFilePath], }; + this.contextValue = 'refactoringFile'; } } } diff --git a/src/utils/openDiffEditor.ts b/src/utils/openDiffEditor.ts deleted file mode 100644 index f73f424..0000000 --- a/src/utils/openDiffEditor.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; - -const openedRefactorDiffs: [vscode.Uri, vscode.Uri][] = []; - -export async function openDiffEditor( - originalFilePath: string, - refactoredFilePath: string, -): Promise { - const fileName = path.basename(originalFilePath); - const originalUri = vscode.Uri.file(originalFilePath); - const refactoredUri = vscode.Uri.file(refactoredFilePath); - - // Store this diff pair for later cleanup - openedRefactorDiffs.push([originalUri, refactoredUri]); - - await vscode.commands.executeCommand( - 'vscode.diff', - originalUri, - refactoredUri, - `Refactoring Comparison (${fileName})`, - { preview: false }, - ); -} - -// Utility to close all tracked diff editors -export async function closeAllRefactorDiffEditors(): Promise { - const visibleEditors = vscode.window.visibleTextEditors; - - for (const editor of visibleEditors) { - const uri = editor.document.uri; - const isRefactorDiff = openedRefactorDiffs.some( - ([original, refactored]) => - uri.toString() === original.toString() || - uri.toString() === refactored.toString(), - ); - - if (isRefactorDiff) { - await vscode.window.showTextDocument(uri, { preview: false }); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - } - } - - openedRefactorDiffs.length = 0; // clear tracked diffs -} diff --git a/src/utils/trackedDiffEditors.ts b/src/utils/trackedDiffEditors.ts new file mode 100644 index 0000000..8155c40 --- /dev/null +++ b/src/utils/trackedDiffEditors.ts @@ -0,0 +1,32 @@ +// utils/trackedDiffEditors.ts +import * as vscode from 'vscode'; + +const trackedDiffs = new Set(); + +export function registerDiffEditor(original: vscode.Uri, modified: vscode.Uri) { + trackedDiffs.add(`${original.toString()}::${modified.toString()}`); +} + +export function isTrackedDiffEditor( + original: vscode.Uri, + modified: vscode.Uri, +): boolean { + return trackedDiffs.has(`${original.toString()}::${modified.toString()}`); +} + +export async function closeAllTrackedDiffEditors() { + const tabs = vscode.window.tabGroups.all.flatMap((group) => group.tabs); + + for (const tab of tabs) { + if (tab.input && (tab.input as any).modified && (tab.input as any).original) { + const original = (tab.input as any).original as vscode.Uri; + const modified = (tab.input as any).modified as vscode.Uri; + + if (isTrackedDiffEditor(original, modified)) { + await vscode.window.tabGroups.close(tab, true); + } + } + } + + trackedDiffs.clear(); +} From 81c2191bf1bce10c3ace657d6aa9c903e519481b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 11:39:26 -0400 Subject: [PATCH 072/121] clean --- src/commands/acceptRefactoring.ts | 2 +- src/commands/refactorSmell.ts | 59 +++--------------------- src/commands/rejectRefactoring.ts | 18 ++++++-- src/extension.ts | 43 ++++-------------- src/utils/refactorActionButtons.ts | 73 ++++++++++++++++++++---------- 5 files changed, 79 insertions(+), 116 deletions(-) diff --git a/src/commands/acceptRefactoring.ts b/src/commands/acceptRefactoring.ts index bc4ce1f..fb4759c 100644 --- a/src/commands/acceptRefactoring.ts +++ b/src/commands/acceptRefactoring.ts @@ -60,7 +60,7 @@ export async function acceptRefactoring( refactoringDetailsViewProvider.resetRefactoringDetails(); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - hideRefactorActionButtons(context); + hideRefactorActionButtons(); smellsViewProvider.refresh(); } catch (error) { diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 8432e91..9197d55 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; import * as path from 'path'; import { backendRefactorSmell } from '../api/backend'; @@ -7,45 +6,12 @@ import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { ecoOutput } from '../extension'; import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; -import { showRefactorActionButtons } from '../utils/refactorActionButtons'; +import { + showRefactorActionButtons, + hideRefactorActionButtons, +} from '../utils/refactorActionButtons'; import { registerDiffEditor } from '../utils/trackedDiffEditors'; -/** - * Recursively collects all Python files (.py) in a directory and its subdirectories - * @param dir - The root directory path to search from - * @returns Array of absolute file paths to all Python files found - */ -function getAllPythonFiles(dir: string): string[] { - const pythonFiles: string[] = []; - - /** - * Recursive directory walker function - * @param currentDir - Current directory being processed - */ - const walkDirectory = (currentDir: string) => { - try { - const entries = fs.readdirSync(currentDir); - - for (const entry of entries) { - const fullPath = path.join(currentDir, entry); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - walkDirectory(fullPath); - } else if (stat.isFile() && fullPath.endsWith('.py')) { - pythonFiles.push(fullPath); - } - } - } catch (error) { - ecoOutput.appendLine(`Error scanning directory ${currentDir}: ${error}`); - console.error(`Directory scan error: ${error}`); - } - }; - - walkDirectory(dir); - return pythonFiles; -} - /** * Handles the complete refactoring workflow for a detected code smell * @param smellsViewProvider - Reference to the smells view provider @@ -74,16 +40,6 @@ export async function refactorSmell( return; } - // Mark all Python files as being refactored - try { - const allPythonFiles = getAllPythonFiles(workspacePath); - allPythonFiles.forEach((filePath) => { - smellsViewProvider.setStatus(filePath, 'refactoring'); - }); - } catch (error) { - ecoOutput.appendLine(`Error marking files for refactoring: ${error}`); - } - // Check backend server status if (serverStatus.getStatus() === ServerStatusType.DOWN) { const warningMsg = @@ -132,7 +88,7 @@ export async function refactorSmell( // Step 4: Focus refactoring view and show action buttons await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); - showRefactorActionButtons(context); + showRefactorActionButtons(); // Step 5: Notify user of success const successMsg = `Refactoring successful! Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`; @@ -147,12 +103,9 @@ export async function refactorSmell( // Reset UI state refactoringDetailsViewProvider.resetRefactoringDetails(); - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); + hideRefactorActionButtons(); // Update file status smellsViewProvider.setStatus(smell.path, 'failed'); - } finally { - // Ensure context is reset even if errors occur - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } diff --git a/src/commands/rejectRefactoring.ts b/src/commands/rejectRefactoring.ts index f81c2e3..682151a 100644 --- a/src/commands/rejectRefactoring.ts +++ b/src/commands/rejectRefactoring.ts @@ -2,17 +2,27 @@ import * as vscode from 'vscode'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; import { closeAllTrackedDiffEditors } from '../utils/trackedDiffEditors'; +import { SmellsViewProvider } from '../providers/SmellsViewProvider'; + +function normalizePath(filePath: string): string { + return filePath.toLowerCase(); +} export async function rejectRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, + smellsViewProvider: SmellsViewProvider, context: vscode.ExtensionContext, ): Promise { vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); // Clear state + UI - refactoringDetailsViewProvider.resetRefactoringDetails(); - hideRefactorActionButtons(context); - - // Close any tracked diff editors + if (refactoringDetailsViewProvider.targetFile?.original) { + smellsViewProvider.setStatus( + normalizePath(refactoringDetailsViewProvider.targetFile.original), + 'passed', + ); + } await closeAllTrackedDiffEditors(); + refactoringDetailsViewProvider.resetRefactoringDetails(); + hideRefactorActionButtons(); } diff --git a/src/extension.ts b/src/extension.ts index be0b65e..e625aed 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -33,43 +33,11 @@ import { exportMetricsData } from './commands/exportMetricsData'; import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; import { LineSelectionManager } from './ui/LineSelection'; import { registerDiffEditor } from './utils/trackedDiffEditors'; +import { initializeRefactorActionButtons } from './utils/refactorActionButtons'; export function activate(context: vscode.ExtensionContext): void { ecoOutput.appendLine('Initializing Eco-Optimizer extension...'); - // === Status Bar Buttons for Refactoring === - const acceptRefactoringItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left, - 100, - ); - acceptRefactoringItem.text = '$(check) Accept Refactoring'; - acceptRefactoringItem.command = 'ecooptimizer.acceptRefactoring'; - acceptRefactoringItem.tooltip = 'Accept and apply the suggested refactoring'; - acceptRefactoringItem.hide(); - - const rejectRefactoringItem = vscode.window.createStatusBarItem( - vscode.StatusBarAlignment.Left, - 99, - ); - rejectRefactoringItem.text = '$(x) Reject Refactoring'; - rejectRefactoringItem.command = 'ecooptimizer.rejectRefactoring'; - rejectRefactoringItem.tooltip = 'Reject the suggested refactoring'; - rejectRefactoringItem.hide(); - - context.subscriptions.push(acceptRefactoringItem, rejectRefactoringItem); - - vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); - - vscode.commands.registerCommand('ecooptimizer.showRefactorStatusBar', () => { - acceptRefactoringItem.show(); - rejectRefactoringItem.show(); - }); - - vscode.commands.registerCommand('ecooptimizer.hideRefactorStatusBar', () => { - acceptRefactoringItem.hide(); - rejectRefactoringItem.hide(); - }); - // === Load Core Data === loadSmells(); @@ -77,6 +45,9 @@ export function activate(context: vscode.ExtensionContext): void { checkServerStatus(); setInterval(checkServerStatus, 10000); + // === Initialize Refactor Action Buttons === + initializeRefactorActionButtons(context); + // === Initialize Managers & Providers === const smellsCacheManager = new SmellsCacheManager(context); const smellsViewProvider = new SmellsViewProvider(context); @@ -201,7 +172,11 @@ export function activate(context: vscode.ExtensionContext): void { }), vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', async () => { - await rejectRefactoring(refactoringDetailsViewProvider, context); + await rejectRefactoring( + refactoringDetailsViewProvider, + smellsViewProvider, + context, + ); }), vscode.commands.registerCommand( diff --git a/src/utils/refactorActionButtons.ts b/src/utils/refactorActionButtons.ts index 21630b7..00a6e27 100644 --- a/src/utils/refactorActionButtons.ts +++ b/src/utils/refactorActionButtons.ts @@ -1,46 +1,71 @@ import * as vscode from 'vscode'; +import { ecoOutput } from '../extension'; -export function showRefactorActionButtons(context: vscode.ExtensionContext) { - const acceptButton = vscode.window.createStatusBarItem( +let acceptButton: vscode.StatusBarItem | undefined; +let rejectButton: vscode.StatusBarItem | undefined; + +/** + * Create and register the status bar buttons (called once at activation). + */ +export function initializeRefactorActionButtons( + context: vscode.ExtensionContext, +): void { + ecoOutput.appendLine('Initializing refactor action buttons...'); + + acceptButton = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, 0, ); - acceptButton.text = '$(check) Accept Refactoring'; - acceptButton.command = 'ecooptimizer.acceptRefactoring'; - acceptButton.color = 'lightgreen'; - acceptButton.tooltip = 'Apply the suggested refactoring'; - - const rejectButton = vscode.window.createStatusBarItem( + rejectButton = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, 1, ); - rejectButton.text = '$(x) Reject Refactoring'; + + acceptButton.text = '$(check) ACCEPT REFACTOR'; + acceptButton.command = 'ecooptimizer.acceptRefactoring'; + acceptButton.tooltip = 'Accept and apply the suggested refactoring'; + acceptButton.color = new vscode.ThemeColor('charts.green'); + + rejectButton.text = '$(x) REJECT REFACTOR'; rejectButton.command = 'ecooptimizer.rejectRefactoring'; - rejectButton.color = 'red'; - rejectButton.tooltip = 'Discard the suggested refactoring'; + rejectButton.tooltip = 'Reject the suggested refactoring'; + rejectButton.color = new vscode.ThemeColor('charts.red'); context.subscriptions.push(acceptButton, rejectButton); - // Show them only when refactoring is active - vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); + ecoOutput.appendLine('Status bar buttons created and registered.'); +} + +/** + * Show the status bar buttons when a refactoring is in progress. + */ +export function showRefactorActionButtons(): void { + if (!acceptButton || !rejectButton) { + ecoOutput.appendLine( + '❌ Tried to show refactor buttons but they are not initialized.', + ); + return; + } + ecoOutput.appendLine('Showing refactor action buttons...'); acceptButton.show(); rejectButton.show(); + vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); } /** - * Hides the refactor action buttons from the status bar. + * Hide the status bar buttons when the refactoring ends. */ -export function hideRefactorActionButtons(context: vscode.ExtensionContext) { - const acceptButton = context.workspaceState.get( - 'ecooptimizer.refactorAcceptButton', - ); - const rejectButton = context.workspaceState.get( - 'ecooptimizer.refactorRejectButton', - ); - - acceptButton?.hide(); - rejectButton?.hide(); +export function hideRefactorActionButtons(): void { + if (!acceptButton || !rejectButton) { + ecoOutput.appendLine( + '❌ Tried to hide refactor buttons but they are not initialized.', + ); + return; + } + ecoOutput.appendLine('Hiding refactor action buttons...'); + acceptButton.hide(); + rejectButton.hide(); vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } From 4d15e0021557357e73dfa9dea03bf7dd6ad14624 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 11:54:13 -0400 Subject: [PATCH 073/121] clean backend.ts --- src/api/backend.ts | 72 ++++++++++++++++++++++------------------------ src/extension.ts | 2 +- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/api/backend.ts b/src/api/backend.ts index 6621bd0..e3b5a69 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -1,38 +1,42 @@ +import path from 'path'; + import { envConfig } from '../utils/envConfig'; import { serverStatus } from '../emitters/serverStatus'; import { ServerStatusType } from '../emitters/serverStatus'; import { ecoOutput } from '../extension'; -const BASE_URL = `http://${envConfig.SERVER_URL}`; // API URL for Python backend +const BASE_URL = `http://${envConfig.SERVER_URL}`; /** - * Checks the status of the backend server. + * Checks the health status of the backend server and updates the extension's status emitter. */ export async function checkServerStatus(): Promise { try { + ecoOutput.appendLine('[backend.ts] Checking backend server health status...'); const response = await fetch(`${BASE_URL}/health`); + if (response.ok) { serverStatus.setStatus(ServerStatusType.UP); + ecoOutput.appendLine('[backend.ts] Backend server is healthy'); } else { serverStatus.setStatus(ServerStatusType.DOWN); + ecoOutput.appendLine(`[backend.ts] Backend server unhealthy status: ${response.status}`); } - } catch { + } catch (error) { serverStatus.setStatus(ServerStatusType.DOWN); + ecoOutput.appendLine(`[backend.ts] Server connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** - * Sends a request to the backend to detect code smells in the specified file. - * - * @param filePath - The absolute path to the file being analyzed. - * @param enabledSmells - A dictionary containing enabled smells and their configured options. - * @returns A promise resolving to the backend response or throwing an error if unsuccessful. + * Detects code smells in a specified file by communicating with the backend service. */ export async function fetchSmells( filePath: string, enabledSmells: Record>, ): Promise<{ smells: Smell[]; status: number }> { const url = `${BASE_URL}/smells`; + ecoOutput.appendLine(`[backend.ts] Starting smell detection for: ${path.basename(filePath)}`); try { const response = await fetch(url, { @@ -47,31 +51,23 @@ export async function fetchSmells( }); if (!response.ok) { - throw new Error( - `Backend request failed with status ${response.status}: ${response.statusText}`, - ); + const errorMsg = `Backend request failed (${response.status})`; + ecoOutput.appendLine(`[backend.ts] ${errorMsg}`); + throw new Error(errorMsg); } const smellsList = await response.json(); - - if (!Array.isArray(smellsList)) { - throw new Error('Unexpected response format from backend.'); - } - + ecoOutput.appendLine(`[backend.ts] Detected ${smellsList.length} smells in ${path.basename(filePath)}`); return { smells: smellsList, status: response.status }; + } catch (error: any) { - throw new Error( - `Failed to connect to the backend: ${error.message}. Please check your network and try again.`, - ); + ecoOutput.appendLine(`[backend.ts] Smell detection failed: ${error.message}`); + throw new Error(`Detection failed: ${error.message}`); } } /** - * Sends a request to the backend to refactor a specific smell. - * - * @param smell - The smell to refactor. - * @param workspacePath - The user-configured workspace root directory. - * @returns A promise resolving to the refactored data or throwing an error if unsuccessful. + * Initiates refactoring of a specific code smell through the backend service. */ export async function backendRefactorSmell( smell: Smell, @@ -80,17 +76,11 @@ export async function backendRefactorSmell( const url = `${BASE_URL}/refactor`; if (!workspacePath) { - throw new Error('No workspace path provided for refactoring.'); + ecoOutput.appendLine('[backend.ts] Refactoring aborted: No workspace path'); + throw new Error('No workspace path provided'); } - ecoOutput.appendLine( - `Eco: Initiating refactoring for smell "${smell.symbol}" in "${workspacePath}"`, - ); - - const payload = { - source_dir: workspacePath, - smell, - }; + ecoOutput.appendLine(`[backend.ts] Starting refactoring for smell: ${smell.symbol}`); try { const response = await fetch(url, { @@ -98,18 +88,24 @@ export async function backendRefactorSmell( headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(payload), + body: JSON.stringify({ + source_dir: workspacePath, + smell, + }), }); if (!response.ok) { const errorData = await response.json(); + ecoOutput.appendLine(`[backend.ts] Refactoring failed: ${errorData.detail || 'Unknown error'}`); throw new Error(errorData.detail || 'Refactoring failed'); } - const refactorResult = (await response.json()) as RefactoredData; - return refactorResult; + const result = await response.json(); + ecoOutput.appendLine(`[backend.ts] Refactoring successful for ${smell.symbol}`); + return result; + } catch (error: any) { - console.error('Eco: Unexpected error in backendRefactorSmell:', error); + ecoOutput.appendLine(`[backend.ts] Refactoring error: ${error.message}`); throw new Error(`Refactoring failed: ${error.message}`); } -} +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index e625aed..5890029 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -43,7 +43,7 @@ export function activate(context: vscode.ExtensionContext): void { // === Start periodic backend status checks === checkServerStatus(); - setInterval(checkServerStatus, 10000); + setInterval(checkServerStatus, 300000); // 5 minutes // === Initialize Refactor Action Buttons === initializeRefactorActionButtons(context); From 0e60510058413148b8c4aff9acf16cf55de975de Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:01:15 -0400 Subject: [PATCH 074/121] clean backend.ts and acceptRefactoring.ts --- src/api/backend.ts | 31 +++++++++-- src/commands/acceptRefactoring.ts | 93 ++++++++++++++++++++++--------- 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/src/api/backend.ts b/src/api/backend.ts index e3b5a69..e0e7fe0 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -1,14 +1,18 @@ import path from 'path'; - import { envConfig } from '../utils/envConfig'; import { serverStatus } from '../emitters/serverStatus'; import { ServerStatusType } from '../emitters/serverStatus'; import { ecoOutput } from '../extension'; +// Base URL for backend API endpoints constructed from environment configuration const BASE_URL = `http://${envConfig.SERVER_URL}`; /** - * Checks the health status of the backend server and updates the extension's status emitter. + * Verifies backend service availability and updates extension status. + * Performs health check by hitting the /health endpoint and handles three scenarios: + * 1. Successful response (200-299) - marks server as UP + * 2. Error response - marks server as DOWN with status code + * 3. Network failure - marks server as DOWN with error details */ export async function checkServerStatus(): Promise { try { @@ -24,12 +28,21 @@ export async function checkServerStatus(): Promise { } } catch (error) { serverStatus.setStatus(ServerStatusType.DOWN); - ecoOutput.appendLine(`[backend.ts] Server connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + ecoOutput.appendLine( + `[backend.ts] Server connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` + ); } } /** - * Detects code smells in a specified file by communicating with the backend service. + * Analyzes source code for code smells using backend detection service. + * @param filePath - Absolute path to the source file for analysis + * @param enabledSmells - Configuration object specifying which smells to detect + * @returns Promise resolving to smell detection results and HTTP status + * @throws Error when: + * - Network request fails + * - Backend returns non-OK status + * - Response contains invalid data format */ export async function fetchSmells( filePath: string, @@ -67,7 +80,14 @@ export async function fetchSmells( } /** - * Initiates refactoring of a specific code smell through the backend service. + * Executes code refactoring for a specific detected smell pattern. + * @param smell - The smell object containing detection details + * @param workspacePath - Root directory of the project workspace + * @returns Promise resolving to refactoring result data + * @throws Error when: + * - Workspace path is not provided + * - Refactoring request fails + * - Network errors occur */ export async function backendRefactorSmell( smell: Smell, @@ -75,6 +95,7 @@ export async function backendRefactorSmell( ): Promise { const url = `${BASE_URL}/refactor`; + // Validate workspace configuration if (!workspacePath) { ecoOutput.appendLine('[backend.ts] Refactoring aborted: No workspace path'); throw new Error('No workspace path provided'); diff --git a/src/commands/acceptRefactoring.ts b/src/commands/acceptRefactoring.ts index fb4759c..aee108d 100644 --- a/src/commands/acceptRefactoring.ts +++ b/src/commands/acceptRefactoring.ts @@ -7,10 +7,29 @@ import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { ecoOutput } from '../extension'; import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; +/** + * Normalizes file paths for consistent comparison and caching + * @param filePath - The file path to normalize + * @returns Lowercase version of the path for case-insensitive comparison + */ function normalizePath(filePath: string): string { return filePath.toLowerCase(); } +/** + * Handles acceptance and application of refactoring changes to the codebase. + * Performs the following operations: + * 1. Applies refactored changes to target and affected files + * 2. Updates energy savings metrics + * 3. Clears cached smell data for modified files + * 4. Updates UI components to reflect changes + * + * @param refactoringDetailsViewProvider - Provides access to refactoring details + * @param metricsDataProvider - Handles metrics tracking and updates + * @param smellsCacheManager - Manages smell detection cache invalidation + * @param smellsViewProvider - Controls the smells view UI updates + * @param context - VS Code extension context + */ export async function acceptRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, metricsDataProvider: MetricsViewProvider, @@ -21,52 +40,76 @@ export async function acceptRefactoring( const targetFile = refactoringDetailsViewProvider.targetFile; const affectedFiles = refactoringDetailsViewProvider.affectedFiles; + // Validate refactoring data exists if (!targetFile || !affectedFiles) { + ecoOutput.appendLine( + '[refactorActions.ts] Error: No refactoring data available', + ); vscode.window.showErrorMessage('No refactoring data available.'); return; } try { + ecoOutput.appendLine( + `[refactorActions.ts] Applying refactoring to target file: ${targetFile.original}`, + ); + + // Apply refactored changes to filesystem fs.copyFileSync(targetFile.refactored, targetFile.original); - for (const file of affectedFiles) { + affectedFiles.forEach((file) => { fs.copyFileSync(file.refactored, file.original); - } - - const energySaved = refactoringDetailsViewProvider.energySaved; - const targetSmell = refactoringDetailsViewProvider.targetSmell?.symbol; - const file = vscode.Uri.file(targetFile.original).fsPath; + ecoOutput.appendLine( + `[refactorActions.ts] Updated affected file: ${file.original}`, + ); + }); - if (energySaved && targetSmell) { - ecoOutput.appendLine(`Updating metrics for ${file}`); - metricsDataProvider.updateMetrics(file, energySaved, targetSmell); + // Update metrics if energy savings data exists + if ( + refactoringDetailsViewProvider.energySaved && + refactoringDetailsViewProvider.targetSmell + ) { + metricsDataProvider.updateMetrics( + targetFile.original, + refactoringDetailsViewProvider.energySaved, + refactoringDetailsViewProvider.targetSmell.symbol, + ); + ecoOutput.appendLine('[refactorActions.ts] Updated energy savings metrics'); } - vscode.window.showInformationMessage('Refactoring accepted! Changes applied.'); - - await smellsCacheManager.clearCachedSmellsForFile( - normalizePath(targetFile.original), + // Invalidate cache for modified files + await Promise.all([ + smellsCacheManager.clearCachedSmellsForFile( + normalizePath(targetFile.original), + ), + ...affectedFiles.map((file) => + smellsCacheManager.clearCachedSmellsForFile(normalizePath(file.original)), + ), + ]); + ecoOutput.appendLine( + '[refactorActions.ts] Cleared smell caches for modified files', ); - for (const file of affectedFiles) { - await smellsCacheManager.clearCachedSmellsForFile( - normalizePath(file.original), - ); - } + // Update UI state smellsViewProvider.setStatus(normalizePath(targetFile.original), 'outdated'); - for (const file of affectedFiles) { + affectedFiles.forEach((file) => { smellsViewProvider.setStatus(normalizePath(file.original), 'outdated'); - } + }); + // Reset UI components refactoringDetailsViewProvider.resetRefactoringDetails(); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); - hideRefactorActionButtons(); - smellsViewProvider.refresh(); + + vscode.window.showInformationMessage('Refactoring successfully applied'); + ecoOutput.appendLine( + '[refactorActions.ts] Refactoring changes completed successfully', + ); } catch (error) { - console.error('Failed to accept refactoring:', error); - vscode.window.showErrorMessage( - 'Failed to accept refactoring. Please try again.', + const errorDetails = error instanceof Error ? error.message : 'Unknown error'; + ecoOutput.appendLine( + `[refactorActions.ts] Error applying refactoring: ${errorDetails}`, ); + vscode.window.showErrorMessage('Failed to apply refactoring. Please try again.'); } } From df5e2b2fc95ec3523316ee574bbb123af8bff4bf Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:03:36 -0400 Subject: [PATCH 075/121] clean configureWorkspace.ts --- src/commands/configureWorkspace.ts | 60 +++++++++++++++--------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index 9d2afef..564a01f 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -3,21 +3,23 @@ import * as path from 'path'; import * as fs from 'fs'; /** - * Prompts the user to configure a workspace by selecting a folder containing Python files. - * Updates the workspace state accordingly and refreshes the tree view to reflect the changes. + * Initializes workspace configuration by prompting user to select a Python project folder. + * This is the main entry point for workspace configuration and delegates to folder-specific logic. * - * @param context - The extension context for managing workspace state. + * @param context - VS Code extension context containing workspace state management */ export async function configureWorkspace(context: vscode.ExtensionContext) { - // Directly configure a Python folder (removed the file option) await configurePythonFolder(context); } /** - * Recursively scans a folder to find subfolders containing Python files or __init__.py. + * Recursively identifies Python project folders by scanning for: + * - Python files (*.py) + * - __init__.py package markers + * Maintains a hierarchical understanding of Python projects in the workspace. * - * @param folderPath - The path of the folder to scan. - * @returns An array of folder paths that contain Python files. + * @param folderPath - Absolute filesystem path to scan + * @returns Array of qualified Python project paths */ function findPythonFoldersRecursively(folderPath: string): string[] { let pythonFolders: string[] = []; @@ -26,7 +28,7 @@ function findPythonFoldersRecursively(folderPath: string): string[] { try { const files = fs.readdirSync(folderPath); - // Check if the current folder contains Python files or __init__.py + // Validate current folder contains Python artifacts if ( files.includes('__init__.py') || files.some((file) => file.endsWith('.py')) @@ -34,7 +36,7 @@ function findPythonFoldersRecursively(folderPath: string): string[] { hasPythonFiles = true; } - // Recursively scan subfolders + // Recursively process subdirectories for (const file of files) { const filePath = path.join(folderPath, file); if (fs.statSync(filePath).isDirectory()) { @@ -46,14 +48,13 @@ function findPythonFoldersRecursively(folderPath: string): string[] { } } - // Only add this folder if it or its subfolders contain Python files + // Include current folder if Python content found at any level if (hasPythonFiles) { pythonFolders.push(folderPath); } } catch (error) { - // Log the error and notify the user vscode.window.showErrorMessage( - `Error scanning folder ${folderPath}: ${(error as Error).message}`, + `Workspace scanning error in ${path.basename(folderPath)}: ${(error as Error).message}`, ); } @@ -61,46 +62,46 @@ function findPythonFoldersRecursively(folderPath: string): string[] { } /** - * Configures the workspace using a selected Python folder. - * Prompts the user to select a folder containing Python files from the workspace. + * Guides user through Python workspace selection process with validation. + * Presents filtered list of valid Python project folders and handles selection. * - * @param context - The extension context for managing workspace state. + * @param context - Extension context for state persistence */ async function configurePythonFolder(context: vscode.ExtensionContext) { const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { + if (!workspaceFolders?.length) { vscode.window.showErrorMessage( - 'No workspace folders found. Open a project in Explorer first.', + 'No workspace detected. Please open a project folder first.', ); return; } - // Find all valid Python folders in the workspace + // Identify all Python project roots const validPythonFolders = workspaceFolders .map((folder) => folder.uri.fsPath) - .flatMap((folderPath) => findPythonFoldersRecursively(folderPath)); + .flatMap(findPythonFoldersRecursively); if (validPythonFolders.length === 0) { vscode.window.showErrorMessage( - 'No valid Python folders found in your workspace. A valid folder must contain Python files (*.py) or an __init__.py file.', + 'No Python projects found. Workspace must contain .py files or __init__.py markers.', ); return; } - // Show folder selection dialog + // Present interactive folder selection const selectedFolder = await vscode.window.showQuickPick( validPythonFolders.map((folder) => ({ label: path.basename(folder), description: folder, - detail: `Contains Python files: ${fs + detail: `Python content: ${fs .readdirSync(folder) .filter((file) => file.endsWith('.py') || file === '__init__.py') .join(', ')}`, folderPath: folder, })), { - placeHolder: 'Select a Python folder to use as your workspace', + placeHolder: 'Select Python project root', matchOnDescription: true, matchOnDetail: true, }, @@ -109,25 +110,26 @@ async function configurePythonFolder(context: vscode.ExtensionContext) { if (selectedFolder) { await updateWorkspace(context, selectedFolder.folderPath); vscode.window.showInformationMessage( - `Workspace configured for folder: ${path.basename(selectedFolder.folderPath)}`, + `Configured workspace: ${path.basename(selectedFolder.folderPath)}`, ); } } /** - * Updates the workspace configuration and refreshes the views. + * Persists workspace configuration and updates extension context. + * Triggers view refreshes to reflect new workspace state. * - * @param context - The extension context for managing workspace state. - * @param workspacePath - The path of the selected workspace (file or folder). + * @param context - Extension context for state management + * @param workspacePath - Absolute path to selected workspace root */ export async function updateWorkspace( context: vscode.ExtensionContext, workspacePath: string, ) { - // Update the workspace state with the selected path + // Persist workspace path await context.workspaceState.update('workspaceConfiguredPath', workspacePath); - // Set the workspace context to indicate that the workspace is configured + // Update extension context for UI state management vscode.commands.executeCommand( 'setContext', 'workspaceState.workspaceConfigured', From b0256849fecb96fb4317708abc75968893512067 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:10:37 -0400 Subject: [PATCH 076/121] clean detectSmells.ts --- src/commands/detectSmells.ts | 111 ++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/src/commands/detectSmells.ts b/src/commands/detectSmells.ts index 99c1d56..c41acaf 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detectSmells.ts @@ -1,14 +1,17 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; + import { fetchSmells } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { getEnabledSmells } from '../utils/smellsData'; import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { ecoOutput } from '../extension'; /** - * Runs smell detection on a single file if valid. + * Performs code smell analysis on a single Python file with comprehensive state management. + * Only shows user notifications for critical events requiring attention. */ export async function detectSmellsFile( filePath: string, @@ -29,29 +32,42 @@ export async function detectSmellsFile( ); try { + ecoOutput.appendLine(`[detection.ts] Analyzing: ${path.basename(filePath)}`); const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); if (status === 200) { if (smells.length > 0) { + ecoOutput.appendLine(`[detection.ts] Detected ${smells.length} smells`); smellsViewProvider.setStatus(filePath, 'passed'); await smellsCacheManager.setCachedSmells(filePath, smells); smellsViewProvider.setSmells(filePath, smells); + vscode.window.showInformationMessage( + `Found ${smells.length} code smells in ${path.basename(filePath)}`, + ); } else { + ecoOutput.appendLine('[detection.ts] File has no detectable smells'); smellsViewProvider.setStatus(filePath, 'no_issues'); await smellsCacheManager.setCachedSmells(filePath, []); } } else { + const msg = `Analysis failed for ${path.basename(filePath)} (status ${status})`; + ecoOutput.appendLine(`[detection.ts] ${msg}`); smellsViewProvider.setStatus(filePath, 'failed'); - vscode.window.showErrorMessage(`Analysis failed (status ${status}).`); + vscode.window.showErrorMessage(msg); } } catch (error: any) { + const msg = `Analysis failed: ${error.message}`; + ecoOutput.appendLine(`[detection.ts] ${msg}`); smellsViewProvider.setStatus(filePath, 'failed'); - vscode.window.showErrorMessage(`Analysis failed: ${error.message}`); + vscode.window.showErrorMessage(msg); } } /** - * Validates workspace state before initiating detection. + * Validates conditions before analysis. Only shows notifications when: + * - Using cached results (info) + * - Server is down (warning) + * - No smells configured (warning) */ async function precheckAndMarkQueued( filePath: string, @@ -60,7 +76,13 @@ async function precheckAndMarkQueued( ): Promise { if (smellsCacheManager.hasCachedSmells(filePath)) { const cached = smellsCacheManager.getCachedSmells(filePath); - vscode.window.showInformationMessage('Using cached smells for this file.'); + ecoOutput.appendLine( + `[detection.ts] Using cached results for ${path.basename(filePath)}`, + ); + vscode.window.showInformationMessage( + `Using cached analysis for ${path.basename(filePath)}`, + ); + if (cached && cached.length > 0) { smellsViewProvider.setStatus(filePath, 'passed'); smellsViewProvider.setSmells(filePath, cached); @@ -71,18 +93,18 @@ async function precheckAndMarkQueued( } if (serverStatus.getStatus() === ServerStatusType.DOWN) { - vscode.window.showWarningMessage( - 'Action blocked: Server is down and no cached smells exist for this file version.', - ); + const msg = 'Backend server unavailable - using cached results where available'; + ecoOutput.appendLine(`[detection.ts] ${msg}`); + vscode.window.showWarningMessage(msg); smellsViewProvider.setStatus(filePath, 'server_down'); return false; } const enabledSmells = getEnabledSmells(); if (Object.keys(enabledSmells).length === 0) { - vscode.window.showWarningMessage( - 'No enabled smells found. Please configure enabled smells in the settings.', - ); + const msg = 'No smell detectors enabled in settings'; + ecoOutput.appendLine(`[detection.ts] ${msg}`); + vscode.window.showWarningMessage(msg); return false; } @@ -91,31 +113,62 @@ async function precheckAndMarkQueued( } /** - * Detects smells in all Python files within the selected folder. + * Recursively analyzes Python files in a directory with progress indication. + * Shows a progress notification for the folder scan operation. */ export async function detectSmellsFolder( folderPath: string, smellsViewProvider: SmellsViewProvider, smellsCacheManager: SmellsCacheManager, ): Promise { - const pythonFiles: string[] = []; - - function walk(dir: string): void { - const entries = fs.readdirSync(dir); - for (const entry of entries) { - const fullPath = path.join(dir, entry); - const stat = fs.statSync(fullPath); - if (stat.isDirectory()) { - walk(fullPath); - } else if (stat.isFile() && fullPath.endsWith('.py')) { - pythonFiles.push(fullPath); + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Scanning for Python files in ${path.basename(folderPath)}...`, + cancellable: false, + }, + async () => { + const pythonFiles: string[] = []; + + function walk(dir: string): void { + try { + const entries = fs.readdirSync(dir); + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + walk(fullPath); + } else if (stat.isFile() && fullPath.endsWith('.py')) { + pythonFiles.push(fullPath); + } + } + } catch (error) { + ecoOutput.appendLine( + `[detection.ts] Scan error: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } } - } - } - walk(folderPath); + walk(folderPath); + ecoOutput.appendLine( + `[detection.ts] Found ${pythonFiles.length} files to analyze`, + ); - for (const file of pythonFiles) { - await detectSmellsFile(file, smellsViewProvider, smellsCacheManager); - } + if (pythonFiles.length === 0) { + vscode.window.showWarningMessage( + `No Python files found in ${path.basename(folderPath)}`, + ); + return; + } + + vscode.window.showInformationMessage( + `Analyzing ${pythonFiles.length} Python files...`, + ); + + for (const file of pythonFiles) { + await detectSmellsFile(file, smellsViewProvider, smellsCacheManager); + } + }, + ); } From 8534c34df1f08e2a04f11f49ee9db185708ab81f Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:16:02 -0400 Subject: [PATCH 077/121] clean refactorSmell.ts --- src/commands/refactorSmell.ts | 93 ++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index 9197d55..b604864 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -13,11 +13,13 @@ import { import { registerDiffEditor } from '../utils/trackedDiffEditors'; /** - * Handles the complete refactoring workflow for a detected code smell - * @param smellsViewProvider - Reference to the smells view provider - * @param refactoringDetailsViewProvider - Reference to the refactoring details provider - * @param smell - The smell object to refactor - * @param context - VS Code extension context + * Orchestrates the complete refactoring workflow including: + * - Pre-flight validation checks + * - Backend communication + * - UI updates and diff visualization + * - Success/error handling + * + * Shows carefully selected user notifications for key milestones and errors. */ export async function refactorSmell( smellsViewProvider: SmellsViewProvider, @@ -25,44 +27,53 @@ export async function refactorSmell( smell: Smell, context: vscode.ExtensionContext, ): Promise { - // Notify user about refactoring start - ecoOutput.appendLine(`Starting refactoring for smell: ${smell.symbol}`); - vscode.window.showInformationMessage(`Refactoring ${smell.symbol} smell...`); + // Log and notify refactoring initiation + ecoOutput.appendLine( + `[refactor.ts] Initiating refactoring for ${smell.symbol} in ${smell.path}`, + ); + vscode.window.showInformationMessage( + `Starting refactoring for ${smell.symbol}...`, + ); - // Verify workspace configuration + // Validate workspace configuration const workspacePath = context.workspaceState.get( 'workspaceConfiguredPath', ); if (!workspacePath) { - const errorMsg = 'No workspace configured. Please set up workspace first.'; + const errorMsg = '[refactor.ts] Refactoring aborted: No workspace configured'; ecoOutput.appendLine(errorMsg); - vscode.window.showErrorMessage(errorMsg); + vscode.window.showErrorMessage('Please configure workspace first'); return; } - // Check backend server status + // Verify backend availability if (serverStatus.getStatus() === ServerStatusType.DOWN) { - const warningMsg = - 'Server unavailable - cannot refactor without backend connection'; + const warningMsg = '[refactor.ts] Refactoring blocked: Backend unavailable'; ecoOutput.appendLine(warningMsg); - vscode.window.showWarningMessage(warningMsg); + vscode.window.showWarningMessage( + 'Cannot refactor - backend service unavailable', + ); smellsViewProvider.setStatus(smell.path, 'server_down'); return; } - // Begin refactoring process + // Update UI state smellsViewProvider.setStatus(smell.path, 'queued'); vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); try { - // Step 1: Send refactoring request to backend + // Execute backend refactoring + ecoOutput.appendLine( + `[refactor.ts] Sending refactoring request for ${smell.symbol}`, + ); const refactoredData = await backendRefactorSmell(smell, workspacePath); - ecoOutput.appendLine(`Refactoring completed for ${smell.path}`); + ecoOutput.appendLine( - `Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, + `[refactor.ts] Refactoring completed for ${path.basename(smell.path)}. ` + + `Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); - // Step 2: Update UI with refactoring results + // Update refactoring details view refactoringDetailsViewProvider.updateRefactoringDetails( smell, refactoredData.targetFile, @@ -70,42 +81,44 @@ export async function refactorSmell( refactoredData.energySaved, ); - // Step 3: Show diff editor comparison + // Show diff comparison const targetFile = refactoredData.targetFile; const fileName = path.basename(targetFile.original); - const originalUri = vscode.Uri.file(targetFile.original); - const refactoredUri = vscode.Uri.file(targetFile.refactored); await vscode.commands.executeCommand( 'vscode.diff', - originalUri, - refactoredUri, - `Refactoring Comparison (${fileName})`, - { - preview: false, // Ensure the diff editor is not in preview mode - }, + vscode.Uri.file(targetFile.original), + vscode.Uri.file(targetFile.refactored), + `Refactoring: ${fileName}`, + { preview: false }, + ); + registerDiffEditor( + vscode.Uri.file(targetFile.original), + vscode.Uri.file(targetFile.refactored), ); - registerDiffEditor(originalUri, refactoredUri); - // Step 4: Focus refactoring view and show action buttons + // Finalize UI updates await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); showRefactorActionButtons(); - // Step 5: Notify user of success - const successMsg = `Refactoring successful! Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`; - ecoOutput.appendLine(successMsg); + // Show completion notification + const successMsg = `Refactoring complete. Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`; + ecoOutput.appendLine(`[refactor.ts] ${successMsg}`); vscode.window.showInformationMessage(successMsg); } catch (error) { - // Handle refactoring failures - const errorMsg = `Refactoring failed: ${error instanceof Error ? error.message : String(error)}`; + // Handle errors and cleanup + const errorMsg = `[refactor.ts] Refactoring failed: ${error instanceof Error ? error.message : 'Unknown error'}`; ecoOutput.appendLine(errorMsg); - console.error('Refactoring error:', error); - vscode.window.showErrorMessage(errorMsg); + + vscode.window.showErrorMessage('Refactoring failed. See output for details.', { + modal: false, + }); // Reset UI state refactoringDetailsViewProvider.resetRefactoringDetails(); hideRefactorActionButtons(); - - // Update file status smellsViewProvider.setStatus(smell.path, 'failed'); + } finally { + // Ensure context is always reset + vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } From b15cbf382a2095683294443ad7d15f8d432b6e0d Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:20:14 -0400 Subject: [PATCH 078/121] clean --- src/commands/acceptRefactoring.ts | 1 - src/commands/filterSmells.ts | 1 + src/commands/rejectRefactoring.ts | 46 +++++++++++++++++++++++-------- src/commands/wipeWorkCache.ts | 5 ++-- src/extension.ts | 7 +---- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/commands/acceptRefactoring.ts b/src/commands/acceptRefactoring.ts index aee108d..8b382ff 100644 --- a/src/commands/acceptRefactoring.ts +++ b/src/commands/acceptRefactoring.ts @@ -35,7 +35,6 @@ export async function acceptRefactoring( metricsDataProvider: MetricsViewProvider, smellsCacheManager: SmellsCacheManager, smellsViewProvider: SmellsViewProvider, - context: vscode.ExtensionContext, ): Promise { const targetFile = refactoringDetailsViewProvider.targetFile; const affectedFiles = refactoringDetailsViewProvider.affectedFiles; diff --git a/src/commands/filterSmells.ts b/src/commands/filterSmells.ts index a3dd7d0..84e8a64 100644 --- a/src/commands/filterSmells.ts +++ b/src/commands/filterSmells.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; + import { FilterViewProvider } from '../providers/FilterViewProvider'; /** diff --git a/src/commands/rejectRefactoring.ts b/src/commands/rejectRefactoring.ts index 682151a..a2b12d3 100644 --- a/src/commands/rejectRefactoring.ts +++ b/src/commands/rejectRefactoring.ts @@ -1,28 +1,52 @@ import * as vscode from 'vscode'; + import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; import { closeAllTrackedDiffEditors } from '../utils/trackedDiffEditors'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { ecoOutput } from '../extension'; +/** + * Normalizes file paths for consistent comparison and caching + * @param filePath - The file path to normalize + * @returns Lowercase version of the path for case-insensitive operations + */ function normalizePath(filePath: string): string { return filePath.toLowerCase(); } +/** + * Handles rejection of proposed refactoring changes by: + * 1. Resetting UI components + * 2. Cleaning up diff editors + * 3. Restoring original file states + * 4. Providing user feedback + * + * Only shows a single notification to avoid interrupting workflow. + */ export async function rejectRefactoring( refactoringDetailsViewProvider: RefactoringDetailsViewProvider, smellsViewProvider: SmellsViewProvider, - context: vscode.ExtensionContext, ): Promise { - vscode.window.showInformationMessage('Refactoring rejected! Changes discarded.'); + ecoOutput.appendLine('[refactorActions.ts] User rejected refactoring changes'); + vscode.window.showInformationMessage('Refactoring changes discarded'); + + try { + // Restore original file status if target exists + if (refactoringDetailsViewProvider.targetFile?.original) { + const originalPath = refactoringDetailsViewProvider.targetFile.original; + smellsViewProvider.setStatus(normalizePath(originalPath), 'passed'); + ecoOutput.appendLine(`[refactorActions.ts] Reset status for ${originalPath}`); + } + + // Clean up UI components + await closeAllTrackedDiffEditors(); + refactoringDetailsViewProvider.resetRefactoringDetails(); + hideRefactorActionButtons(); - // Clear state + UI - if (refactoringDetailsViewProvider.targetFile?.original) { - smellsViewProvider.setStatus( - normalizePath(refactoringDetailsViewProvider.targetFile.original), - 'passed', - ); + ecoOutput.appendLine('[refactorActions.ts] Refactoring rejection completed'); + } catch (error) { + const errorMsg = `[refactorActions.ts] Error during rejection cleanup: ${error instanceof Error ? error.message : 'Unknown error'}`; + ecoOutput.appendLine(errorMsg); } - await closeAllTrackedDiffEditors(); - refactoringDetailsViewProvider.resetRefactoringDetails(); - hideRefactorActionButtons(); } diff --git a/src/commands/wipeWorkCache.ts b/src/commands/wipeWorkCache.ts index 79b9bab..7c3a16b 100644 --- a/src/commands/wipeWorkCache.ts +++ b/src/commands/wipeWorkCache.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; + import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; @@ -12,7 +13,7 @@ export async function wipeWorkCache( smellsViewProvider: SmellsViewProvider, ) { const userResponse = await vscode.window.showWarningMessage( - 'Are you sure you want to clear the smells cache? This action cannot be undone.', + 'Are you sure you want to clear the entire workspace analysis? This action cannot be undone.', { modal: true }, 'Confirm', ); @@ -22,7 +23,7 @@ export async function wipeWorkCache( smellsViewProvider.clearAllStatuses(); smellsViewProvider.refresh(); - vscode.window.showInformationMessage('Smells cache cleared successfully.'); + vscode.window.showInformationMessage('Workspace analysis cleared successfully.'); } else { vscode.window.showInformationMessage('Operation cancelled.'); } diff --git a/src/extension.ts b/src/extension.ts index 5890029..fc6c94c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -167,16 +167,11 @@ export function activate(context: vscode.ExtensionContext): void { metricsViewProvider, smellsCacheManager, smellsViewProvider, - context, ); }), vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', async () => { - await rejectRefactoring( - refactoringDetailsViewProvider, - smellsViewProvider, - context, - ); + await rejectRefactoring(refactoringDetailsViewProvider, smellsViewProvider); }), vscode.commands.registerCommand( From e378125a73a588d0f3968875e7380fe0d7610332 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:22:32 -0400 Subject: [PATCH 079/121] clean serverStatus.ts --- src/emitters/serverStatus.ts | 54 ++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/emitters/serverStatus.ts b/src/emitters/serverStatus.ts index 752f410..4b785da 100644 --- a/src/emitters/serverStatus.ts +++ b/src/emitters/serverStatus.ts @@ -1,35 +1,71 @@ import * as vscode from 'vscode'; import { EventEmitter } from 'events'; +import { ecoOutput } from '../extension'; +/** + * Represents possible server connection states + */ export enum ServerStatusType { - UNKNOWN = 'unknown', - UP = 'up', - DOWN = 'down', + UNKNOWN = 'unknown', // Initial state before first connection attempt + UP = 'up', // Server is available and responsive + DOWN = 'down', // Server is unreachable or unresponsive } +/** + * Tracks and manages backend server connection state with: + * - Status change detection + * - Appropriate user notifications + * - Event emission for dependent components + */ class ServerStatus extends EventEmitter { private status: ServerStatusType = ServerStatusType.UNKNOWN; + /** + * Gets current server connection state + * @returns Current ServerStatusType + */ getStatus(): ServerStatusType { return this.status; } + /** + * Updates server status with change detection and notifications + * @param newStatus - Either UP or DOWN status + */ setStatus(newStatus: ServerStatusType.UP | ServerStatusType.DOWN): void { if (this.status !== newStatus) { + const previousStatus = this.status; + this.status = newStatus; + + // Log status transition + ecoOutput.appendLine( + `[serverStatus.ts] Server status changed from ${previousStatus} to ${newStatus}`, + ); + + // Handle status-specific notifications if (newStatus === ServerStatusType.UP) { - if (this.status !== ServerStatusType.UNKNOWN) { + if (previousStatus !== ServerStatusType.UNKNOWN) { + ecoOutput.appendLine('[serverStatus.ts] Server connection re-established'); vscode.window.showInformationMessage( - 'Server connection re-established. Smell detection and refactoring functionality resumed.', + 'Backend server reconnected - full functionality restored', + { modal: false }, ); } } else { - vscode.window.showWarningMessage("Can't connect to ecooptimizer server."); + ecoOutput.appendLine('[serverStatus.ts] Server connection lost'); + vscode.window.showWarningMessage( + 'Backend server unavailable - limited functionality', + { modal: false }, + ); } - this.status = newStatus; - this.emit('change', newStatus); // Notify listeners + + // Notify subscribers + this.emit('change', newStatus); } } } -// Singleton instance +/** + * Singleton instance providing global server status management + */ export const serverStatus = new ServerStatus(); From 974c9351e4fe14f1bacc484a7d2dec244a4da6aa Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:28:31 -0400 Subject: [PATCH 080/121] clean workspaceModifiedListener.ts --- src/listeners/workspaceModifiedListener.ts | 145 ++++++++++++++++----- 1 file changed, 111 insertions(+), 34 deletions(-) diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index e8d88f3..1633555 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -1,11 +1,17 @@ import * as vscode from 'vscode'; +import path from 'path'; + import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; +import { ecoOutput } from '../extension'; /** - * Listens for workspace modifications (file creation, deletion, and changes) - * and refreshes the SmellsViewProvider and MetricsViewProvider accordingly. + * Monitors workspace modifications and maintains analysis state consistency by: + * - Tracking file system changes (create/change/delete) + * - Handling document save events + * - Managing cache invalidation + * - Coordinating view updates */ export class WorkspaceModifiedListener { private fileWatcher: vscode.FileSystemWatcher | undefined; @@ -19,78 +25,145 @@ export class WorkspaceModifiedListener { ) { this.initializeFileWatcher(); this.initializeSaveListener(); + ecoOutput.appendLine( + '[WorkspaceListener] Initialized workspace modification listener', + ); } /** - * Initializes the file system watcher for the configured workspace. + * Creates file system watcher for Python files in configured workspace */ private initializeFileWatcher(): void { const configuredPath = this.context.workspaceState.get( 'workspaceConfiguredPath', ); - if (!configuredPath) return; + if (!configuredPath) { + ecoOutput.appendLine( + '[WorkspaceListener] No workspace configured - skipping file watcher', + ); + return; + } - this.fileWatcher = vscode.workspace.createFileSystemWatcher( - new vscode.RelativePattern(configuredPath, '**/*.py'), - false, // Do not ignore create events - false, // Do not ignore change events - false, // Do not ignore delete events - ); + try { + this.fileWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(configuredPath, '**/*.py'), + false, // Watch create events + false, // Watch change events + false, // Watch delete events + ); - this.fileWatcher.onDidCreate(() => this.refreshViews()); - this.fileWatcher.onDidChange((uri) => this.handleFileChange(uri.fsPath)); - this.fileWatcher.onDidDelete((uri) => this.handleFileDeletion(uri.fsPath)); + this.fileWatcher.onDidCreate(() => { + ecoOutput.appendLine('[WorkspaceListener] Detected new Python file'); + this.refreshViews(); + }); + + this.fileWatcher.onDidChange((uri) => { + ecoOutput.appendLine( + `[WorkspaceListener] Detected changes in ${uri.fsPath}`, + ); + this.handleFileChange(uri.fsPath); + }); + + this.fileWatcher.onDidDelete((uri) => { + ecoOutput.appendLine( + `[WorkspaceListener] Detected deletion of ${uri.fsPath}`, + ); + this.handleFileDeletion(uri.fsPath); + }); + + ecoOutput.appendLine( + `[WorkspaceListener] Watching Python files in ${configuredPath}`, + ); + } catch (error) { + ecoOutput.appendLine( + `[WorkspaceListener] Error initializing file watcher: ${error instanceof Error ? error.message : String(error)}`, + ); + } } /** - * Initializes a listener for file save events to handle changes in Python files. + * Sets up document save listener for Python files */ private initializeSaveListener(): void { this.saveListener = vscode.workspace.onDidSaveTextDocument((document) => { if (document.languageId === 'python') { + ecoOutput.appendLine( + `[WorkspaceListener] Detected save in ${document.uri.fsPath}`, + ); this.handleFileChange(document.uri.fsPath); } }); } /** - * Handles file changes by clearing the cache for the modified file and marking it as outdated, - * but only if the file had previously been analyzed (i.e., is in the cache). - * @param filePath - The path of the modified file. + * Handles file modifications by: + * - Invalidating cached analysis if exists + * - Marking file as outdated in UI + * @param filePath - Absolute path to modified file */ private async handleFileChange(filePath: string): Promise { - const hadCache = this.smellsCacheManager.getAllFilePaths().includes(filePath); - if (!hadCache) return; // Skip if no analysis was done before + const hadCache = this.smellsCacheManager.hasCachedSmells(filePath); + if (!hadCache) { + ecoOutput.appendLine( + `[WorkspaceListener] No cache to invalidate for ${filePath}`, + ); + return; + } - await this.smellsCacheManager.clearCachedSmellsForFile(filePath); - this.smellsViewProvider.setStatus(filePath, 'outdated'); + try { + await this.smellsCacheManager.clearCachedSmellsForFile(filePath); + this.smellsViewProvider.setStatus(filePath, 'outdated'); - vscode.window.showInformationMessage( - `File modified: ${filePath}\nAnalysis data is now outdated.`, - ); + ecoOutput.appendLine( + `[WorkspaceListener] Invalidated cache for modified file: ${filePath}`, + ); + vscode.window.showInformationMessage( + `Analysis data marked outdated for ${path.basename(filePath)}`, + { modal: false }, + ); - this.refreshViews(); + this.refreshViews(); + } catch (error) { + ecoOutput.appendLine( + `[WorkspaceListener] Error handling file change: ${error instanceof Error ? error.message : String(error)}`, + ); + } } /** - * Handles file deletions by clearing the cache and removing the file from the tree view. - * @param filePath - The path of the deleted file. + * Handles file deletions by: + * - Clearing related cache entries + * - Removing from UI views + * @param filePath - Absolute path to deleted file */ private async handleFileDeletion(filePath: string): Promise { - const hadCache = this.smellsCacheManager.getAllFilePaths().includes(filePath); + const hadCache = this.smellsCacheManager.hasCachedSmells(filePath); let removed = false; if (hadCache) { - await this.smellsCacheManager.clearCachedSmellsByPath(filePath); - removed = true; + try { + await this.smellsCacheManager.clearCachedSmellsByPath(filePath); + removed = true; + ecoOutput.appendLine( + `[WorkspaceListener] Cleared cache for deleted file: ${filePath}`, + ); + } catch (error) { + ecoOutput.appendLine( + `[WorkspaceListener] Error clearing cache: ${error instanceof Error ? error.message : String(error)}`, + ); + } } const removedFromTree = this.smellsViewProvider.removeFile(filePath); - removed ||= removedFromTree; + if (removedFromTree) { + removed = true; + ecoOutput.appendLine(`[WorkspaceListener] Removed from view: ${filePath}`); + } if (removed) { vscode.window.showInformationMessage( - `Removed deleted file from analysis view: ${filePath}`, + `Removed analysis data for deleted file: ${path.basename(filePath)}`, + { modal: false }, ); } @@ -98,18 +171,22 @@ export class WorkspaceModifiedListener { } /** - * Refreshes both the SmellsViewProvider and MetricsViewProvider. + * Triggers refresh of all dependent views */ private refreshViews(): void { this.smellsViewProvider.refresh(); this.metricsViewProvider.refresh(); + ecoOutput.appendLine('[WorkspaceListener] Refreshed all views'); } /** - * Disposes of the file watcher and save listener to clean up resources. + * Cleans up resources including: + * - File system watcher + * - Document save listener */ public dispose(): void { this.fileWatcher?.dispose(); this.saveListener?.dispose(); + ecoOutput.appendLine('[WorkspaceListener] Disposed all listeners'); } } From 9fbaeca43dcca2cf5502b056ad29c1640a2439b6 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 12:47:55 -0400 Subject: [PATCH 081/121] clean --- src/commands/refactorSmell.ts | 2 +- src/providers/SmellsViewProvider.ts | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/commands/refactorSmell.ts b/src/commands/refactorSmell.ts index b604864..d1e3733 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactorSmell.ts @@ -88,7 +88,7 @@ export async function refactorSmell( 'vscode.diff', vscode.Uri.file(targetFile.original), vscode.Uri.file(targetFile.refactored), - `Refactoring: ${fileName}`, + `Refactoring Comparison (${fileName})`, { preview: false }, ); registerDiffEditor( diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index e7c5830..879220a 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; import { buildPythonTree } from '../utils/TreeStructureBuilder'; import { getAcronymByMessageId } from '../utils/smellsData'; -import { ecoOutput } from '../extension'; export class SmellsViewProvider implements vscode.TreeDataProvider @@ -66,13 +65,11 @@ export class SmellsViewProvider 'workspaceConfiguredPath', ); if (!rootPath) { - ecoOutput.appendLine('No workspace configured.'); return []; } // Smell nodes never have children if (element instanceof SmellTreeItem) { - ecoOutput.appendLine('SmellTreeItem has no children.'); return []; } @@ -81,7 +78,6 @@ export class SmellsViewProvider element?.contextValue === 'file' || element?.contextValue === 'file_with_smells' ) { - ecoOutput.appendLine(`Getting smells for file: ${element.fullPath}`); const smells = this.fileSmells.get(element.fullPath) ?? []; return smells.map((smell) => new SmellTreeItem(smell)); } @@ -90,10 +86,8 @@ export class SmellsViewProvider if (!element) { const stat = fs.statSync(rootPath); if (stat.isFile()) { - ecoOutput.appendLine(`Root is a file: ${rootPath}`); return [this.createTreeItem(rootPath, true)]; } else if (stat.isDirectory()) { - ecoOutput.appendLine(`Root is a directory: ${rootPath}`); return [this.createTreeItem(rootPath, false)]; // 👈 Show the root folder as the top node } } @@ -102,12 +96,7 @@ export class SmellsViewProvider const currentPath = element?.resourceUri?.fsPath; if (!currentPath) return []; - ecoOutput.appendLine(`Getting children of folder: ${currentPath}`); const childNodes = buildPythonTree(currentPath); - ecoOutput.appendLine(` Found ${childNodes.length} children.`); - childNodes.forEach((node) => - ecoOutput.appendLine(` - ${node.fullPath} (isFile: ${node.isFile})`), - ); return childNodes.map(({ fullPath, isFile }) => this.createTreeItem(fullPath, isFile), @@ -144,13 +133,6 @@ export class SmellsViewProvider item.description = 'outdated'; } - // ✅ Log the context value - ecoOutput.appendLine(`Created TreeItem: ${filePath}`); - ecoOutput.appendLine(` → Label: ${label}`); - ecoOutput.appendLine(` → isFile: ${isFile}`); - ecoOutput.appendLine(` → Context Value: ${item.contextValue}`); - ecoOutput.appendLine(` → Status: ${status}`); - return item; } } From cdad9f15a26bc2f8a13264974ffa8a82e523055a Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 13:41:41 -0400 Subject: [PATCH 082/121] refactorSmellsByType added --- src/api/backend.ts | 57 ++++++++++++- .../{refactorSmell.ts => refactor.ts} | 59 ++++++------- src/extension.ts | 84 +++++++++++++++++-- 3 files changed, 159 insertions(+), 41 deletions(-) rename src/commands/{refactorSmell.ts => refactor.ts} (68%) diff --git a/src/api/backend.ts b/src/api/backend.ts index e0e7fe0..de881ec 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -82,7 +82,7 @@ export async function fetchSmells( /** * Executes code refactoring for a specific detected smell pattern. * @param smell - The smell object containing detection details - * @param workspacePath - Root directory of the project workspace + * @param workspacePath - The path to the workspace. * @returns Promise resolving to refactoring result data * @throws Error when: * - Workspace path is not provided @@ -125,6 +125,61 @@ export async function backendRefactorSmell( ecoOutput.appendLine(`[backend.ts] Refactoring successful for ${smell.symbol}`); return result; + } catch (error: any) { + ecoOutput.appendLine(`[backend.ts] Refactoring error: ${error.message}`); + throw new Error(`Refactoring failed: ${error.message}`); + } +} + +/** + * Sends a request to the backend to refactor all smells of a type. + * + * @param smell - The smell to refactor. + * @param workspacePath - The path to the workspace. + * @returns A promise resolving to the refactored data or throwing an error if unsuccessful. + */ +export async function backendRefactorSmellType( + smell: Smell, + workspacePath: string +): Promise { + const url = `${BASE_URL}/refactor-by-type`; + const filePath = smell.path; + const smellType = smell.symbol; + + // Validate workspace configuration + if (!workspacePath) { + ecoOutput.appendLine('[backend.ts] Refactoring aborted: No workspace path'); + throw new Error('No workspace path provided'); + } + + ecoOutput.appendLine(`[backend.ts] Starting refactoring for smells of type "${smellType}" in "${filePath}"`); + + // Prepare the payload for the backend + const payload = { + sourceDir: workspacePath, + smellType, + firstSmell: smell, + }; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorData = await response.json(); + ecoOutput.appendLine(`[backend.ts] Refactoring failed: ${errorData.detail || 'Unknown error'}`); + throw new Error(errorData.detail || 'Refactoring failed'); + } + + const result = await response.json(); + ecoOutput.appendLine(`[backend.ts] Refactoring successful for ${smell.symbol}`); + return result; + } catch (error: any) { ecoOutput.appendLine(`[backend.ts] Refactoring error: ${error.message}`); throw new Error(`Refactoring failed: ${error.message}`); diff --git a/src/commands/refactorSmell.ts b/src/commands/refactor.ts similarity index 68% rename from src/commands/refactorSmell.ts rename to src/commands/refactor.ts index d1e3733..fef0b0d 100644 --- a/src/commands/refactorSmell.ts +++ b/src/commands/refactor.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { backendRefactorSmell } from '../api/backend'; +import { backendRefactorSmell, backendRefactorSmellType } from '../api/backend'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; import { ecoOutput } from '../extension'; @@ -13,43 +13,45 @@ import { import { registerDiffEditor } from '../utils/trackedDiffEditors'; /** - * Orchestrates the complete refactoring workflow including: - * - Pre-flight validation checks + * Orchestrates the complete refactoring workflow. + * If isRefactorAllOfType is true, it sends a request to refactor all smells of the same type. + * + * - Pre-flight validation checks * - Backend communication * - UI updates and diff visualization * - Success/error handling * * Shows carefully selected user notifications for key milestones and errors. */ -export async function refactorSmell( +export async function refactor( smellsViewProvider: SmellsViewProvider, refactoringDetailsViewProvider: RefactoringDetailsViewProvider, smell: Smell, context: vscode.ExtensionContext, + isRefactorAllOfType: boolean = false, ): Promise { // Log and notify refactoring initiation - ecoOutput.appendLine( - `[refactor.ts] Initiating refactoring for ${smell.symbol} in ${smell.path}`, - ); - vscode.window.showInformationMessage( - `Starting refactoring for ${smell.symbol}...`, - ); + const action = isRefactorAllOfType + ? 'Refactoring all smells of type' + : 'Refactoring'; + ecoOutput.appendLine(`[refactor.ts] ${action} ${smell.symbol} in ${smell.path}`); + vscode.window.showInformationMessage(`${action} ${smell.symbol}...`); // Validate workspace configuration const workspacePath = context.workspaceState.get( 'workspaceConfiguredPath', ); if (!workspacePath) { - const errorMsg = '[refactor.ts] Refactoring aborted: No workspace configured'; - ecoOutput.appendLine(errorMsg); + ecoOutput.appendLine( + '[refactor.ts] Refactoring aborted: No workspace configured', + ); vscode.window.showErrorMessage('Please configure workspace first'); return; } // Verify backend availability if (serverStatus.getStatus() === ServerStatusType.DOWN) { - const warningMsg = '[refactor.ts] Refactoring blocked: Backend unavailable'; - ecoOutput.appendLine(warningMsg); + ecoOutput.appendLine('[refactor.ts] Refactoring blocked: Backend unavailable'); vscode.window.showWarningMessage( 'Cannot refactor - backend service unavailable', ); @@ -63,10 +65,10 @@ export async function refactorSmell( try { // Execute backend refactoring - ecoOutput.appendLine( - `[refactor.ts] Sending refactoring request for ${smell.symbol}`, - ); - const refactoredData = await backendRefactorSmell(smell, workspacePath); + ecoOutput.appendLine(`[refactor.ts] Sending ${action} request...`); + const refactoredData = isRefactorAllOfType + ? await backendRefactorSmellType(smell, workspacePath) + : await backendRefactorSmell(smell, workspacePath); ecoOutput.appendLine( `[refactor.ts] Refactoring completed for ${path.basename(smell.path)}. ` + @@ -96,29 +98,22 @@ export async function refactorSmell( vscode.Uri.file(targetFile.refactored), ); - // Finalize UI updates await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); showRefactorActionButtons(); - // Show completion notification - const successMsg = `Refactoring complete. Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`; - ecoOutput.appendLine(`[refactor.ts] ${successMsg}`); - vscode.window.showInformationMessage(successMsg); + vscode.window.showInformationMessage( + `Refactoring complete. Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, + ); } catch (error) { - // Handle errors and cleanup - const errorMsg = `[refactor.ts] Refactoring failed: ${error instanceof Error ? error.message : 'Unknown error'}`; - ecoOutput.appendLine(errorMsg); - - vscode.window.showErrorMessage('Refactoring failed. See output for details.', { - modal: false, - }); + ecoOutput.appendLine( + `[refactor.ts] Refactoring failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + vscode.window.showErrorMessage('Refactoring failed. See output for details.'); - // Reset UI state refactoringDetailsViewProvider.resetRefactoringDetails(); hideRefactorActionButtons(); smellsViewProvider.setStatus(smell.path, 'failed'); } finally { - // Ensure context is always reset vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } diff --git a/src/extension.ts b/src/extension.ts index fc6c94c..9864784 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import path from 'path'; export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); // === Core Utilities === -import { loadSmells } from './utils/smellsData'; +import { getNameByMessageId, loadSmells } from './utils/smellsData'; import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; import { envConfig } from './utils/envConfig'; import { checkServerStatus } from './api/backend'; @@ -24,7 +24,7 @@ import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; import { registerFilterSmellCommands } from './commands/filterSmells'; import { jumpToSmell } from './commands/jumpToSmell'; import { wipeWorkCache } from './commands/wipeWorkCache'; -import { refactorSmell } from './commands/refactorSmell'; +import { refactor } from './commands/refactor'; import { acceptRefactoring } from './commands/acceptRefactoring'; import { rejectRefactoring } from './commands/rejectRefactoring'; import { exportMetricsData } from './commands/exportMetricsData'; @@ -43,7 +43,7 @@ export function activate(context: vscode.ExtensionContext): void { // === Start periodic backend status checks === checkServerStatus(); - setInterval(checkServerStatus, 300000); // 5 minutes + setInterval(checkServerStatus, 50000); // === Initialize Refactor Action Buttons === initializeRefactorActionButtons(context); @@ -152,12 +152,80 @@ export function activate(context: vscode.ExtensionContext): void { vscode.window.showErrorMessage('No code smell detected for this item.'); return; } - refactorSmell( - smellsViewProvider, - refactoringDetailsViewProvider, - smell, - context, + refactor(smellsViewProvider, refactoringDetailsViewProvider, smell, context); + }, + ), + + vscode.commands.registerCommand( + 'ecooptimizer.refactorAllSmellsOfType', + async (item: any) => { + const filePath = item?.fullPath; + if (!filePath) { + vscode.window.showWarningMessage( + 'Unable to get file path for smell refactoring.', + ); + return; + } + + const cachedSmells = smellsCacheManager.getCachedSmells(filePath); + if (!cachedSmells || cachedSmells.length === 0) { + vscode.window.showInformationMessage('No smells detected in this file.'); + return; + } + + ecoOutput.appendLine( + `🟡 Found ${cachedSmells.length} smells in ${filePath}`, ); + + const uniqueMessageIds = new Set(); + for (const smell of cachedSmells) { + uniqueMessageIds.add(smell.messageId); + } + + const quickPickItems: vscode.QuickPickItem[] = Array.from( + uniqueMessageIds, + ).map((id) => { + const name = getNameByMessageId(id) ?? id; + return { + label: name, + description: id, + }; + }); + + const selected = await vscode.window.showQuickPick(quickPickItems, { + title: 'Select a smell type to refactor', + placeHolder: 'Choose the type of smell you want to refactor', + matchOnDescription: false, + matchOnDetail: false, + ignoreFocusOut: false, + canPickMany: false, + }); + + if (selected) { + const selectedMessageId = selected.description; + const firstSmell = cachedSmells.find( + (smell) => smell.messageId === selectedMessageId, + ); + + if (!firstSmell) { + vscode.window.showWarningMessage( + 'No smells found for the selected type.', + ); + return; + } + + ecoOutput.appendLine( + `🔁 Triggering refactorAllSmellsOfType for: ${selectedMessageId}`, + ); + + await refactor( + smellsViewProvider, + refactoringDetailsViewProvider, + firstSmell, + context, + true, // isRefactorAllOfType + ); + } }, ), From 49ce6e05465e1250b50b23a03963463b154b264e Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 15:17:14 -0400 Subject: [PATCH 083/121] toggle linting --- assets/black_leaf.png | Bin 0 -> 979 bytes assets/green_leaf.png | Bin 0 -> 1288 bytes assets/white_leaf.png | Bin 0 -> 965 bytes package.json | 32 ++++++++++++++++++- src/context/SmellsCacheManager.ts | 18 +++++++++++ src/extension.ts | 35 +++++++++++++++++++++ src/listeners/workspaceModifiedListener.ts | 30 ++++++++++++------ 7 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 assets/black_leaf.png create mode 100644 assets/green_leaf.png create mode 100644 assets/white_leaf.png diff --git a/assets/black_leaf.png b/assets/black_leaf.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd8163192dfea2ffe4da78f84d9bfba15b4ecc0 GIT binary patch literal 979 zcmV;^11$WBP)%#6)TWVA(VF_ZGaD3+R#LXk#!Y4(Be zfqd}x0m<5$hA^SwgA`gJg_V-mY2_se*@vN+mzcM_#n?|LXLY-r=XuV3o_jO({69XO z>wo>P`L}68MZ6r+`jg zM*+|o7!OndAF;#6jdS0yJ3&eGVK0RvDJm1E5=b3Qq=B10Q0D zZU9PwJQ2UtFt((<#7_n4qlo?r90CfWZBXE}i2DL$w59OA!0{-;-vVX8Kuf-CU92B_ z+Dd#r@HB$(_rMw;&4IU6aD5AX7Jx!My%H_JVcRbrfx9>GPVjCDQ~Y8rRN@{mHl!To zg1Zq&3WIo_?$Hwalr;!LyaF&-TMIv?c<}%jW!PZNz_PfABj-CSa_x=R!!#Jg7Y5`> z0u})|7GsMWG z4baokl|B(%(__SJ6XSQe6mU?GR0JmZ%`;K(C~KW!(e?bOaY?LhOMXmnEZ~ITIcgD; zCt~Vd63g|U?!b|4=&K;lxo3#@%Ps{BGT7&t7ID=gM!DG00dtoLj_rX9&_p$IEdrTQ#;-6eovjXe zF9?pgt_9>8+UXr&K!g)lcAcBH8@Clemf`M^<+%dbZ1}3m+f4S28L2PwetfdA&lbz4 z&Wi`yjLCJ~iYxl6`9l2O;%K|Gbm8$iQ3G*`p|3~7dWRfIz-oRG98R!B`(tk7E(vQEYi#-C)0bA4| z7`kvFLfHZIW~(yUhz<>4zJB_4etAwHfdsrZ!!P2OvZew|IvxN3002ovPDHLkV1iNo B#fty{ literal 0 HcmV?d00001 diff --git a/assets/green_leaf.png b/assets/green_leaf.png new file mode 100644 index 0000000000000000000000000000000000000000..485924636ff3fd7e587ae7fb308f194c8be8e511 GIT binary patch literal 1288 zcmV+j1^4=iP)>veb)zyRVqG^XzSjYo7O1QB1BXK6%=U; zKB)bns6P~c`1+v|EG?DR&}3(J!)iehR1_7#DhSf}s0cRVYjbBdrUv2@AFa}=)Y?YQ z?#Avtd$aekH#LO}+&{}XbG|ue=9}-Fb(~q7wL{xBeC8d$ajf%X=TNcU@g>J{1+Hbq z4Xn701pXm~FZ)M2PIP9f8F1v|Di(Z=6#ihv3Hu{~*C!^N!PO}g9kl^-spz**bxX%RrzX0zaS#%l+u4ctwqJWa(_bl)t3*6t7Qi9Sy z(sA@b1Kv~49Yx|kz=FR=?CvNltSJh1HO^;&yPE3(S05DcdBtl;L8shl!M|AH*@6$< ztqPPC-v|Z^f08VoKW)H!%ehln;guFCvV#=XayfTK9Dl_Mo>vPtz)FQ_0bdYzDGPiv z1r`3rf)8>zH#ezYR#;>C+mrbzka)|?@rICYE8by% zHm3`e`+Z!J$r^&|tP}mVXRv!t+m6}Cn`<21lr{rK8aNqQUpaTSkrs_>6nyvUww#iW z7uf~I3VTW5?yP_}8VSB>okG!RN5jKjA0<-Ixv2%`zWIYW?i?@;o zptvn~?C#}h0wBd#t&MmgYEDtG%bKHU0tn=Panzfa)JIL@SiCOo(#UzE-aM;jeohm> zo<=8WIwA`^ZAp%g<4Sb4bW!2}Na62BCx(@h%r6Qqu<4KmZjCdS%elFB-6Rfx6@P4W zl2=%maDG7Jn=7W-**J4a7SFdeQJMe-EdY1Mol6zCv01Uk@7{%qdtvb8a5!-Qr1-Gq zWNWN>{=!ph-8PT{?+e^u znu#SSjp%lQe07&+5wiU7w%Z5k6NXnuH&kmIpk5R)~N74dyWYx zZq0De-zb-J9hm?(JC;{`$QE`DJijBXJChHWhCPDCDE`gnnLZQnRUnGgYOz+wTuqUf ze{zuEr!2VK$=d9zT|L)##^vUhCsrK6idUMynONZy^Y~k3F@dj1@og4-!Zbl=I?c#| yI(SoTr7#&Wqm2Y_sXl$T@$x)tvo>qawAsJ*r5OAzpqD50mo3D5NH&P^4L5;RWFZ zdEw~=5^a%(#)Le)kU}e@uu}3kDVrq3i;eOeGvuimyE;0z{{1@l{{QFP|DC(K-}=3| zo!|F-&-tJ8JKu9Iq>)CuM|Yq%?Fe0g8Ne=}7WfT31bULfP~sK?=Ya1(#1Q9!4x|t= zfQi6i;A;%g5fPh7ku|*%Xb31e;)y?c(EkPffNely0HJSy)4&?Tl6nGkX-naqfwjOF zAJO$d8Bi$lmmAuuiD(l{2cE_e{S!D26!~n>3nK3akef{5eSkA@gf{`@z<_{!`FgNk zjswXOKLL0dL-=Q49gyw7TPC=glkpXb_2-qsTY(c6Gj2jJ;FI9p5~6mNYN1m6BVb%e zeQX!puYk-ji0A7XEyW+w)*vhl*4Dz?$FxLAM;kU+3$TLZ;F9R;V4@zT-XLD>1skYk zuml(upzT7@-)j=UCd0B>V>S#-f|xB>p#bVBi~Thl@;+rj>!4o(Dy$t;w5o85swOvyA~EbB7BiGehCs)m25EFP!3B2 z=4KoAjWym0k^;+o>G2!%%mzWO!4p~Injl$bkvqq50NR}1BSh|F2mNdiBsBr^`s&lx zn|s}|?^z=MhJ$_v8SL}y0C`m+N4eNBmVK8Cjy*V{+OQS}#%aIGuyoW5oGq>jj(M&F zC@_@M2SEQAC$6kIbru^piaf*JL;dEAt%g}u-e#(&%_x15_qO>K&_4SuA7$(kLWW^o zN8_fxYQ7b}w>sMHJU#G)ov1{Xp{~b^jShWe0#^jj`!KJp8T!&Ky^V}YNAXn#E_FK# z9b*m8AJI6e2O5Q{SR(@*S~UhPCkyQdwJC4K5MLq0cLHwfDpskVUb1ynQa-6fTWwk8 zR6Gy+QLCFinN5#cs^qOU)$k>p00000NkvXXu0mjf@r=Np literal 0 HcmV?d00001 diff --git a/package.json b/package.json index da577d6..6b69d50 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "Tanveer Brar", "Ayushi Amin", "Mya Hussain", - "Nivetah Kuruparan" + "Nivetha Kuruparan" ], "description": "VS Code Plugin for EcoOptimizer Refactoring Tool", "version": "0.0.1", @@ -255,6 +255,24 @@ "title": "Refresh Metrics Data", "icon": "$(sync)", "category": "Eco" + }, + { + "command": "ecooptimizer.toggleSmellLintingOn", + "title": "Toggle Smell Linting", + "category": "Eco", + "icon": { + "light": "assets/green_leaf.png", + "dark": "assets/green_leaf.png" + } + }, + { + "command": "ecooptimizer.toggleSmellLintingOff", + "title": "Toggle Smell Linting", + "category": "Eco", + "icon": { + "light": "assets/white_leaf.png", + "dark": "assets/black_leaf.png" + } } ], "menus": { @@ -321,6 +339,18 @@ "when": "view == ecooptimizer.smellsView && viewItem == smell && !refactoringInProgress", "group": "inline" } + ], + "editor/title": [ + { + "command": "ecooptimizer.toggleSmellLintingOn", + "when": "workspaceState.workspaceConfigured && editorLangId == python && ecooptimizer.smellLintingEnabled", + "group": "navigation" + }, + { + "command": "ecooptimizer.toggleSmellLintingOff", + "when": "workspaceState.workspaceConfigured && editorLangId == python && !ecooptimizer.smellLintingEnabled", + "group": "navigation" + } ] }, "configuration": { diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index 07a5aef..0c0e3e7 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import { createHash } from 'crypto'; import { envConfig } from '../utils/envConfig'; +import { ecoOutput } from '../extension'; /** * Manages caching of detected smells to avoid redundant backend calls. @@ -147,4 +148,21 @@ export class SmellsCacheManager { ); return Object.values(map); } + + /** + * Checks if a file exists in the cache (by path) regardless of its current content hash + * @param filePath - The file path to check + * @returns true if the file has any cache entries (current or historical), false otherwise + */ + public hasFileInCache(filePath: string): boolean { + const pathMap = this.getHashToPathMap(); + + const fileExistsInCache = Object.values(pathMap).includes(filePath); + ecoOutput.appendLine( + `[SmellsCache] Path existence check for ${filePath}: ` + + `${fileExistsInCache ? 'EXISTS' : 'NOT FOUND'} in cache`, + ); + + return fileExistsInCache; + } } diff --git a/src/extension.ts b/src/extension.ts index 9864784..b376271 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,13 @@ import path from 'path'; // === Output Channel === export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); +// === Smell Linting === +let smellLintingEnabled = false; + +export function isSmellLintingEnabled(): boolean { + return smellLintingEnabled; +} + // === Core Utilities === import { getNameByMessageId, loadSmells } from './utils/smellsData'; import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; @@ -315,6 +322,34 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + function updateSmellLintingContext() { + vscode.commands.executeCommand( + 'setContext', + 'ecooptimizer.smellLintingEnabled', + smellLintingEnabled, + ); + } + + const toggleSmellLinting = () => { + smellLintingEnabled = !smellLintingEnabled; + updateSmellLintingContext(); + const msg = smellLintingEnabled + ? 'Smell linting enabled' + : 'Smell linting disabled'; + vscode.window.showInformationMessage(msg); + }; + + context.subscriptions.push( + vscode.commands.registerCommand( + 'ecooptimizer.toggleSmellLintingOn', + toggleSmellLinting, + ), + vscode.commands.registerCommand( + 'ecooptimizer.toggleSmellLintingOff', + toggleSmellLinting, + ), + ); + ecoOutput.appendLine('Eco-Optimizer extension activated successfully'); } diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index 1633555..291c023 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -4,7 +4,8 @@ import path from 'path'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; -import { ecoOutput } from '../extension'; +import { ecoOutput, isSmellLintingEnabled } from '../extension'; +import { detectSmellsFile } from '../commands/detectSmells'; /** * Monitors workspace modifications and maintains analysis state consistency by: @@ -57,13 +58,6 @@ export class WorkspaceModifiedListener { this.refreshViews(); }); - this.fileWatcher.onDidChange((uri) => { - ecoOutput.appendLine( - `[WorkspaceListener] Detected changes in ${uri.fsPath}`, - ); - this.handleFileChange(uri.fsPath); - }); - this.fileWatcher.onDidDelete((uri) => { ecoOutput.appendLine( `[WorkspaceListener] Detected deletion of ${uri.fsPath}`, @@ -91,6 +85,17 @@ export class WorkspaceModifiedListener { `[WorkspaceListener] Detected save in ${document.uri.fsPath}`, ); this.handleFileChange(document.uri.fsPath); + + if (isSmellLintingEnabled()) { + ecoOutput.appendLine( + `[WorkspaceListener] Smell linting is ON — auto-detecting smells for ${document.uri.fsPath}`, + ); + detectSmellsFile( + document.uri.fsPath, + this.smellsViewProvider, + this.smellsCacheManager, + ); + } } }); } @@ -102,7 +107,14 @@ export class WorkspaceModifiedListener { * @param filePath - Absolute path to modified file */ private async handleFileChange(filePath: string): Promise { - const hadCache = this.smellsCacheManager.hasCachedSmells(filePath); + // Log current cache state for debugging + const cachedFiles = this.smellsCacheManager.getAllFilePaths(); + ecoOutput.appendLine( + `[WorkspaceListener] Current cached files (${cachedFiles.length}):\n` + + cachedFiles.map((f) => ` - ${f}`).join('\n'), + ); + + const hadCache = this.smellsCacheManager.hasFileInCache(filePath); if (!hadCache) { ecoOutput.appendLine( `[WorkspaceListener] No cache to invalidate for ${filePath}`, From 5639c48a860f436fcc1d792d01482cb2fe904be7 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 15:56:49 -0400 Subject: [PATCH 084/121] cache bug fix --- src/context/SmellsCacheManager.ts | 98 ++++++++++++++---------- src/ui/LineSelection.ts | 6 -- src/utils/initializeStatusesFromCache.ts | 59 ++++++++++++-- 3 files changed, 110 insertions(+), 53 deletions(-) diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index 0c0e3e7..8636eb5 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -6,14 +6,22 @@ import { ecoOutput } from '../extension'; /** * Manages caching of detected smells to avoid redundant backend calls. - * This class handles storing, retrieving, and clearing cached smells. + * Uses workspace storage to persist cache between sessions. + * Implements file content hashing for change detection and maintains + * a bidirectional mapping between file paths and their content hashes. */ export class SmellsCacheManager { + // Event emitter for cache update notifications private cacheUpdatedEmitter = new vscode.EventEmitter(); public readonly onSmellsUpdated = this.cacheUpdatedEmitter.event; constructor(private context: vscode.ExtensionContext) {} + /** + * Generates a stable identifier for a smell based on its properties + * @param smell - The smell object to generate ID for + * @returns Short SHA-256 hash (first 5 chars) of the serialized smell + */ private generateSmellId(smell: Smell): string { return createHash('sha256') .update(JSON.stringify(smell)) @@ -21,16 +29,27 @@ export class SmellsCacheManager { .substring(0, 5); } + /** + * Generates content hash for a file to detect changes + * @param filePath - Absolute path to the file + * @returns SHA-256 hash of file content + */ private generateFileHash(filePath: string): string { const content = fs.readFileSync(filePath, 'utf-8'); return createHash('sha256').update(content).digest('hex'); } + /** + * Stores smells in cache for specified file + * @param filePath - File path to associate with smells + * @param smells - Array of smell objects to cache + */ public async setCachedSmells(filePath: string, smells: Smell[]): Promise { const cache = this.getFullSmellCache(); const pathMap = this.getHashToPathMap(); const fileHash = this.generateFileHash(filePath); + // Augment smells with stable identifiers const smellsWithIds = smells.map((smell) => ({ ...smell, id: this.generateSmellId(smell), @@ -45,27 +64,32 @@ export class SmellsCacheManager { this.cacheUpdatedEmitter.fire(filePath); } + /** + * Retrieves cached smells for a file + * @param filePath - File path to look up in cache + * @returns Array of smells or undefined if not found + */ public getCachedSmells(filePath: string): Smell[] | undefined { const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); return cache[fileHash]; } + /** + * Checks if smells exist in cache for a file + * @param filePath - File path to check + * @returns True if file has cached smells + */ public hasCachedSmells(filePath: string): boolean { const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); return cache[fileHash] !== undefined; } - public getSmellById(id: string): Smell | undefined { - const cache = this.getFullSmellCache(); - for (const hash in cache) { - const smell = cache[hash].find((s) => s.id === id); - if (smell) return smell; - } - return undefined; - } - + /** + * Clears cache for a file by its current content hash + * @param filePath - File path to clear from cache + */ public async clearCachedSmellsForFile(filePath: string): Promise { const fileHash = this.generateFileHash(filePath); const cache = this.getFullSmellCache(); @@ -80,6 +104,10 @@ export class SmellsCacheManager { this.cacheUpdatedEmitter.fire(filePath); } + /** + * Clears cache for a file by path (regardless of current content hash) + * @param filePath - File path to clear from cache + */ public async clearCachedSmellsByPath(filePath: string): Promise { const pathMap = this.getHashToPathMap(); const hash = Object.keys(pathMap).find((h) => pathMap[h] === filePath); @@ -95,6 +123,10 @@ export class SmellsCacheManager { this.cacheUpdatedEmitter.fire(filePath); } + /** + * Retrieves complete smell cache + * @returns Object mapping file hashes to smell arrays + */ public getFullSmellCache(): Record { return this.context.workspaceState.get>( envConfig.SMELL_CACHE_KEY!, @@ -102,6 +134,10 @@ export class SmellsCacheManager { ); } + /** + * Retrieves hash-to-path mapping + * @returns Object mapping file hashes to original paths + */ public getHashToPathMap(): Record { return this.context.workspaceState.get>( envConfig.HASH_PATH_MAP_KEY!, @@ -109,6 +145,9 @@ export class SmellsCacheManager { ); } + /** + * Clears entire smell cache + */ public async clearAllCachedSmells(): Promise { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, {}); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, {}); @@ -116,31 +155,10 @@ export class SmellsCacheManager { this.cacheUpdatedEmitter.fire('all'); } - public async reassociateCacheFromHash( - hash: string, - newPath: string, - ): Promise { - const cache = this.getFullSmellCache(); - const pathMap = this.getHashToPathMap(); - - if (cache[hash]) { - pathMap[hash] = newPath; - await this.context.workspaceState.update( - envConfig.HASH_PATH_MAP_KEY!, - pathMap, - ); - this.cacheUpdatedEmitter.fire(newPath); - return true; - } - - return false; - } - - public getPreviousFilePathForHash(hash: string): string | undefined { - const pathMap = this.getHashToPathMap(); - return pathMap[hash]; - } - + /** + * Retrieves all file paths currently in cache + * @returns Array of cached file paths + */ public getAllFilePaths(): string[] { const map = this.context.workspaceState.get>( envConfig.HASH_PATH_MAP_KEY!, @@ -150,16 +168,16 @@ export class SmellsCacheManager { } /** - * Checks if a file exists in the cache (by path) regardless of its current content hash - * @param filePath - The file path to check - * @returns true if the file has any cache entries (current or historical), false otherwise + * Checks if a file has any cache entries (current or historical) + * @param filePath - File path to check + * @returns True if file exists in cache metadata */ public hasFileInCache(filePath: string): boolean { const pathMap = this.getHashToPathMap(); - const fileExistsInCache = Object.values(pathMap).includes(filePath); + ecoOutput.appendLine( - `[SmellsCache] Path existence check for ${filePath}: ` + + `[SmellCacheManager] Path existence check for ${filePath}: ` + `${fileExistsInCache ? 'EXISTS' : 'NOT FOUND'} in cache`, ); diff --git a/src/ui/LineSelection.ts b/src/ui/LineSelection.ts index 5160d1a..5b1c1e2 100644 --- a/src/ui/LineSelection.ts +++ b/src/ui/LineSelection.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { ecoOutput } from '../extension'; /** * Manages line selection and decoration in a VS Code editor, specifically for @@ -20,9 +19,6 @@ export class LineSelectionManager { const activeEditor = vscode.window.activeTextEditor; if (activeEditor && activeEditor.document.uri.fsPath === targetFilePath) { - ecoOutput.appendLine( - `[LineSelect] Cache cleared for active file — removing comment`, - ); this.removeLastComment(); } }); @@ -33,7 +29,6 @@ export class LineSelectionManager { */ public removeLastComment(): void { if (this.decoration) { - ecoOutput.appendLine('[LineSelect] Removing decoration'); this.decoration.dispose(); this.decoration = null; } @@ -62,7 +57,6 @@ export class LineSelectionManager { this.removeLastComment(); this.lastDecoratedLine = selectedLine; - ecoOutput.appendLine(`[LineSelect] Decorating line ${selectedLine + 1}`); const smellsAtLine = smells.filter((smell) => smell.occurences.some((occ) => occ.line === selectedLine + 1), diff --git a/src/utils/initializeStatusesFromCache.ts b/src/utils/initializeStatusesFromCache.ts index f8bcb20..9093f0e 100644 --- a/src/utils/initializeStatusesFromCache.ts +++ b/src/utils/initializeStatusesFromCache.ts @@ -16,34 +16,79 @@ export async function initializeStatusesFromCache( const configuredPath = context.workspaceState.get( 'workspaceConfiguredPath', ); - if (!configuredPath) return; - const pathMap = smellsCacheManager.getAllFilePaths(); // Returns string[] + if (!configuredPath) { + ecoOutput.appendLine( + '[CacheInit] No configured workspace path found - skipping cache initialization', + ); + return; + } + + ecoOutput.appendLine( + `[CacheInit] Starting cache initialization for workspace: ${configuredPath}`, + ); + + const pathMap = smellsCacheManager.getAllFilePaths(); + ecoOutput.appendLine(`[CacheInit] Found ${pathMap.length} files in cache`); + ecoOutput.appendLine(`[CacheInit] Found ${pathMap} files in cache`); + let validFiles = 0; + let removedFiles = 0; + let filesWithSmells = 0; + let cleanFiles = 0; + for (const filePath of pathMap) { - // Ignore files outside the configured workspace or that don't exist anymore - ecoOutput.appendLine(`Checking file: ${filePath}`); + ecoOutput.appendLine(`[CacheInit] Processing cache entry: ${filePath}`); + + // Ignore files outside the configured workspace if (!filePath.startsWith(configuredPath)) { + ecoOutput.appendLine( + `[CacheInit] File outside workspace - removing from cache: ${filePath}`, + ); await smellsCacheManager.clearCachedSmellsForFile(filePath); + removedFiles++; continue; } + // Verify file still exists try { - await fs.access(filePath); // Throws if file doesn't exist + await fs.access(filePath); + ecoOutput.appendLine(`[CacheInit] File verified: ${filePath}`); } catch { + ecoOutput.appendLine( + `[CacheInit] File not found - removing from cache: ${filePath}`, + ); await smellsCacheManager.clearCachedSmellsForFile(filePath); + removedFiles++; continue; } const smells = smellsCacheManager.getCachedSmells(filePath); if (smells !== undefined) { + validFiles++; + if (smells.length > 0) { - // The file has one or more smells + ecoOutput.appendLine( + `[CacheInit] Found ${smells.length} smells for file: ${filePath}`, + ); smellsViewProvider.setStatus(filePath, 'passed'); smellsViewProvider.setSmells(filePath, smells); + filesWithSmells++; } else { - // The file was analyzed but has no smells + ecoOutput.appendLine(`[CacheInit] File has no smells: ${filePath}`); smellsViewProvider.setStatus(filePath, 'no_issues'); + cleanFiles++; } + } else { + ecoOutput.appendLine( + `[CacheInit] No cache data found for file (should not happen): ${filePath}`, + ); } } + + // Summary statistics + ecoOutput.appendLine( + `[CacheInit] Cache initialization complete. ` + + `Results: ${validFiles} valid files (${filesWithSmells} with smells, ${cleanFiles} clean), ` + + `${removedFiles} files removed from cache`, + ); } From 4fb0072990ce86bfd85c14c71b35ad6a63d61978 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Mon, 24 Mar 2025 16:17:42 -0400 Subject: [PATCH 085/121] commit --- assets/darkgreen_leaf.png | Bin 0 -> 1292 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/darkgreen_leaf.png diff --git a/assets/darkgreen_leaf.png b/assets/darkgreen_leaf.png new file mode 100644 index 0000000000000000000000000000000000000000..1b5d1ea29880db6754dfe0db9f908af0fe38bf82 GIT binary patch literal 1292 zcmV+n1@roeP)?`s8#C&#a3f-=j=8%TEQPiK}D#j2r4L| zf)8qcDC!TzAHII5ES46fT58vepaukSeI&Uv*VQUkEYd2}Cq7!GRjD;y zIhzf+JA1Qx@7u&OFX{W*ui<=H<|qC7*6S7Pf3S(K%G4Ykd9D;L%ke z_!KaHWCZ^Orq6r0|9EGviUnr`R~g3F4C9|hsG=7LzV1{ihdFabD!Bz&uXr&C6+viE z7--Y5@Uc0O(;23F4AYKiplNzBO<1S@7vMdg^q&Gus}0kyt%2eXM(8CFyuTr(7{BcK zs{g2i27LL};Uf*x10eKA3((sQ;~JN-FSy?@-PKSJOsfwHc<}K`VA|Fsr~d|~=Z#-j z(X2p$=?%5_ukO)&=d1zm8Xr2z2))we#CI8?wIBnhM$zZ`n6=KTh!ti9yz3V(0>N)X zTOs4`hVjAWrGZ6p^%}-CO4q*Bqu_dYy$x?jejfm#%~4N0OtR(T35IF6(wj>Sd`9pN z3pAwlZ3V^)(y(Eeo=`RPom0wnw5=GJZnix-rE3pT`c&XB}2TGe3w(NRRm$oCZ z4Ntz70){=hB#8rt@l_Sc7hCOd8T(WOwk0rJpa0o0TpU+NjpNkybxTEB?aPkgVpTK0 z#CE`Pk)VEq^xMP!<|Gvme7xaQnRaG;ynO1Lj8{0P_K**TeQy-{+%kQHJf|(Gk};Ln)6w`~ z@~KHbD)0xA&~B)nKJIOsi8d%MZCV&IyP0u;&?B*plyV)0@f{WEpVND?R&om_AKmKB z+K^uOY!dJg7G=hjPTG?33+FeCKc-ve3)a!8Z*9lwmVvWdURsQ!A-YD=2QT?$Ao$@7 zr@tWxzOP(7!Z3~5E@q=*M=zG+REe68y&1t9E!Eatu9o-Tq&n0(tz0~&b~A?zwP?b4 zbf4a19g12rrao=9FC8D~NdhQPP#qetodK;LYw3nbW zn_}retv&9uK*DImoE8wg#g4w)nLN+Syv&O;Yw{oO1lbyN70`zO0000 Date: Mon, 24 Mar 2025 16:53:38 -0400 Subject: [PATCH 086/121] hover manager --- src/extension.ts | 6 ++++ src/ui/hoverManager.ts | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/ui/hoverManager.ts diff --git a/src/extension.ts b/src/extension.ts index b376271..6a46e0c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -41,6 +41,7 @@ import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener import { LineSelectionManager } from './ui/LineSelection'; import { registerDiffEditor } from './utils/trackedDiffEditors'; import { initializeRefactorActionButtons } from './utils/refactorActionButtons'; +import { HoverManager } from './ui/hoverManager'; export function activate(context: vscode.ExtensionContext): void { ecoOutput.appendLine('Initializing Eco-Optimizer extension...'); @@ -322,6 +323,11 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + // == Hover Manager === + const hoverManager = new HoverManager(smellsCacheManager); + hoverManager.register(context); + + // === Smell Linting === function updateSmellLintingContext() { vscode.commands.executeCommand( 'setContext', diff --git a/src/ui/hoverManager.ts b/src/ui/hoverManager.ts new file mode 100644 index 0000000..1217258 --- /dev/null +++ b/src/ui/hoverManager.ts @@ -0,0 +1,62 @@ +import * as vscode from 'vscode'; +import { SmellsCacheManager } from '../context/SmellsCacheManager'; +import { getDescriptionByMessageId, getNameByMessageId } from '../utils/smellsData'; + +/** + * Displays smell information on hover when hovering over lines in Python files. + */ +export class HoverManager implements vscode.HoverProvider { + constructor(private smellsCacheManager: SmellsCacheManager) {} + + /** + * Registers the hover provider for Python files. + */ + public register(context: vscode.ExtensionContext): void { + const selector: vscode.DocumentSelector = { language: 'python', scheme: 'file' }; + const disposable = vscode.languages.registerHoverProvider(selector, this); + context.subscriptions.push(disposable); + } + + /** + * Provides hover content with stacked smell info. + */ + public provideHover( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken, + ): vscode.ProviderResult { + const filePath = document.uri.fsPath; + const smells = this.smellsCacheManager.getCachedSmells(filePath); + if (!smells || smells.length === 0) return; + + const lineNumber = position.line + 1; + + const smellsAtLine = smells.filter((smell) => + smell.occurences.some((occ) => occ.line === lineNumber), + ); + + if (smellsAtLine.length === 0) return; + + const wrap = (text: string, width = 50): string => + text.replace(new RegExp(`(.{1,${width}})(\\s+|$)`, 'g'), '$1\n').trim(); + + const hoverSections = smellsAtLine.map((smell) => { + const name = + getNameByMessageId(smell.messageId) ?? `Unknown Smell (${smell.messageId})`; + const description = + getDescriptionByMessageId(smell.messageId) ?? 'No description available.'; + const message = smell.message ?? 'No message provided.'; + + return [ + `🍂 **${name}**`, + `- \`${wrap(message)}\``, + `- _${wrap(description)}_`, + ].join('\n'); + }); + + const markdown = new vscode.MarkdownString(hoverSections.join('\n\n---\n\n')); + markdown.isTrusted = true; + + return new vscode.Hover(markdown); + } +} From ea65950913adaf37422fdb2869a4573046bf8373 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:10:26 -0400 Subject: [PATCH 087/121] Added back in missing functionality --- src/api/backend.ts | 81 ++++++-- src/commands/showLogs.ts | 187 ++++++++++++++++++ src/context/SmellsCacheManager.ts | 27 ++- src/extension.ts | 42 ++-- src/ui/hoverManager.ts | 2 +- ...neSelection.ts => lineSelectionManager.ts} | 0 6 files changed, 296 insertions(+), 43 deletions(-) create mode 100644 src/commands/showLogs.ts rename src/ui/{LineSelection.ts => lineSelectionManager.ts} (100%) diff --git a/src/api/backend.ts b/src/api/backend.ts index de881ec..5078a4a 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -16,24 +16,66 @@ const BASE_URL = `http://${envConfig.SERVER_URL}`; */ export async function checkServerStatus(): Promise { try { - ecoOutput.appendLine('[backend.ts] Checking backend server health status...'); + ecoOutput.trace('[backend.ts] Checking backend server health status...'); const response = await fetch(`${BASE_URL}/health`); if (response.ok) { serverStatus.setStatus(ServerStatusType.UP); - ecoOutput.appendLine('[backend.ts] Backend server is healthy'); + ecoOutput.trace('[backend.ts] Backend server is healthy'); } else { serverStatus.setStatus(ServerStatusType.DOWN); - ecoOutput.appendLine(`[backend.ts] Backend server unhealthy status: ${response.status}`); + ecoOutput.trace(`[backend.ts] Backend server unhealthy status: ${response.status}`); } } catch (error) { serverStatus.setStatus(ServerStatusType.DOWN); - ecoOutput.appendLine( + ecoOutput.error( `[backend.ts] Server connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } +/** + * Initializes and synchronizes logs with the backend server. + * + * This function sends a POST request to the backend to initialize logging + * for the specified log directory. If the request is successful, logging + * is initialized; otherwise, an error is logged, and an error message is + * displayed to the user. + * + * @param log_dir - The directory path where logs are stored. + * @returns A promise that resolves to `true` if logging is successfully initialized, + * or `false` if an error occurs. + */ +export async function initLogs(log_dir: string): Promise { + const url = `${BASE_URL}/logs/init`; + + try { + console.log('Initializing and synching logs with backend'); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ log_dir }), + }); + + if (!response.ok) { + console.error(`Unable to initialize logging: ${JSON.stringify(response)}`); + + return false; + } + + return true; + } catch (error: any) { + console.error(`Eco: Unable to initialize logging: ${error.message}`); + ecoOutput.warn( + 'Eco: Unable to reach the backend. Please check your connection.', + ); + return false; + } +} + /** * Analyzes source code for code smells using backend detection service. * @param filePath - Absolute path to the source file for analysis @@ -49,7 +91,7 @@ export async function fetchSmells( enabledSmells: Record>, ): Promise<{ smells: Smell[]; status: number }> { const url = `${BASE_URL}/smells`; - ecoOutput.appendLine(`[backend.ts] Starting smell detection for: ${path.basename(filePath)}`); + ecoOutput.info(`[backend.ts] Starting smell detection for: ${path.basename(filePath)}`); try { const response = await fetch(url, { @@ -65,16 +107,16 @@ export async function fetchSmells( if (!response.ok) { const errorMsg = `Backend request failed (${response.status})`; - ecoOutput.appendLine(`[backend.ts] ${errorMsg}`); + ecoOutput.error(`[backend.ts] ${errorMsg}`); throw new Error(errorMsg); } const smellsList = await response.json(); - ecoOutput.appendLine(`[backend.ts] Detected ${smellsList.length} smells in ${path.basename(filePath)}`); + ecoOutput.info(`[backend.ts] Detected ${smellsList.length} smells in ${path.basename(filePath)}`); return { smells: smellsList, status: response.status }; } catch (error: any) { - ecoOutput.appendLine(`[backend.ts] Smell detection failed: ${error.message}`); + ecoOutput.error(`[backend.ts] Smell detection failed: ${error.message}`); throw new Error(`Detection failed: ${error.message}`); } } @@ -97,11 +139,12 @@ export async function backendRefactorSmell( // Validate workspace configuration if (!workspacePath) { - ecoOutput.appendLine('[backend.ts] Refactoring aborted: No workspace path'); + ecoOutput.error('[backend.ts] Refactoring aborted: No workspace path'); throw new Error('No workspace path provided'); } - ecoOutput.appendLine(`[backend.ts] Starting refactoring for smell: ${smell.symbol}`); + ecoOutput.info(`[backend.ts] Starting refactoring for smell: ${smell.symbol}`); + console.log('Starting refactoring for smell:', smell); try { const response = await fetch(url, { @@ -110,23 +153,23 @@ export async function backendRefactorSmell( 'Content-Type': 'application/json', }, body: JSON.stringify({ - source_dir: workspacePath, + sourceDir: workspacePath, smell, }), }); if (!response.ok) { const errorData = await response.json(); - ecoOutput.appendLine(`[backend.ts] Refactoring failed: ${errorData.detail || 'Unknown error'}`); + ecoOutput.error(`[backend.ts] Refactoring failed: ${errorData.detail || 'Unknown error'}`); throw new Error(errorData.detail || 'Refactoring failed'); } const result = await response.json(); - ecoOutput.appendLine(`[backend.ts] Refactoring successful for ${smell.symbol}`); + ecoOutput.info(`[backend.ts] Refactoring successful for ${smell.symbol}`); return result; } catch (error: any) { - ecoOutput.appendLine(`[backend.ts] Refactoring error: ${error.message}`); + ecoOutput.error(`[backend.ts] Refactoring error: ${error.message}`); throw new Error(`Refactoring failed: ${error.message}`); } } @@ -148,11 +191,11 @@ export async function backendRefactorSmellType( // Validate workspace configuration if (!workspacePath) { - ecoOutput.appendLine('[backend.ts] Refactoring aborted: No workspace path'); + ecoOutput.error('[backend.ts] Refactoring aborted: No workspace path'); throw new Error('No workspace path provided'); } - ecoOutput.appendLine(`[backend.ts] Starting refactoring for smells of type "${smellType}" in "${filePath}"`); + ecoOutput.info(`[backend.ts] Starting refactoring for smells of type "${smellType}" in "${filePath}"`); // Prepare the payload for the backend const payload = { @@ -172,16 +215,16 @@ export async function backendRefactorSmellType( if (!response.ok) { const errorData = await response.json(); - ecoOutput.appendLine(`[backend.ts] Refactoring failed: ${errorData.detail || 'Unknown error'}`); + ecoOutput.error(`[backend.ts] Refactoring failed: ${errorData.detail || 'Unknown error'}`); throw new Error(errorData.detail || 'Refactoring failed'); } const result = await response.json(); - ecoOutput.appendLine(`[backend.ts] Refactoring successful for ${smell.symbol}`); + ecoOutput.info(`[backend.ts] Refactoring successful for ${smell.symbol}`); return result; } catch (error: any) { - ecoOutput.appendLine(`[backend.ts] Refactoring error: ${error.message}`); + ecoOutput.error(`[backend.ts] Refactoring error: ${error.message}`); throw new Error(`Refactoring failed: ${error.message}`); } } \ No newline at end of file diff --git a/src/commands/showLogs.ts b/src/commands/showLogs.ts new file mode 100644 index 0000000..368a82d --- /dev/null +++ b/src/commands/showLogs.ts @@ -0,0 +1,187 @@ +import * as vscode from 'vscode'; +import WebSocket from 'ws'; + +import { initLogs } from '../api/backend'; +import { envConfig } from '../utils/envConfig'; +import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; + +const WEBSOCKET_BASE_URL = `ws://${envConfig.SERVER_URL}/logs`; + +class LogInitializationError extends Error { + constructor(message: string) { + super(message); + this.name = 'LogInitializationError'; + } +} + +export class LogManager { + private websockets: { [key: string]: WebSocket | undefined }; + private channels: { + [key: string]: { name: string; channel: vscode.LogOutputChannel | undefined }; + }; + private channelsCreated: boolean; + private context: vscode.ExtensionContext; + + constructor(context: vscode.ExtensionContext) { + this.context = context; + this.websockets = { + main: undefined, + detect: undefined, + refactor: undefined, + }; + this.channels = { + main: { name: 'EcoOptimizer: Main', channel: undefined }, + detect: { name: 'EcoOptimizer: Detect', channel: undefined }, + refactor: { name: 'EcoOptimizer: Refactor', channel: undefined }, + }; + this.channelsCreated = false; + + // Listen for server status changes + serverStatus.on('change', async (newStatus: ServerStatusType) => { + console.log('Server status changed:', newStatus); + if (newStatus === ServerStatusType.DOWN) { + this.channels.main.channel?.appendLine('Server connection lost'); + } else { + this.channels.main.channel?.appendLine('Server connection re-established.'); + await this.startLogging(); + } + }); + } + + /** + * Starts the logging process, including initializing logs and WebSockets. + * @param retries - Number of retry attempts. + * @param delay - Initial delay between retries (in milliseconds). + */ + public async startLogging(retries = 3, delay = 1000): Promise { + let logInitialized = false; + const logPath = this.context.logUri?.fsPath; + + if (!logPath) { + throw new LogInitializationError( + 'Missing contextManager or logUri. Cannot initialize logging.', + ); + } + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + if (!logInitialized) { + logInitialized = await initLogs(logPath); + + if (!logInitialized) { + throw new LogInitializationError( + `Failed to initialize logs at path: ${logPath}`, + ); + } + console.log('Log initialization successful.'); + } + + this.initializeWebSockets(); + console.log('Successfully initialized WebSockets. Logging is now active.'); + return; + } catch (error) { + const err = error as Error; + console.error(`[Attempt ${attempt}/${retries}] ${err.name}: ${err.message}`); + + if (attempt < retries) { + console.log(`Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + delay *= 2; // Exponential backoff + } else { + throw new Error('Max retries reached. Logging process failed.'); + } + } + } + } + + /** + * Initializes WebSocket connections for logging. + */ + private initializeWebSockets(): void { + if (!this.channelsCreated) { + this.createOutputChannels(); + this.channelsCreated = true; + } + this.startWebSocket('main'); + this.startWebSocket('detect'); + this.startWebSocket('refactor'); + } + + /** + * Creates output channels for logging. + */ + private createOutputChannels(): void { + console.log('Creating output channels'); + for (const channel of Object.keys(this.channels)) { + this.channels[channel].channel = vscode.window.createOutputChannel( + this.channels[channel].name, + { log: true }, + ); + } + } + + /** + * Starts a WebSocket connection for a specific log type. + * @param logType - The type of log (e.g., 'main', 'detect', 'refactor'). + */ + private startWebSocket(logType: string): void { + const url = `${WEBSOCKET_BASE_URL}/${logType}`; + const ws = new WebSocket(url); + this.websockets[logType] = ws; + + ws.on('message', (data) => { + const logEvent = data.toString('utf8'); + const level = + logEvent.match(/\b(ERROR|DEBUG|INFO|WARNING|TRACE)\b/i)?.[0].trim() || + 'UNKNOWN'; + const msg = logEvent.split(`[${level}]`, 2)[1].trim(); + + switch (level) { + case 'ERROR': { + this.channels[logType].channel!.error(msg); + break; + } + case 'DEBUG': { + this.channels[logType].channel!.debug(msg); + break; + } + case 'WARNING': { + this.channels[logType].channel!.warn(msg); + break; + } + case 'CRITICAL': { + this.channels[logType].channel!.error(msg); + break; + } + default: { + this.channels[logType].channel!.info(msg); + break; + } + } + }); + + ws.on('error', (err) => { + this.channels[logType].channel!.error(`WebSocket error: ${err.message}`); + }); + + ws.on('close', () => { + this.channels[logType].channel!.appendLine( + `WebSocket connection closed for ${this.channels[logType].name}`, + ); + }); + + ws.on('open', () => { + this.channels[logType].channel!.appendLine( + `Connected to ${logType} via WebSocket`, + ); + }); + } + + /** + * Stops watching logs and cleans up resources. + */ + public stopWatchingLogs(): void { + Object.values(this.websockets).forEach((ws) => ws?.close()); + Object.values(this.channels).forEach((channel) => channel.channel?.dispose()); + } +} diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index 8636eb5..f4ec6b4 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -47,7 +47,9 @@ export class SmellsCacheManager { public async setCachedSmells(filePath: string, smells: Smell[]): Promise { const cache = this.getFullSmellCache(); const pathMap = this.getHashToPathMap(); - const fileHash = this.generateFileHash(filePath); + + const normalizedPath = vscode.Uri.file(filePath).fsPath; + const fileHash = this.generateFileHash(normalizedPath); // Augment smells with stable identifiers const smellsWithIds = smells.map((smell) => ({ @@ -56,7 +58,7 @@ export class SmellsCacheManager { })); cache[fileHash] = smellsWithIds; - pathMap[fileHash] = filePath; + pathMap[fileHash] = normalizedPath; await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); @@ -70,7 +72,8 @@ export class SmellsCacheManager { * @returns Array of smells or undefined if not found */ public getCachedSmells(filePath: string): Smell[] | undefined { - const fileHash = this.generateFileHash(filePath); + const normalizedPath = vscode.Uri.file(filePath).fsPath; + const fileHash = this.generateFileHash(normalizedPath); const cache = this.getFullSmellCache(); return cache[fileHash]; } @@ -81,7 +84,8 @@ export class SmellsCacheManager { * @returns True if file has cached smells */ public hasCachedSmells(filePath: string): boolean { - const fileHash = this.generateFileHash(filePath); + const normalizedPath = vscode.Uri.file(filePath).fsPath; + const fileHash = this.generateFileHash(normalizedPath); const cache = this.getFullSmellCache(); return cache[fileHash] !== undefined; } @@ -91,7 +95,8 @@ export class SmellsCacheManager { * @param filePath - File path to clear from cache */ public async clearCachedSmellsForFile(filePath: string): Promise { - const fileHash = this.generateFileHash(filePath); + const normalizedPath = vscode.Uri.file(filePath).fsPath; + const fileHash = this.generateFileHash(normalizedPath); const cache = this.getFullSmellCache(); const pathMap = this.getHashToPathMap(); @@ -101,7 +106,7 @@ export class SmellsCacheManager { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); - this.cacheUpdatedEmitter.fire(filePath); + this.cacheUpdatedEmitter.fire(normalizedPath); } /** @@ -110,7 +115,8 @@ export class SmellsCacheManager { */ public async clearCachedSmellsByPath(filePath: string): Promise { const pathMap = this.getHashToPathMap(); - const hash = Object.keys(pathMap).find((h) => pathMap[h] === filePath); + const normalizedPath = vscode.Uri.file(filePath).fsPath; + const hash = Object.keys(pathMap).find((h) => pathMap[h] === normalizedPath); if (!hash) return; const cache = this.getFullSmellCache(); @@ -120,7 +126,7 @@ export class SmellsCacheManager { await this.context.workspaceState.update(envConfig.SMELL_CACHE_KEY!, cache); await this.context.workspaceState.update(envConfig.HASH_PATH_MAP_KEY!, pathMap); - this.cacheUpdatedEmitter.fire(filePath); + this.cacheUpdatedEmitter.fire(normalizedPath); } /** @@ -174,10 +180,11 @@ export class SmellsCacheManager { */ public hasFileInCache(filePath: string): boolean { const pathMap = this.getHashToPathMap(); - const fileExistsInCache = Object.values(pathMap).includes(filePath); + const normalizedPath = vscode.Uri.file(filePath).fsPath; + const fileExistsInCache = Object.values(pathMap).includes(normalizedPath); ecoOutput.appendLine( - `[SmellCacheManager] Path existence check for ${filePath}: ` + + `[SmellCacheManager] Path existence check for ${normalizedPath}: ` + `${fileExistsInCache ? 'EXISTS' : 'NOT FOUND'} in cache`, ); diff --git a/src/extension.ts b/src/extension.ts index 6a46e0c..fff3a17 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,9 @@ import * as vscode from 'vscode'; import path from 'path'; // === Output Channel === -export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer'); +export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer', { + log: true, +}); // === Smell Linting === let smellLintingEnabled = false; @@ -38,13 +40,20 @@ import { exportMetricsData } from './commands/exportMetricsData'; // === Listeners & UI === import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; -import { LineSelectionManager } from './ui/LineSelection'; +import { FileHighlighter } from './ui/FileHighlighter'; +import { LineSelectionManager } from './ui/lineSelectionManager'; +import { HoverManager } from './ui/hoverManager'; import { registerDiffEditor } from './utils/trackedDiffEditors'; import { initializeRefactorActionButtons } from './utils/refactorActionButtons'; -import { HoverManager } from './ui/hoverManager'; +import { LogManager } from './commands/showLogs'; + +let backendLogManager: LogManager; export function activate(context: vscode.ExtensionContext): void { - ecoOutput.appendLine('Initializing Eco-Optimizer extension...'); + ecoOutput.info('Initializing Eco-Optimizer extension...'); + console.log('Initializing Eco-Optimizer extension...'); + + backendLogManager = new LogManager(context); // === Load Core Data === loadSmells(); @@ -181,9 +190,7 @@ export function activate(context: vscode.ExtensionContext): void { return; } - ecoOutput.appendLine( - `🟡 Found ${cachedSmells.length} smells in ${filePath}`, - ); + ecoOutput.info(`🟡 Found ${cachedSmells.length} smells in ${filePath}`); const uniqueMessageIds = new Set(); for (const smell of cachedSmells) { @@ -222,7 +229,7 @@ export function activate(context: vscode.ExtensionContext): void { return; } - ecoOutput.appendLine( + ecoOutput.info( `🔁 Triggering refactorAllSmellsOfType for: ${selectedMessageId}`, ); @@ -315,6 +322,11 @@ export function activate(context: vscode.ExtensionContext): void { ), ); + // === File Highlighting === + const fileHighlighter = FileHighlighter.getInstance(smellsCacheManager); + + fileHighlighter.updateHighlightsForVisibleEditors(); + // === Line Selection === const lineSelectManager = new LineSelectionManager(smellsCacheManager); context.subscriptions.push( @@ -328,15 +340,15 @@ export function activate(context: vscode.ExtensionContext): void { hoverManager.register(context); // === Smell Linting === - function updateSmellLintingContext() { + const updateSmellLintingContext = (): void => { vscode.commands.executeCommand( 'setContext', 'ecooptimizer.smellLintingEnabled', smellLintingEnabled, ); - } + }; - const toggleSmellLinting = () => { + const toggleSmellLinting = (): void => { smellLintingEnabled = !smellLintingEnabled; updateSmellLintingContext(); const msg = smellLintingEnabled @@ -356,9 +368,13 @@ export function activate(context: vscode.ExtensionContext): void { ), ); - ecoOutput.appendLine('Eco-Optimizer extension activated successfully'); + ecoOutput.info('Eco-Optimizer extension activated successfully'); + console.log('Eco-Optimizer extension activated successfully'); } export function deactivate(): void { - ecoOutput.appendLine('Extension deactivated'); + ecoOutput.info('Extension deactivated'); + console.log('Extension deactivated'); + + backendLogManager.stopWatchingLogs(); } diff --git a/src/ui/hoverManager.ts b/src/ui/hoverManager.ts index 1217258..2ee09e7 100644 --- a/src/ui/hoverManager.ts +++ b/src/ui/hoverManager.ts @@ -23,7 +23,7 @@ export class HoverManager implements vscode.HoverProvider { public provideHover( document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken, + _token: vscode.CancellationToken, ): vscode.ProviderResult { const filePath = document.uri.fsPath; const smells = this.smellsCacheManager.getCachedSmells(filePath); diff --git a/src/ui/LineSelection.ts b/src/ui/lineSelectionManager.ts similarity index 100% rename from src/ui/LineSelection.ts rename to src/ui/lineSelectionManager.ts From d56c5ea5cdad69af9bf4f0613de504124e9ae5cb Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:28:47 -0400 Subject: [PATCH 088/121] Added back in all functionality closes the following issues: - ssm-lab/capstone--source-code-optimizer#428 - ssm-lab/capstone--source-code-optimizer#369 - ssm-lab/capstone--source-code-optimizer#499 - ssm-lab/capstone--source-code-optimizer#500 - ssm-lab/capstone--source-code-optimizer#517 --- .env | 2 + ...smells.json => default_smells_config.json} | 0 data/working_smells_config.json | 101 +++++ package.json | 11 + src/commands/configureWorkspace.ts | 6 +- src/commands/{ => detection}/detectSmells.ts | 49 +-- src/commands/{ => detection}/wipeWorkCache.ts | 4 +- .../{ => refactor}/acceptRefactoring.ts | 65 ++- src/commands/{ => refactor}/refactor.ts | 107 +++-- .../{ => refactor}/rejectRefactoring.ts | 33 +- src/commands/resetConfiguration.ts | 6 +- src/commands/showLogs.ts | 2 +- src/commands/{ => views}/exportMetricsData.ts | 7 +- src/commands/{ => views}/filterSmells.ts | 10 +- src/commands/{ => views}/jumpToSmell.ts | 18 +- src/commands/{ => views}/openFile.ts | 0 src/context/SmellsCacheManager.ts | 15 +- src/emitters/serverStatus.ts | 6 +- src/extension.ts | 407 ++++++++++++++---- src/global.d.ts | 5 + src/listeners/workspaceModifiedListener.ts | 43 +- src/providers/FilterViewProvider.ts | 21 +- src/providers/MetricsViewProvider.ts | 201 +++------ src/providers/SmellsViewProvider.ts | 18 +- src/ui/FileHighlighter.ts | 7 + src/ui/hoverManager.ts | 46 +- src/utils/envConfig.ts | 4 + src/utils/initializeStatusesFromCache.ts | 33 +- src/utils/normalizePath.ts | 8 + src/utils/refactorActionButtons.ts | 12 +- src/utils/smellsData.ts | 18 +- src/utils/trackedDiffEditors.ts | 7 +- 32 files changed, 842 insertions(+), 430 deletions(-) rename data/{smells.json => default_smells_config.json} (100%) create mode 100644 data/working_smells_config.json rename src/commands/{ => detection}/detectSmells.ts (79%) rename src/commands/{ => detection}/wipeWorkCache.ts (86%) rename src/commands/{ => refactor}/acceptRefactoring.ts (63%) rename src/commands/{ => refactor}/refactor.ts (51%) rename src/commands/{ => refactor}/rejectRefactoring.ts (52%) rename src/commands/{ => views}/exportMetricsData.ts (91%) rename src/commands/{ => views}/filterSmells.ts (89%) rename src/commands/{ => views}/jumpToSmell.ts (61%) rename src/commands/{ => views}/openFile.ts (100%) create mode 100644 src/utils/normalizePath.ts diff --git a/.env b/.env index 7482d22..aee5d12 100644 --- a/.env +++ b/.env @@ -2,3 +2,5 @@ SERVER_URL='127.0.0.1:8000' HASH_PATH_MAP_KEY='hashPathMap' SMELL_CACHE_KEY='smellCache' WORKSPACE_METRICS_DATA='metricsData' +WORKSPACE_CONFIGURED_PATH='workspaceConfiguredPath' +UNFINISHED_REFACTORING='unfinishedRefactoring' diff --git a/data/smells.json b/data/default_smells_config.json similarity index 100% rename from data/smells.json rename to data/default_smells_config.json diff --git a/data/working_smells_config.json b/data/working_smells_config.json new file mode 100644 index 0000000..4c7e3df --- /dev/null +++ b/data/working_smells_config.json @@ -0,0 +1,101 @@ +{ + "use-a-generator": { + "message_id": "R1729", + "name": "Use A Generator (UGEN)", + "acronym": "UGEN", + "enabled": true, + "smell_description": "Using generators instead of lists reduces memory consumption and avoids unnecessary allocations, leading to more efficient CPU and energy use.", + "analyzer_options": {} + }, + "too-many-arguments": { + "message_id": "R0913", + "name": "Too Many Arguments (LPL)", + "acronym": "LPL", + "enabled": true, + "smell_description": "Functions with many arguments are harder to optimize and often require more memory and call overhead, increasing CPU load and energy usage.", + "analyzer_options": { + "max_args": { + "label": "Number of Arguments", + "description": "Detecting functions with this many arguments.", + "value": 6 + } + } + }, + "no-self-use": { + "message_id": "R6301", + "name": "No Self Use (NSU)", + "acronym": "NSU", + "enabled": true, + "smell_description": "Methods that don't use 'self' can be static, reducing object overhead and avoiding unnecessary memory binding at runtime.", + "analyzer_options": {} + }, + "long-lambda-expression": { + "message_id": "LLE001", + "name": "Long Lambda Expression (LLE)", + "acronym": "LLE", + "enabled": true, + "smell_description": "Complex lambdas are harder for the interpreter to optimize and may lead to repeated evaluations, which can increase CPU usage and energy draw.", + "analyzer_options": { + "threshold_length": { + "label": "Lambda Length", + "description": "Detects lambda expressions exceeding this length.", + "value": 9 + }, + "threshold_count": { + "label": "Repetition Count", + "description": "Flags patterns that repeat at least this many times.", + "value": 5 + } + } + }, + "long-message-chain": { + "message_id": "LMC001", + "name": "Long Message Chain (LMC)", + "acronym": "LMC", + "enabled": true, + "smell_description": "Deeply nested calls create performance bottlenecks due to increased dereferencing and lookup time, which adds to CPU cycles and energy usage.", + "analyzer_options": { + "threshold": { + "label": "Threshold", + "description": "Defines a threshold for triggering this smell.", + "value": 9 + } + } + }, + "long-element-chain": { + "message_id": "LEC001", + "name": "Long Element Chain (LEC)", + "acronym": "LEC", + "enabled": true, + "smell_description": "Chained element access can be inefficient in large structures, increasing access time and CPU effort, thereby consuming more energy.", + "analyzer_options": { + "threshold": { + "label": "Threshold", + "description": "Defines a threshold for triggering this smell.", + "value": 3 + } + } + }, + "cached-repeated-calls": { + "message_id": "CRC001", + "name": "Cached Repeated Calls (CRC)", + "acronym": "CRC", + "enabled": true, + "smell_description": "Failing to cache repeated expensive calls leads to redundant computation, which wastes CPU cycles and drains energy needlessly.", + "analyzer_options": { + "threshold": { + "label": "Cache Threshold", + "description": "Number of times a function must repeat before caching.", + "value": 2 + } + } + }, + "string-concat-loop": { + "message_id": "SCL001", + "name": "String Concatenation in Loops (SCL)", + "acronym": "SCL", + "enabled": true, + "smell_description": "String concatenation in loops creates new objects each time, increasing memory churn and CPU workload, which leads to higher energy consumption.", + "analyzer_options": {} + } +} diff --git a/package.json b/package.json index 21bebfa..5b22331 100644 --- a/package.json +++ b/package.json @@ -204,6 +204,12 @@ "title": "Deselect All Smells", "category": "Eco" }, + { + "command": "ecooptimizer.setFilterDefaults", + "title": "Set Filter Defaults", + "category": "Eco", + "when": "view == ecooptimizer.filterView && !refactoringInProgress" + }, { "command": "ecooptimizer.detectSmellsFolder", "title": "Detect Smells for All Files", @@ -297,6 +303,11 @@ "when": "view == ecooptimizer.filterView && !refactoringInProgress", "group": "resource" }, + { + "command": "ecooptimizer.setFilterDefaults", + "when": "view == ecooptimizer.filterView && !refactoringInProgress", + "group": "resource" + }, { "command": "ecooptimizer.exportMetricsData", "when": "view == ecooptimizer.metricsView", diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index 564a01f..2c0bc92 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -127,7 +127,11 @@ export async function updateWorkspace( workspacePath: string, ) { // Persist workspace path - await context.workspaceState.update('workspaceConfiguredPath', workspacePath); + await context.workspaceState.update( + envConfig, + WORKSPACE_CONFIGURED_PATH!, + workspacePath, + ); // Update extension context for UI state management vscode.commands.executeCommand( diff --git a/src/commands/detectSmells.ts b/src/commands/detection/detectSmells.ts similarity index 79% rename from src/commands/detectSmells.ts rename to src/commands/detection/detectSmells.ts index c41acaf..e530d2b 100644 --- a/src/commands/detectSmells.ts +++ b/src/commands/detection/detectSmells.ts @@ -2,12 +2,12 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { fetchSmells } from '../api/backend'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; -import { getEnabledSmells } from '../utils/smellsData'; -import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { ecoOutput } from '../extension'; +import { fetchSmells } from '../../api/backend'; +import { SmellsViewProvider } from '../../providers/SmellsViewProvider'; +import { getEnabledSmells } from '../../utils/smellsData'; +import { serverStatus, ServerStatusType } from '../../emitters/serverStatus'; +import { SmellsCacheManager } from '../../context/SmellsCacheManager'; +import { ecoOutput } from '../../extension'; /** * Performs code smell analysis on a single Python file with comprehensive state management. @@ -32,32 +32,29 @@ export async function detectSmellsFile( ); try { - ecoOutput.appendLine(`[detection.ts] Analyzing: ${path.basename(filePath)}`); + ecoOutput.info(`[detection.ts] Analyzing: ${path.basename(filePath)}`); const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); if (status === 200) { if (smells.length > 0) { - ecoOutput.appendLine(`[detection.ts] Detected ${smells.length} smells`); + ecoOutput.info(`[detection.ts] Detected ${smells.length} smells`); smellsViewProvider.setStatus(filePath, 'passed'); await smellsCacheManager.setCachedSmells(filePath, smells); smellsViewProvider.setSmells(filePath, smells); - vscode.window.showInformationMessage( - `Found ${smells.length} code smells in ${path.basename(filePath)}`, - ); } else { - ecoOutput.appendLine('[detection.ts] File has no detectable smells'); + ecoOutput.info('[detection.ts] File has no detectable smells'); smellsViewProvider.setStatus(filePath, 'no_issues'); await smellsCacheManager.setCachedSmells(filePath, []); } } else { const msg = `Analysis failed for ${path.basename(filePath)} (status ${status})`; - ecoOutput.appendLine(`[detection.ts] ${msg}`); + ecoOutput.error(`[detection.ts] ${msg}`); smellsViewProvider.setStatus(filePath, 'failed'); vscode.window.showErrorMessage(msg); } } catch (error: any) { const msg = `Analysis failed: ${error.message}`; - ecoOutput.appendLine(`[detection.ts] ${msg}`); + ecoOutput.error(`[detection.ts] ${msg}`); smellsViewProvider.setStatus(filePath, 'failed'); vscode.window.showErrorMessage(msg); } @@ -74,14 +71,20 @@ async function precheckAndMarkQueued( smellsViewProvider: SmellsViewProvider, smellsCacheManager: SmellsCacheManager, ): Promise { + const fileUri = vscode.Uri.file(filePath); + if (fileUri.scheme !== 'file') { + return false; + } + + if (!filePath.endsWith('.py')) { + return false; + } + if (smellsCacheManager.hasCachedSmells(filePath)) { const cached = smellsCacheManager.getCachedSmells(filePath); - ecoOutput.appendLine( + ecoOutput.info( `[detection.ts] Using cached results for ${path.basename(filePath)}`, ); - vscode.window.showInformationMessage( - `Using cached analysis for ${path.basename(filePath)}`, - ); if (cached && cached.length > 0) { smellsViewProvider.setStatus(filePath, 'passed'); @@ -94,7 +97,7 @@ async function precheckAndMarkQueued( if (serverStatus.getStatus() === ServerStatusType.DOWN) { const msg = 'Backend server unavailable - using cached results where available'; - ecoOutput.appendLine(`[detection.ts] ${msg}`); + ecoOutput.warn(`[detection.ts] ${msg}`); vscode.window.showWarningMessage(msg); smellsViewProvider.setStatus(filePath, 'server_down'); return false; @@ -103,7 +106,7 @@ async function precheckAndMarkQueued( const enabledSmells = getEnabledSmells(); if (Object.keys(enabledSmells).length === 0) { const msg = 'No smell detectors enabled in settings'; - ecoOutput.appendLine(`[detection.ts] ${msg}`); + ecoOutput.warn(`[detection.ts] ${msg}`); vscode.window.showWarningMessage(msg); return false; } @@ -144,16 +147,14 @@ export async function detectSmellsFolder( } } } catch (error) { - ecoOutput.appendLine( + ecoOutput.error( `[detection.ts] Scan error: ${error instanceof Error ? error.message : 'Unknown error'}`, ); } } walk(folderPath); - ecoOutput.appendLine( - `[detection.ts] Found ${pythonFiles.length} files to analyze`, - ); + ecoOutput.info(`[detection.ts] Found ${pythonFiles.length} files to analyze`); if (pythonFiles.length === 0) { vscode.window.showWarningMessage( diff --git a/src/commands/wipeWorkCache.ts b/src/commands/detection/wipeWorkCache.ts similarity index 86% rename from src/commands/wipeWorkCache.ts rename to src/commands/detection/wipeWorkCache.ts index 7c3a16b..80533fc 100644 --- a/src/commands/wipeWorkCache.ts +++ b/src/commands/detection/wipeWorkCache.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; +import { SmellsCacheManager } from '../../context/SmellsCacheManager'; +import { SmellsViewProvider } from '../../providers/SmellsViewProvider'; /** * Clears the smells cache and refreshes the UI. diff --git a/src/commands/acceptRefactoring.ts b/src/commands/refactor/acceptRefactoring.ts similarity index 63% rename from src/commands/acceptRefactoring.ts rename to src/commands/refactor/acceptRefactoring.ts index 8b382ff..d5f5042 100644 --- a/src/commands/acceptRefactoring.ts +++ b/src/commands/refactor/acceptRefactoring.ts @@ -1,20 +1,14 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; -import { MetricsViewProvider } from '../providers/MetricsViewProvider'; -import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; -import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { ecoOutput } from '../extension'; -import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; - -/** - * Normalizes file paths for consistent comparison and caching - * @param filePath - The file path to normalize - * @returns Lowercase version of the path for case-insensitive comparison - */ -function normalizePath(filePath: string): string { - return filePath.toLowerCase(); -} +import { SmellsViewProvider } from '../../providers/SmellsViewProvider'; +import { MetricsViewProvider } from '../../providers/MetricsViewProvider'; +import { RefactoringDetailsViewProvider } from '../../providers/RefactoringDetailsViewProvider'; +import { SmellsCacheManager } from '../../context/SmellsCacheManager'; +import { ecoOutput } from '../../extension'; +import { hideRefactorActionButtons } from '../../utils/refactorActionButtons'; +import { detectSmellsFile } from '../detection/detectSmells'; +import { closeAllTrackedDiffEditors } from '../../utils/trackedDiffEditors'; +import { envConfig } from '../../utils/envConfig'; /** * Handles acceptance and application of refactoring changes to the codebase. @@ -31,6 +25,7 @@ function normalizePath(filePath: string): string { * @param context - VS Code extension context */ export async function acceptRefactoring( + context: vscode.ExtensionContext, refactoringDetailsViewProvider: RefactoringDetailsViewProvider, metricsDataProvider: MetricsViewProvider, smellsCacheManager: SmellsCacheManager, @@ -41,15 +36,13 @@ export async function acceptRefactoring( // Validate refactoring data exists if (!targetFile || !affectedFiles) { - ecoOutput.appendLine( - '[refactorActions.ts] Error: No refactoring data available', - ); + ecoOutput.error('[refactorActions.ts] Error: No refactoring data available'); vscode.window.showErrorMessage('No refactoring data available.'); return; } try { - ecoOutput.appendLine( + ecoOutput.info( `[refactorActions.ts] Applying refactoring to target file: ${targetFile.original}`, ); @@ -57,9 +50,7 @@ export async function acceptRefactoring( fs.copyFileSync(targetFile.refactored, targetFile.original); affectedFiles.forEach((file) => { fs.copyFileSync(file.refactored, file.original); - ecoOutput.appendLine( - `[refactorActions.ts] Updated affected file: ${file.original}`, - ); + ecoOutput.info(`[refactorActions.ts] Updated affected file: ${file.original}`); }); // Update metrics if energy savings data exists @@ -72,41 +63,45 @@ export async function acceptRefactoring( refactoringDetailsViewProvider.energySaved, refactoringDetailsViewProvider.targetSmell.symbol, ); - ecoOutput.appendLine('[refactorActions.ts] Updated energy savings metrics'); + ecoOutput.info('[refactorActions.ts] Updated energy savings metrics'); } // Invalidate cache for modified files await Promise.all([ - smellsCacheManager.clearCachedSmellsForFile( - normalizePath(targetFile.original), - ), + smellsCacheManager.clearCachedSmellsForFile(targetFile.original), ...affectedFiles.map((file) => - smellsCacheManager.clearCachedSmellsForFile(normalizePath(file.original)), + smellsCacheManager.clearCachedSmellsForFile(file.original), ), ]); - ecoOutput.appendLine( - '[refactorActions.ts] Cleared smell caches for modified files', - ); + ecoOutput.trace('[refactorActions.ts] Cleared smell caches for modified files'); // Update UI state - smellsViewProvider.setStatus(normalizePath(targetFile.original), 'outdated'); + smellsViewProvider.setStatus(targetFile.original, 'outdated'); affectedFiles.forEach((file) => { - smellsViewProvider.setStatus(normalizePath(file.original), 'outdated'); + smellsViewProvider.setStatus(file.original, 'outdated'); }); + await detectSmellsFile( + targetFile.original, + smellsViewProvider, + smellsCacheManager, + ); + // Reset UI components refactoringDetailsViewProvider.resetRefactoringDetails(); - await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + closeAllTrackedDiffEditors(); hideRefactorActionButtons(); smellsViewProvider.refresh(); + context.workspaceState.update(envConfig.UNFINISHED_REFACTORING!, undefined); + vscode.window.showInformationMessage('Refactoring successfully applied'); - ecoOutput.appendLine( + ecoOutput.info( '[refactorActions.ts] Refactoring changes completed successfully', ); } catch (error) { const errorDetails = error instanceof Error ? error.message : 'Unknown error'; - ecoOutput.appendLine( + ecoOutput.error( `[refactorActions.ts] Error applying refactoring: ${errorDetails}`, ); vscode.window.showErrorMessage('Failed to apply refactoring. Please try again.'); diff --git a/src/commands/refactor.ts b/src/commands/refactor/refactor.ts similarity index 51% rename from src/commands/refactor.ts rename to src/commands/refactor/refactor.ts index fef0b0d..b6d984b 100644 --- a/src/commands/refactor.ts +++ b/src/commands/refactor/refactor.ts @@ -1,16 +1,18 @@ import * as vscode from 'vscode'; import * as path from 'path'; +import { promises } from 'fs'; -import { backendRefactorSmell, backendRefactorSmellType } from '../api/backend'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; -import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; -import { ecoOutput } from '../extension'; -import { serverStatus, ServerStatusType } from '../emitters/serverStatus'; +import { backendRefactorSmell, backendRefactorSmellType } from '../../api/backend'; +import { SmellsViewProvider } from '../../providers/SmellsViewProvider'; +import { RefactoringDetailsViewProvider } from '../../providers/RefactoringDetailsViewProvider'; +import { ecoOutput } from '../../extension'; +import { serverStatus, ServerStatusType } from '../../emitters/serverStatus'; import { showRefactorActionButtons, hideRefactorActionButtons, -} from '../utils/refactorActionButtons'; -import { registerDiffEditor } from '../utils/trackedDiffEditors'; +} from '../../utils/refactorActionButtons'; +import { registerDiffEditor } from '../../utils/trackedDiffEditors'; +import { envConfig } from '../../utils/envConfig'; /** * Orchestrates the complete refactoring workflow. @@ -34,24 +36,23 @@ export async function refactor( const action = isRefactorAllOfType ? 'Refactoring all smells of type' : 'Refactoring'; - ecoOutput.appendLine(`[refactor.ts] ${action} ${smell.symbol} in ${smell.path}`); + ecoOutput.info(`[refactor.ts] ${action} ${smell.symbol} in ${smell.path}`); vscode.window.showInformationMessage(`${action} ${smell.symbol}...`); // Validate workspace configuration const workspacePath = context.workspaceState.get( - 'workspaceConfiguredPath', + envConfig.WORKSPACE_CONFIGURED_PATH!, ); + if (!workspacePath) { - ecoOutput.appendLine( - '[refactor.ts] Refactoring aborted: No workspace configured', - ); + ecoOutput.error('[refactor.ts] Refactoring aborted: No workspace configured'); vscode.window.showErrorMessage('Please configure workspace first'); return; } // Verify backend availability if (serverStatus.getStatus() === ServerStatusType.DOWN) { - ecoOutput.appendLine('[refactor.ts] Refactoring blocked: Backend unavailable'); + ecoOutput.warn('[refactor.ts] Refactoring blocked: Backend unavailable'); vscode.window.showWarningMessage( 'Cannot refactor - backend service unavailable', ); @@ -65,47 +66,24 @@ export async function refactor( try { // Execute backend refactoring - ecoOutput.appendLine(`[refactor.ts] Sending ${action} request...`); + ecoOutput.trace(`[refactor.ts] Sending ${action} request...`); const refactoredData = isRefactorAllOfType ? await backendRefactorSmellType(smell, workspacePath) : await backendRefactorSmell(smell, workspacePath); - ecoOutput.appendLine( + ecoOutput.info( `[refactor.ts] Refactoring completed for ${path.basename(smell.path)}. ` + `Energy saved: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); - // Update refactoring details view - refactoringDetailsViewProvider.updateRefactoringDetails( + await context.workspaceState.update(envConfig.UNFINISHED_REFACTORING!, { + refactoredData, smell, - refactoredData.targetFile, - refactoredData.affectedFiles, - refactoredData.energySaved, - ); - - // Show diff comparison - const targetFile = refactoredData.targetFile; - const fileName = path.basename(targetFile.original); - await vscode.commands.executeCommand( - 'vscode.diff', - vscode.Uri.file(targetFile.original), - vscode.Uri.file(targetFile.refactored), - `Refactoring Comparison (${fileName})`, - { preview: false }, - ); - registerDiffEditor( - vscode.Uri.file(targetFile.original), - vscode.Uri.file(targetFile.refactored), - ); + }); - await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); - showRefactorActionButtons(); - - vscode.window.showInformationMessage( - `Refactoring complete. Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, - ); + startRefactorSession(smell, refactoredData, refactoringDetailsViewProvider); } catch (error) { - ecoOutput.appendLine( + ecoOutput.error( `[refactor.ts] Refactoring failed: ${error instanceof Error ? error.message : 'Unknown error'}`, ); vscode.window.showErrorMessage('Refactoring failed. See output for details.'); @@ -117,3 +95,46 @@ export async function refactor( vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); } } + +export async function startRefactorSession( + smell: Smell, + refactoredData: RefactoredData, + refactoringDetailsViewProvider: RefactoringDetailsViewProvider, +): Promise { + // Update refactoring details view + refactoringDetailsViewProvider.updateRefactoringDetails( + smell, + refactoredData.targetFile, + refactoredData.affectedFiles, + refactoredData.energySaved, + ); + + // Show diff comparison + const targetFile = refactoredData.targetFile; + const fileName = path.basename(targetFile.original); + await vscode.commands.executeCommand( + 'vscode.diff', + vscode.Uri.file(targetFile.original), + vscode.Uri.file(targetFile.refactored), + `Refactoring Comparison (${fileName})`, + { preview: false }, + ); + registerDiffEditor( + vscode.Uri.file(targetFile.original), + vscode.Uri.file(targetFile.refactored), + ); + + await vscode.commands.executeCommand('ecooptimizer.refactorView.focus'); + showRefactorActionButtons(); + + vscode.window.showInformationMessage( + `Refactoring complete. Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, + ); +} + +export async function cleanTemps(pastData: RefactoredData): Promise { + console.log('Cleaning up stale artifacts'); + const tempDirs = pastData.tempDir; + + await promises.rm(tempDirs, { recursive: true, force: true }); +} diff --git a/src/commands/rejectRefactoring.ts b/src/commands/refactor/rejectRefactoring.ts similarity index 52% rename from src/commands/rejectRefactoring.ts rename to src/commands/refactor/rejectRefactoring.ts index a2b12d3..da1e282 100644 --- a/src/commands/rejectRefactoring.ts +++ b/src/commands/refactor/rejectRefactoring.ts @@ -1,19 +1,11 @@ import * as vscode from 'vscode'; -import { RefactoringDetailsViewProvider } from '../providers/RefactoringDetailsViewProvider'; -import { hideRefactorActionButtons } from '../utils/refactorActionButtons'; -import { closeAllTrackedDiffEditors } from '../utils/trackedDiffEditors'; -import { SmellsViewProvider } from '../providers/SmellsViewProvider'; -import { ecoOutput } from '../extension'; - -/** - * Normalizes file paths for consistent comparison and caching - * @param filePath - The file path to normalize - * @returns Lowercase version of the path for case-insensitive operations - */ -function normalizePath(filePath: string): string { - return filePath.toLowerCase(); -} +import { RefactoringDetailsViewProvider } from '../../providers/RefactoringDetailsViewProvider'; +import { hideRefactorActionButtons } from '../../utils/refactorActionButtons'; +import { closeAllTrackedDiffEditors } from '../../utils/trackedDiffEditors'; +import { SmellsViewProvider } from '../../providers/SmellsViewProvider'; +import { ecoOutput } from '../../extension'; +import { envConfig } from '../../utils/envConfig'; /** * Handles rejection of proposed refactoring changes by: @@ -25,18 +17,19 @@ function normalizePath(filePath: string): string { * Only shows a single notification to avoid interrupting workflow. */ export async function rejectRefactoring( + context: vscode.ExtensionContext, refactoringDetailsViewProvider: RefactoringDetailsViewProvider, smellsViewProvider: SmellsViewProvider, ): Promise { - ecoOutput.appendLine('[refactorActions.ts] User rejected refactoring changes'); + ecoOutput.info('[refactorActions.ts] Refactoring changes discarded'); vscode.window.showInformationMessage('Refactoring changes discarded'); try { // Restore original file status if target exists if (refactoringDetailsViewProvider.targetFile?.original) { const originalPath = refactoringDetailsViewProvider.targetFile.original; - smellsViewProvider.setStatus(normalizePath(originalPath), 'passed'); - ecoOutput.appendLine(`[refactorActions.ts] Reset status for ${originalPath}`); + smellsViewProvider.setStatus(originalPath, 'passed'); + ecoOutput.trace(`[refactorActions.ts] Reset status for ${originalPath}`); } // Clean up UI components @@ -44,9 +37,11 @@ export async function rejectRefactoring( refactoringDetailsViewProvider.resetRefactoringDetails(); hideRefactorActionButtons(); - ecoOutput.appendLine('[refactorActions.ts] Refactoring rejection completed'); + context.workspaceState.update(envConfig.UNFINISHED_REFACTORING!, undefined); + + ecoOutput.trace('[refactorActions.ts] Refactoring rejection completed'); } catch (error) { const errorMsg = `[refactorActions.ts] Error during rejection cleanup: ${error instanceof Error ? error.message : 'Unknown error'}`; - ecoOutput.appendLine(errorMsg); + ecoOutput.error(errorMsg); } } diff --git a/src/commands/resetConfiguration.ts b/src/commands/resetConfiguration.ts index 49262cc..bd3e004 100644 --- a/src/commands/resetConfiguration.ts +++ b/src/commands/resetConfiguration.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { envConfig } from '../utils/envConfig'; /** * Resets the workspace configuration by clearing the selected workspace path. @@ -16,7 +17,10 @@ export async function resetConfiguration( ); if (confirm === 'Reset') { - await context.workspaceState.update('workspaceConfiguredPath', undefined); + await context.workspaceState.update( + envConfig.WORKSPACE_CONFIGURED_PATH!, + undefined, + ); vscode.commands.executeCommand( 'setContext', diff --git a/src/commands/showLogs.ts b/src/commands/showLogs.ts index 368a82d..c4f0441 100644 --- a/src/commands/showLogs.ts +++ b/src/commands/showLogs.ts @@ -59,7 +59,7 @@ export class LogManager { if (!logPath) { throw new LogInitializationError( - 'Missing contextManager or logUri. Cannot initialize logging.', + 'Missing extension context or logUri. Cannot initialize logging.', ); } diff --git a/src/commands/exportMetricsData.ts b/src/commands/views/exportMetricsData.ts similarity index 91% rename from src/commands/exportMetricsData.ts rename to src/commands/views/exportMetricsData.ts index 3a1b91f..0859a82 100644 --- a/src/commands/exportMetricsData.ts +++ b/src/commands/views/exportMetricsData.ts @@ -2,8 +2,8 @@ import * as vscode from 'vscode'; import { dirname } from 'path'; import { writeFileSync } from 'fs'; -import { MetricsDataItem } from '../providers/MetricsViewProvider'; -import { envConfig } from '../utils/envConfig'; +import { MetricsDataItem } from '../../providers/MetricsViewProvider'; +import { envConfig } from '../../utils/envConfig'; export async function exportMetricsData( context: vscode.ExtensionContext, @@ -18,7 +18,8 @@ export async function exportMetricsData( } const configuredWorkspacePath = context.workspaceState.get( - 'workspaceConfiguredPath', + envConfig, + WORKSPACE_CONFIGURED_PATH!, ); if (!configuredWorkspacePath) { diff --git a/src/commands/filterSmells.ts b/src/commands/views/filterSmells.ts similarity index 89% rename from src/commands/filterSmells.ts rename to src/commands/views/filterSmells.ts index 84e8a64..7a97c8f 100644 --- a/src/commands/filterSmells.ts +++ b/src/commands/views/filterSmells.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { FilterViewProvider } from '../providers/FilterViewProvider'; +import { FilterViewProvider } from '../../providers/FilterViewProvider'; /** * Registers VS Code commands for managing smell filters. @@ -10,7 +10,7 @@ import { FilterViewProvider } from '../providers/FilterViewProvider'; export function registerFilterSmellCommands( context: vscode.ExtensionContext, filterSmellsProvider: FilterViewProvider, -) { +): void { /** * Toggles the state of a specific smell filter. */ @@ -70,4 +70,10 @@ export function registerFilterSmellCommands( filterSmellsProvider.setAllSmellsEnabled(false); }), ); + + context.subscriptions.push( + vscode.commands.registerCommand('ecooptimizer.setFilterDefaults', () => { + filterSmellsProvider.resetToDefaults(); + }), + ); } diff --git a/src/commands/jumpToSmell.ts b/src/commands/views/jumpToSmell.ts similarity index 61% rename from src/commands/jumpToSmell.ts rename to src/commands/views/jumpToSmell.ts index 94fb8ff..7946047 100644 --- a/src/commands/jumpToSmell.ts +++ b/src/commands/views/jumpToSmell.ts @@ -13,10 +13,20 @@ export async function jumpToSmell(filePath: string, line: number): Promise // Move cursor to the specified line const position = new vscode.Position(line, 0); editor.selection = new vscode.Selection(position, position); - editor.revealRange( - new vscode.Range(position, position), - vscode.TextEditorRevealType.InCenter, - ); + + const range = new vscode.Range(position, position); + editor.revealRange(range, vscode.TextEditorRevealType.InCenter); + + const flashDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: new vscode.ThemeColor('editor.wordHighlightBackground'), + isWholeLine: true, + }); + + editor.setDecorations(flashDecorationType, [range]); + + setTimeout(() => { + editor.setDecorations(flashDecorationType, []); + }, 500); } catch (error: any) { vscode.window.showErrorMessage( `Failed to jump to smell in ${filePath}: ${error.message}`, diff --git a/src/commands/openFile.ts b/src/commands/views/openFile.ts similarity index 100% rename from src/commands/openFile.ts rename to src/commands/views/openFile.ts diff --git a/src/context/SmellsCacheManager.ts b/src/context/SmellsCacheManager.ts index f4ec6b4..4eb69e4 100644 --- a/src/context/SmellsCacheManager.ts +++ b/src/context/SmellsCacheManager.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import { createHash } from 'crypto'; import { envConfig } from '../utils/envConfig'; import { ecoOutput } from '../extension'; +import { normalizePath } from '../utils/normalizePath'; /** * Manages caching of detected smells to avoid redundant backend calls. @@ -48,7 +49,7 @@ export class SmellsCacheManager { const cache = this.getFullSmellCache(); const pathMap = this.getHashToPathMap(); - const normalizedPath = vscode.Uri.file(filePath).fsPath; + const normalizedPath = normalizePath(filePath); const fileHash = this.generateFileHash(normalizedPath); // Augment smells with stable identifiers @@ -72,7 +73,7 @@ export class SmellsCacheManager { * @returns Array of smells or undefined if not found */ public getCachedSmells(filePath: string): Smell[] | undefined { - const normalizedPath = vscode.Uri.file(filePath).fsPath; + const normalizedPath = normalizePath(filePath); const fileHash = this.generateFileHash(normalizedPath); const cache = this.getFullSmellCache(); return cache[fileHash]; @@ -84,7 +85,7 @@ export class SmellsCacheManager { * @returns True if file has cached smells */ public hasCachedSmells(filePath: string): boolean { - const normalizedPath = vscode.Uri.file(filePath).fsPath; + const normalizedPath = normalizePath(filePath); const fileHash = this.generateFileHash(normalizedPath); const cache = this.getFullSmellCache(); return cache[fileHash] !== undefined; @@ -95,7 +96,7 @@ export class SmellsCacheManager { * @param filePath - File path to clear from cache */ public async clearCachedSmellsForFile(filePath: string): Promise { - const normalizedPath = vscode.Uri.file(filePath).fsPath; + const normalizedPath = normalizePath(filePath); const fileHash = this.generateFileHash(normalizedPath); const cache = this.getFullSmellCache(); const pathMap = this.getHashToPathMap(); @@ -115,7 +116,7 @@ export class SmellsCacheManager { */ public async clearCachedSmellsByPath(filePath: string): Promise { const pathMap = this.getHashToPathMap(); - const normalizedPath = vscode.Uri.file(filePath).fsPath; + const normalizedPath = normalizePath(filePath); const hash = Object.keys(pathMap).find((h) => pathMap[h] === normalizedPath); if (!hash) return; @@ -180,10 +181,10 @@ export class SmellsCacheManager { */ public hasFileInCache(filePath: string): boolean { const pathMap = this.getHashToPathMap(); - const normalizedPath = vscode.Uri.file(filePath).fsPath; + const normalizedPath = normalizePath(filePath); const fileExistsInCache = Object.values(pathMap).includes(normalizedPath); - ecoOutput.appendLine( + ecoOutput.debug( `[SmellCacheManager] Path existence check for ${normalizedPath}: ` + `${fileExistsInCache ? 'EXISTS' : 'NOT FOUND'} in cache`, ); diff --git a/src/emitters/serverStatus.ts b/src/emitters/serverStatus.ts index 4b785da..60bd515 100644 --- a/src/emitters/serverStatus.ts +++ b/src/emitters/serverStatus.ts @@ -38,21 +38,21 @@ class ServerStatus extends EventEmitter { this.status = newStatus; // Log status transition - ecoOutput.appendLine( + ecoOutput.trace( `[serverStatus.ts] Server status changed from ${previousStatus} to ${newStatus}`, ); // Handle status-specific notifications if (newStatus === ServerStatusType.UP) { if (previousStatus !== ServerStatusType.UNKNOWN) { - ecoOutput.appendLine('[serverStatus.ts] Server connection re-established'); + ecoOutput.info('[serverStatus.ts] Server connection re-established'); vscode.window.showInformationMessage( 'Backend server reconnected - full functionality restored', { modal: false }, ); } } else { - ecoOutput.appendLine('[serverStatus.ts] Server connection lost'); + ecoOutput.info('[serverStatus.ts] Server connection lost'); vscode.window.showWarningMessage( 'Backend server unavailable - limited functionality', { modal: false }, diff --git a/src/extension.ts b/src/extension.ts index fff3a17..ad896ad 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,14 +14,18 @@ export function isSmellLintingEnabled(): boolean { } // === Core Utilities === +import { envConfig } from './utils/envConfig'; import { getNameByMessageId, loadSmells } from './utils/smellsData'; import { initializeStatusesFromCache } from './utils/initializeStatusesFromCache'; -import { envConfig } from './utils/envConfig'; import { checkServerStatus } from './api/backend'; // === Context & View Providers === import { SmellsCacheManager } from './context/SmellsCacheManager'; -import { SmellsViewProvider, SmellTreeItem } from './providers/SmellsViewProvider'; +import { + SmellsViewProvider, + SmellTreeItem, + TreeItem, +} from './providers/SmellsViewProvider'; import { MetricsViewProvider } from './providers/MetricsViewProvider'; import { FilterViewProvider } from './providers/FilterViewProvider'; import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsViewProvider'; @@ -29,23 +33,31 @@ import { RefactoringDetailsViewProvider } from './providers/RefactoringDetailsVi // === Commands === import { configureWorkspace } from './commands/configureWorkspace'; import { resetConfiguration } from './commands/resetConfiguration'; -import { detectSmellsFile, detectSmellsFolder } from './commands/detectSmells'; -import { registerFilterSmellCommands } from './commands/filterSmells'; -import { jumpToSmell } from './commands/jumpToSmell'; -import { wipeWorkCache } from './commands/wipeWorkCache'; -import { refactor } from './commands/refactor'; -import { acceptRefactoring } from './commands/acceptRefactoring'; -import { rejectRefactoring } from './commands/rejectRefactoring'; -import { exportMetricsData } from './commands/exportMetricsData'; +import { + detectSmellsFile, + detectSmellsFolder, +} from './commands/detection/detectSmells'; +import { registerFilterSmellCommands } from './commands/views/filterSmells'; +import { jumpToSmell } from './commands/views/jumpToSmell'; +import { wipeWorkCache } from './commands/detection/wipeWorkCache'; +import { refactor, startRefactorSession } from './commands/refactor/refactor'; +import { acceptRefactoring } from './commands/refactor/acceptRefactoring'; +import { rejectRefactoring } from './commands/refactor/rejectRefactoring'; +import { exportMetricsData } from './commands/views/exportMetricsData'; // === Listeners & UI === import { WorkspaceModifiedListener } from './listeners/workspaceModifiedListener'; -import { FileHighlighter } from './ui/FileHighlighter'; +import { FileHighlighter } from './ui/fileHighlighter'; import { LineSelectionManager } from './ui/lineSelectionManager'; import { HoverManager } from './ui/hoverManager'; -import { registerDiffEditor } from './utils/trackedDiffEditors'; +import { + closeAllTrackedDiffEditors, + registerDiffEditor, +} from './utils/trackedDiffEditors'; import { initializeRefactorActionButtons } from './utils/refactorActionButtons'; import { LogManager } from './commands/showLogs'; +import { RefactorArtifacts } from './global'; +import { existsSync, promises } from 'fs'; let backendLogManager: LogManager; @@ -104,13 +116,13 @@ export function activate(context: vscode.ExtensionContext): void { }), ); - const workspaceConfigured = Boolean( - context.workspaceState.get('workspaceConfiguredPath'), + const workspaceConfigured = context.workspaceState.get( + envConfig.WORKSPACE_CONFIGURED_PATH!, ); vscode.commands.executeCommand( 'setContext', 'workspaceState.workspaceConfigured', - workspaceConfigured, + Boolean(workspaceConfigured), ); // === Register Commands === @@ -140,22 +152,183 @@ export function activate(context: vscode.ExtensionContext): void { await wipeWorkCache(smellsCacheManager, smellsViewProvider); }), - vscode.commands.registerCommand('ecooptimizer.detectSmellsFile', (fileItem) => { - const filePath = fileItem?.resourceUri?.fsPath; - if (!filePath) { - vscode.window.showWarningMessage('Please select a file to analyze.'); - return; - } - detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); - }), + vscode.commands.registerCommand( + 'ecooptimizer.detectSmellsFile', + async (fileItem: TreeItem) => { + let filePath: string; + if (!fileItem) { + const allPythonFiles: vscode.QuickPickItem[] = []; + const folderPath = workspaceConfigured; + + if (!folderPath) { + vscode.window.showWarningMessage('No workspace configured.'); + return; + } + + const gatherPythonFiles = async (dirPath: string): Promise => { + const files = await vscode.workspace.fs.readDirectory( + vscode.Uri.file(dirPath), + ); + for (const [name, type] of files) { + const fullPath = path.join(dirPath, name); + if (type === vscode.FileType.File && name.endsWith('.py')) { + const relativePath = path.relative(folderPath, fullPath); + allPythonFiles.push({ + label: `${name}`, + description: `${path.dirname(relativePath) === '.' ? undefined : path.dirname(relativePath)}`, + iconPath: new vscode.ThemeIcon('symbol-file'), + }); + } else if (type === vscode.FileType.Directory) { + await gatherPythonFiles(fullPath); // Recursively gather Python files in subdirectories + } + } + }; + + const currentFile = vscode.window.activeTextEditor?.document.fileName; + if (currentFile && currentFile.endsWith('.py')) { + const relativePath = path.relative(folderPath, currentFile); + allPythonFiles.push({ + label: `${path.basename(currentFile)}`, + description: `${path.dirname(relativePath) === '.' ? undefined : path.dirname(relativePath)}`, + detail: 'Current File', + iconPath: new vscode.ThemeIcon('symbol-file'), + }); + + allPythonFiles.push({ + label: '───────────────', + kind: vscode.QuickPickItemKind.Separator, + }); + } + + await gatherPythonFiles(folderPath); + + if (allPythonFiles.length === 0) { + vscode.window.showWarningMessage( + 'No Python files found in the workspace.', + ); + return; + } + + const selectedFile = await vscode.window.showQuickPick(allPythonFiles, { + title: 'Select a Python file to analyze', + placeHolder: 'Choose a Python file from the workspace', + canPickMany: false, + }); + + if (!selectedFile) { + vscode.window.showWarningMessage('No file selected.'); + return; + } + + filePath = path.join( + folderPath, + selectedFile.description!, + selectedFile.label, + ); + } else { + if (!(fileItem instanceof vscode.TreeItem)) { + vscode.window.showWarningMessage('Invalid file item selected.'); + return; + } + filePath = fileItem.resourceUri!.fsPath; + if (!filePath) { + vscode.window.showWarningMessage('Please select a file to analyze.'); + return; + } + } + detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); + }, + ), vscode.commands.registerCommand( 'ecooptimizer.detectSmellsFolder', - (folderItem) => { - const folderPath = folderItem?.resourceUri?.fsPath; - if (!folderPath) { - vscode.window.showWarningMessage('Please select a folder to analyze.'); - return; + async (folderItem: vscode.TreeItem) => { + let folderPath: string; + if (!folderItem) { + if (!workspaceConfigured) { + vscode.window.showWarningMessage('No workspace configured.'); + return; + } + + const allDirectories: vscode.QuickPickItem[] = []; + const directoriesWithPythonFiles = new Set(); + + const gatherDirectories = async ( + dirPath: string, + relativePath = '', + ): Promise => { + const files = await vscode.workspace.fs.readDirectory( + vscode.Uri.file(dirPath), + ); + let hasPythonFile = false; + + for (const [name, type] of files) { + const fullPath = path.join(dirPath, name); + const newRelativePath = path.join(relativePath, name); + if (type === vscode.FileType.File && name.endsWith('.py')) { + hasPythonFile = true; + } else if (type === vscode.FileType.Directory) { + const subDirHasPythonFile = await gatherDirectories( + fullPath, + newRelativePath, + ); + if (subDirHasPythonFile) { + hasPythonFile = true; + } + } + } + + if (hasPythonFile) { + directoriesWithPythonFiles.add(dirPath); + const isDirectChild = relativePath.split(path.sep).length === 1; + allDirectories.push({ + label: `${path.basename(dirPath)}`, + description: isDirectChild ? undefined : path.dirname(relativePath), + iconPath: new vscode.ThemeIcon('folder'), + }); + } + + return hasPythonFile; + }; + + await gatherDirectories(workspaceConfigured); + + if (allDirectories.length === 0) { + vscode.window.showWarningMessage( + 'No directories with Python files found in the workspace.', + ); + return; + } + + const selectedDirectory = await vscode.window.showQuickPick( + allDirectories, + { + title: 'Select a directory to analyze', + placeHolder: 'Choose a directory with Python files from the workspace', + canPickMany: false, + }, + ); + + if (!selectedDirectory) { + vscode.window.showWarningMessage('No directory selected.'); + return; + } + + folderPath = path.join( + workspaceConfigured, + selectedDirectory.description + ? path.join( + selectedDirectory.description, + path.basename(selectedDirectory.label), + ) + : path.basename(selectedDirectory.label), + ); + } else { + if (!(folderItem instanceof vscode.TreeItem)) { + vscode.window.showWarningMessage('Invalid folder item selected.'); + return; + } + folderPath = folderItem.resourceUri!.fsPath; } detectSmellsFolder(folderPath, smellsViewProvider, smellsCacheManager); }, @@ -163,8 +336,13 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand( 'ecooptimizer.refactorSmell', - (item: SmellTreeItem) => { - const smell = item?.smell; + (item: SmellTreeItem | Smell) => { + let smell: Smell; + if (item instanceof SmellTreeItem) { + smell = item.smell; + } else { + smell = item; + } if (!smell) { vscode.window.showErrorMessage('No code smell detected for this item.'); return; @@ -175,8 +353,8 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand( 'ecooptimizer.refactorAllSmellsOfType', - async (item: any) => { - const filePath = item?.fullPath; + async (item: TreeItem | { fullPath: string; smellType: string }) => { + let filePath = item.fullPath; if (!filePath) { vscode.window.showWarningMessage( 'Unable to get file path for smell refactoring.', @@ -197,55 +375,61 @@ export function activate(context: vscode.ExtensionContext): void { uniqueMessageIds.add(smell.messageId); } - const quickPickItems: vscode.QuickPickItem[] = Array.from( - uniqueMessageIds, - ).map((id) => { - const name = getNameByMessageId(id) ?? id; - return { - label: name, - description: id, - }; - }); - - const selected = await vscode.window.showQuickPick(quickPickItems, { - title: 'Select a smell type to refactor', - placeHolder: 'Choose the type of smell you want to refactor', - matchOnDescription: false, - matchOnDetail: false, - ignoreFocusOut: false, - canPickMany: false, - }); - - if (selected) { - const selectedMessageId = selected.description; - const firstSmell = cachedSmells.find( - (smell) => smell.messageId === selectedMessageId, - ); - - if (!firstSmell) { - vscode.window.showWarningMessage( - 'No smells found for the selected type.', - ); + let selectedSmell: string; + if (item instanceof TreeItem) { + const quickPickItems: vscode.QuickPickItem[] = Array.from( + uniqueMessageIds, + ).map((id) => { + const name = getNameByMessageId(id) ?? id; + return { + label: name, + description: id, + }; + }); + + const selected = await vscode.window.showQuickPick(quickPickItems, { + title: 'Select a smell type to refactor', + placeHolder: 'Choose the type of smell you want to refactor', + matchOnDescription: false, + matchOnDetail: false, + ignoreFocusOut: false, + canPickMany: false, + }); + + if (!selected) { return; } + selectedSmell = selected.description!; + } else { + selectedSmell = item.smellType; + } - ecoOutput.info( - `🔁 Triggering refactorAllSmellsOfType for: ${selectedMessageId}`, - ); + const firstSmell = cachedSmells.find( + (smell) => smell.messageId === selectedSmell, + ); - await refactor( - smellsViewProvider, - refactoringDetailsViewProvider, - firstSmell, - context, - true, // isRefactorAllOfType - ); + if (!firstSmell) { + vscode.window.showWarningMessage('No smells found for the selected type.'); + return; } + + ecoOutput.info( + `🔁 Triggering refactorAllSmellsOfType for: ${selectedSmell}`, + ); + + await refactor( + smellsViewProvider, + refactoringDetailsViewProvider, + firstSmell, + context, + true, // isRefactorAllOfType + ); }, ), vscode.commands.registerCommand('ecooptimizer.acceptRefactoring', async () => { await acceptRefactoring( + context, refactoringDetailsViewProvider, metricsViewProvider, smellsCacheManager, @@ -254,7 +438,11 @@ export function activate(context: vscode.ExtensionContext): void { }), vscode.commands.registerCommand('ecooptimizer.rejectRefactoring', async () => { - await rejectRefactoring(refactoringDetailsViewProvider, smellsViewProvider); + await rejectRefactoring( + context, + refactoringDetailsViewProvider, + smellsViewProvider, + ); }), vscode.commands.registerCommand( @@ -331,7 +519,11 @@ export function activate(context: vscode.ExtensionContext): void { const lineSelectManager = new LineSelectionManager(smellsCacheManager); context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection((event) => { - lineSelectManager.commentLine(event.textEditor); + const textEditor = event.textEditor; + if (!textEditor.document.fileName.endsWith('.py')) { + return; + } + lineSelectManager.commentLine(textEditor); }), ); @@ -348,12 +540,23 @@ export function activate(context: vscode.ExtensionContext): void { ); }; + const lintActiveEditors = (): void => { + for (const editor of vscode.window.visibleTextEditors) { + const filePath = editor.document.uri.fsPath; + detectSmellsFile(filePath, smellsViewProvider, smellsCacheManager); + ecoOutput.info( + `[WorkspaceListener] Smell linting is ON — auto-detecting smells for ${filePath}`, + ); + } + }; + const toggleSmellLinting = (): void => { smellLintingEnabled = !smellLintingEnabled; updateSmellLintingContext(); const msg = smellLintingEnabled ? 'Smell linting enabled' : 'Smell linting disabled'; + lintActiveEditors(); vscode.window.showInformationMessage(msg); }; @@ -368,6 +571,64 @@ export function activate(context: vscode.ExtensionContext): void { ), ); + // === File View Change Listner === + context.subscriptions.push( + vscode.window.onDidChangeVisibleTextEditors(() => { + fileHighlighter.updateHighlightsForVisibleEditors(); + + if (smellLintingEnabled) { + lintActiveEditors(); + } + }), + ); + + const cleanPastSessionArtifacts = async (): Promise => { + const pastData = context.workspaceState.get( + envConfig.UNFINISHED_REFACTORING!, + ); + + if (pastData) { + const tempDir = pastData.refactoredData.tempDir; + + try { + const tempDirExists = existsSync(tempDir); + + if (tempDirExists) { + const userChoice = await vscode.window.showWarningMessage( + 'A previous refactoring session was detected. Would you like to continue or discard it?', + { modal: true }, + 'Continue', + 'Discard', + ); + + if (userChoice === 'Discard') { + await promises.rm(tempDir, { recursive: true, force: true }); + + context.workspaceState.update( + envConfig.UNFINISHED_REFACTORING!, + undefined, + ); + + closeAllTrackedDiffEditors(); + } else if (userChoice === 'Continue') { + ecoOutput.info('Resuming previous refactoring session...'); + startRefactorSession( + pastData.smell, + pastData.refactoredData, + refactoringDetailsViewProvider, + ); + return; + } + } + } catch (error) { + ecoOutput.error(`Error handling past refactoring session: ${error}`); + context.workspaceState.update(envConfig.UNFINISHED_REFACTORING!, undefined); + } + } + }; + + cleanPastSessionArtifacts(); + ecoOutput.info('Eco-Optimizer extension activated successfully'); console.log('Eco-Optimizer extension activated successfully'); } diff --git a/src/global.d.ts b/src/global.d.ts index 7b026dd..d25b7da 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -91,4 +91,9 @@ declare global { refactored: string; }[]; } +} + +export interface RefactorArtifacts { + refactoredData: RefactoredData; + smell: Smell; } \ No newline at end of file diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index 291c023..d267455 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -5,7 +5,8 @@ import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { MetricsViewProvider } from '../providers/MetricsViewProvider'; import { ecoOutput, isSmellLintingEnabled } from '../extension'; -import { detectSmellsFile } from '../commands/detectSmells'; +import { detectSmellsFile } from '../commands/detection/detectSmells'; +import { envConfig } from '../utils/envConfig'; /** * Monitors workspace modifications and maintains analysis state consistency by: @@ -26,7 +27,7 @@ export class WorkspaceModifiedListener { ) { this.initializeFileWatcher(); this.initializeSaveListener(); - ecoOutput.appendLine( + ecoOutput.trace( '[WorkspaceListener] Initialized workspace modification listener', ); } @@ -36,10 +37,10 @@ export class WorkspaceModifiedListener { */ private initializeFileWatcher(): void { const configuredPath = this.context.workspaceState.get( - 'workspaceConfiguredPath', + envConfig.WORKSPACE_CONFIGURED_PATH!, ); if (!configuredPath) { - ecoOutput.appendLine( + ecoOutput.trace( '[WorkspaceListener] No workspace configured - skipping file watcher', ); return; @@ -54,22 +55,20 @@ export class WorkspaceModifiedListener { ); this.fileWatcher.onDidCreate(() => { - ecoOutput.appendLine('[WorkspaceListener] Detected new Python file'); + ecoOutput.trace('[WorkspaceListener] Detected new Python file'); this.refreshViews(); }); this.fileWatcher.onDidDelete((uri) => { - ecoOutput.appendLine( - `[WorkspaceListener] Detected deletion of ${uri.fsPath}`, - ); + ecoOutput.trace(`[WorkspaceListener] Detected deletion of ${uri.fsPath}`); this.handleFileDeletion(uri.fsPath); }); - ecoOutput.appendLine( + ecoOutput.trace( `[WorkspaceListener] Watching Python files in ${configuredPath}`, ); } catch (error) { - ecoOutput.appendLine( + ecoOutput.error( `[WorkspaceListener] Error initializing file watcher: ${error instanceof Error ? error.message : String(error)}`, ); } @@ -81,13 +80,13 @@ export class WorkspaceModifiedListener { private initializeSaveListener(): void { this.saveListener = vscode.workspace.onDidSaveTextDocument((document) => { if (document.languageId === 'python') { - ecoOutput.appendLine( + ecoOutput.trace( `[WorkspaceListener] Detected save in ${document.uri.fsPath}`, ); this.handleFileChange(document.uri.fsPath); if (isSmellLintingEnabled()) { - ecoOutput.appendLine( + ecoOutput.info( `[WorkspaceListener] Smell linting is ON — auto-detecting smells for ${document.uri.fsPath}`, ); detectSmellsFile( @@ -109,16 +108,14 @@ export class WorkspaceModifiedListener { private async handleFileChange(filePath: string): Promise { // Log current cache state for debugging const cachedFiles = this.smellsCacheManager.getAllFilePaths(); - ecoOutput.appendLine( + ecoOutput.trace( `[WorkspaceListener] Current cached files (${cachedFiles.length}):\n` + cachedFiles.map((f) => ` - ${f}`).join('\n'), ); const hadCache = this.smellsCacheManager.hasFileInCache(filePath); if (!hadCache) { - ecoOutput.appendLine( - `[WorkspaceListener] No cache to invalidate for ${filePath}`, - ); + ecoOutput.trace(`[WorkspaceListener] No cache to invalidate for ${filePath}`); return; } @@ -126,7 +123,7 @@ export class WorkspaceModifiedListener { await this.smellsCacheManager.clearCachedSmellsForFile(filePath); this.smellsViewProvider.setStatus(filePath, 'outdated'); - ecoOutput.appendLine( + ecoOutput.trace( `[WorkspaceListener] Invalidated cache for modified file: ${filePath}`, ); vscode.window.showInformationMessage( @@ -136,7 +133,7 @@ export class WorkspaceModifiedListener { this.refreshViews(); } catch (error) { - ecoOutput.appendLine( + ecoOutput.error( `[WorkspaceListener] Error handling file change: ${error instanceof Error ? error.message : String(error)}`, ); } @@ -156,11 +153,11 @@ export class WorkspaceModifiedListener { try { await this.smellsCacheManager.clearCachedSmellsByPath(filePath); removed = true; - ecoOutput.appendLine( + ecoOutput.trace( `[WorkspaceListener] Cleared cache for deleted file: ${filePath}`, ); } catch (error) { - ecoOutput.appendLine( + ecoOutput.error( `[WorkspaceListener] Error clearing cache: ${error instanceof Error ? error.message : String(error)}`, ); } @@ -169,7 +166,7 @@ export class WorkspaceModifiedListener { const removedFromTree = this.smellsViewProvider.removeFile(filePath); if (removedFromTree) { removed = true; - ecoOutput.appendLine(`[WorkspaceListener] Removed from view: ${filePath}`); + ecoOutput.trace(`[WorkspaceListener] Removed from view: ${filePath}`); } if (removed) { @@ -188,7 +185,7 @@ export class WorkspaceModifiedListener { private refreshViews(): void { this.smellsViewProvider.refresh(); this.metricsViewProvider.refresh(); - ecoOutput.appendLine('[WorkspaceListener] Refreshed all views'); + ecoOutput.trace('[WorkspaceListener] Refreshed all views'); } /** @@ -199,6 +196,6 @@ export class WorkspaceModifiedListener { public dispose(): void { this.fileWatcher?.dispose(); this.saveListener?.dispose(); - ecoOutput.appendLine('[WorkspaceListener] Disposed all listeners'); + ecoOutput.trace('[WorkspaceListener] Disposed all listeners'); } } diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index eec8ec6..55dc968 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -1,5 +1,10 @@ import * as vscode from 'vscode'; -import { FilterSmellConfig, getFilterSmells, saveSmells } from '../utils/smellsData'; +import { + FilterSmellConfig, + getFilterSmells, + loadSmells, + saveSmells, +} from '../utils/smellsData'; import { MetricsViewProvider } from './MetricsViewProvider'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from './SmellsViewProvider'; @@ -129,6 +134,20 @@ export class FilterViewProvider implements vscode.TreeDataProvider { + const confirmed = await this.confirmFilterChange(); + if (!confirmed) return; + + loadSmells('default'); + + const defaultSmells = getFilterSmells(); + this.smells = defaultSmells; + saveSmells(this.smells); + + await this.invalidateCachedSmellsForAffectedFiles(); + this._onDidChangeTreeData.fire(); + } + async invalidateCachedSmellsForAffectedFiles(): Promise { const cachedFilePaths = this.smellsCacheManager.getAllFilePaths(); diff --git a/src/providers/MetricsViewProvider.ts b/src/providers/MetricsViewProvider.ts index 5291c0f..43f58c3 100644 --- a/src/providers/MetricsViewProvider.ts +++ b/src/providers/MetricsViewProvider.ts @@ -1,51 +1,45 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import { basename } from 'path'; - +import { buildPythonTree } from '../utils/TreeStructureBuilder'; import { envConfig } from '../utils/envConfig'; import { getFilterSmells } from '../utils/smellsData'; +import { normalizePath } from '../utils/normalizePath'; -/** - * Represents a metric item in the tree view. - */ -class MetricItem extends vscode.TreeItem { +class MetricTreeItem extends vscode.TreeItem { constructor( public readonly label: string, public readonly collapsibleState: vscode.TreeItemCollapsibleState, public readonly contextValue: string, public readonly carbonSaved?: number, - public readonly resourceUri?: vscode.Uri, // For file/folder paths - public readonly smellName?: string, // For smell names + public readonly resourceUri?: vscode.Uri, + public readonly smellName?: string, ) { super(label, collapsibleState); // Set icon based on contextValue switch (this.contextValue) { case 'folder': - this.iconPath = new vscode.ThemeIcon('folder'); // Built-in folder icon + this.iconPath = new vscode.ThemeIcon('folder'); break; case 'file': - this.iconPath = new vscode.ThemeIcon('file'); // Built-in file icon + this.iconPath = new vscode.ThemeIcon('file'); break; case 'smell': - this.iconPath = new vscode.ThemeIcon('tag'); // Built-in warning icon + this.iconPath = new vscode.ThemeIcon('tag'); break; case 'folder-stats': - this.iconPath = new vscode.ThemeIcon('graph'); // Optional stats icon + this.iconPath = new vscode.ThemeIcon('graph'); break; } - // Set description for carbon saved this.description = carbonSaved !== undefined ? `Carbon Saved: ${formatNumber(carbonSaved)} kg` : ''; - this.tooltip = this.description; - - this.tooltip = smellName !== undefined ? smellName : ''; + this.tooltip = smellName || this.description; if (resourceUri && contextValue === 'file') { - this.resourceUri = resourceUri; this.command = { title: 'Open File', command: 'vscode.open', @@ -62,8 +56,10 @@ export interface MetricsDataItem { }; } -export class MetricsViewProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData = new vscode.EventEmitter(); +export class MetricsViewProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter< + MetricTreeItem | undefined + >(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; constructor(private context: vscode.ExtensionContext) {} @@ -72,19 +68,18 @@ export class MetricsViewProvider implements vscode.TreeDataProvider this._onDidChangeTreeData.fire(undefined); } - getTreeItem(element: MetricItem): vscode.TreeItem { + getTreeItem(element: MetricTreeItem): vscode.TreeItem { return element; } - async getChildren(element?: MetricItem): Promise { + async getChildren(element?: MetricTreeItem): Promise { const metricsData = this.context.workspaceState.get<{ [path: string]: MetricsDataItem; }>(envConfig.WORKSPACE_METRICS_DATA!, {}); if (!element) { - // Root level: Show configured folder/file const configuredPath = this.context.workspaceState.get( - 'workspaceConfiguredPath', + envConfig.WORKSPACE_CONFIGURED_PATH!, ); if (!configuredPath) return []; @@ -98,41 +93,32 @@ export class MetricsViewProvider implements vscode.TreeDataProvider } if (element.contextValue === 'folder') { - // Show folder stats and contents const folderPath = element.resourceUri!.fsPath; - const folderContents = await this.getFolderContents(folderPath); const folderMetrics = await this.calculateFolderMetrics( - folderContents, + folderPath, metricsData, ); + const treeNodes = buildPythonTree(folderPath); - // Folder stats const folderStats = [ - new MetricItem( + new MetricTreeItem( `Total Carbon Saved: ${formatNumber(folderMetrics.totalCarbonSaved)} kg`, vscode.TreeItemCollapsibleState.None, 'folder-stats', ), ...folderMetrics.smellData.map((data) => this.createSmellItem(data)), - ]; - - // Folder contents (subfolders and files) - const contents = await Promise.all( - folderContents.map((item) => { - if (item.type === 'folder') { - return this.createFolderItem(item.path); - } else { - return this.createFileItem(item.path, metricsData); - } - }), - ); + ].sort(compareTreeItems); + + const contents = treeNodes.map((node) => { + return node.isFile + ? this.createFileItem(node.fullPath, metricsData) + : this.createFolderItem(node.fullPath); + }); - const children = [...folderStats, ...contents]; - return children.sort(compareTreeItems); + return [...contents, ...folderStats]; } if (element.contextValue === 'file') { - // Show smells in the file const filePath = element.resourceUri!.fsPath; const fileMetrics = this.calculateFileMetrics(filePath, metricsData); return fileMetrics.smellData.map((data) => this.createSmellItem(data)); @@ -141,22 +127,22 @@ export class MetricsViewProvider implements vscode.TreeDataProvider return []; } - private createFolderItem(folderPath: string): MetricItem { - return new MetricItem( + private createFolderItem(folderPath: string): MetricTreeItem { + return new MetricTreeItem( basename(folderPath), vscode.TreeItemCollapsibleState.Collapsed, 'folder', undefined, - vscode.Uri.file(folderPath), // resourceUri + vscode.Uri.file(folderPath), ); } private createFileItem( filePath: string, metricsData: { [path: string]: MetricsDataItem }, - ): MetricItem { + ): MetricTreeItem { const fileMetrics = this.calculateFileMetrics(filePath, metricsData); - return new MetricItem( + return new MetricTreeItem( basename(filePath), vscode.TreeItemCollapsibleState.Collapsed, 'file', @@ -169,8 +155,8 @@ export class MetricsViewProvider implements vscode.TreeDataProvider acronym: string; name: string; carbonSaved: number; - }): MetricItem { - return new MetricItem( + }): MetricTreeItem { + return new MetricTreeItem( `${data.acronym}: ${formatNumber(data.carbonSaved)} kg`, vscode.TreeItemCollapsibleState.None, 'smell', @@ -180,45 +166,8 @@ export class MetricsViewProvider implements vscode.TreeDataProvider ); } - /** - * Retrieves the contents of a folder (subfolders and files). - * @param folderPath - The path of the folder. - */ - private async getFolderContents( - folderPath: string, - ): Promise> { - try { - const folderUri = vscode.Uri.file(folderPath); - const directoryEntries = await vscode.workspace.fs.readDirectory(folderUri); - - const contents: Array<{ - type: 'folder' | 'file'; - name: string; - path: string; - }> = []; - for (const [name, type] of directoryEntries) { - const fullPath = vscode.Uri.joinPath(folderUri, name).fsPath; - if (type === vscode.FileType.Directory) { - contents.push({ type: 'folder', name, path: fullPath }); - } else if (type === vscode.FileType.File && name.endsWith('.py')) { - contents.push({ type: 'file', name, path: fullPath }); - } - } - - return contents; - } catch (error) { - console.error(`Failed to read directory ${folderPath}:`, error); - return []; - } - } - - /** - * Calculates the carbon saved for a specific folder dynamically. - * @param folderContents - The contents of the folder. - * @param metricsData - The metrics data from the workspace state. - */ private async calculateFolderMetrics( - folderContents: Array<{ type: 'folder' | 'file'; name: string; path: string }>, + folderPath: string, metricsData: { [path: string]: MetricsDataItem }, ): Promise<{ totalCarbonSaved: number; @@ -227,19 +176,19 @@ export class MetricsViewProvider implements vscode.TreeDataProvider let totalCarbonSaved = 0; const smellDistribution = new Map(); - for (const item of folderContents) { - if (item.type === 'file') { - const fileMetrics = this.calculateFileMetrics(item.path, metricsData); - totalCarbonSaved += fileMetrics.totalCarbonSaved; - - for (const smellData of fileMetrics.smellData) { - const currentCarbonSaved = - smellDistribution.get(smellData.acronym)?.[1] || 0; - smellDistribution.set(smellData.acronym, [ - smellData.name, - currentCarbonSaved + smellData.carbonSaved, - ]); - } + const treeNodes = buildPythonTree(folderPath); + const fileNodes = treeNodes.filter((node) => node.isFile); + + for (const node of fileNodes) { + const fileMetrics = this.calculateFileMetrics(node.fullPath, metricsData); + totalCarbonSaved += fileMetrics.totalCarbonSaved; + + for (const smellData of fileMetrics.smellData) { + const current = smellDistribution.get(smellData.acronym)?.[1] || 0; + smellDistribution.set(smellData.acronym, [ + smellData.name, + current + smellData.carbonSaved, + ]); } } @@ -255,11 +204,6 @@ export class MetricsViewProvider implements vscode.TreeDataProvider }; } - /** - * Calculates the carbon saved for a specific file. - * @param filePath - The path of the file. - * @param metricsData - The metrics data from the workspace state. - */ private calculateFileMetrics( filePath: string, metricsData: { [path: string]: MetricsDataItem }, @@ -268,12 +212,11 @@ export class MetricsViewProvider implements vscode.TreeDataProvider smellData: { acronym: string; name: string; carbonSaved: number }[]; } { const smellConfigData = getFilterSmells(); - const fileData = metricsData[filePath] || { + const fileData = metricsData[normalizePath(filePath)] || { totalCarbonSaved: 0, smellDistribution: {}, }; - // Initialize smell distribution with only enabled smells, defaulting to 0 const smellDistribution = Object.keys(smellConfigData).reduce( (acc, symbol) => { if (smellConfigData[symbol]) { @@ -294,37 +237,34 @@ export class MetricsViewProvider implements vscode.TreeDataProvider }; } - /** - * Updates the metrics view when a smell is refactored. - * @param filePath - The path of the refactored file. - * @param carbonSaved - The amount of carbon saved in kg. - */ updateMetrics(filePath: string, carbonSaved: number, smellSymbol: string): void { const metrics = this.context.workspaceState.get<{ [path: string]: MetricsDataItem; }>(envConfig.WORKSPACE_METRICS_DATA!, {}); - if (!metrics[filePath]) { - metrics[filePath] = { + const normalizedPath = normalizePath(filePath); + + if (!metrics[normalizedPath]) { + metrics[normalizedPath] = { totalCarbonSaved: 0, smellDistribution: {}, }; } - metrics[filePath].totalCarbonSaved = - (metrics[filePath].totalCarbonSaved || 0) + carbonSaved; + metrics[normalizedPath].totalCarbonSaved = + (metrics[normalizedPath].totalCarbonSaved || 0) + carbonSaved; - if (!metrics[filePath].smellDistribution[smellSymbol]) { - metrics[filePath].smellDistribution[smellSymbol] = 0; + if (!metrics[normalizedPath].smellDistribution[smellSymbol]) { + metrics[normalizedPath].smellDistribution[smellSymbol] = 0; } - metrics[filePath].smellDistribution[smellSymbol] += carbonSaved; + metrics[normalizedPath].smellDistribution[smellSymbol] += carbonSaved; this.context.workspaceState.update(envConfig.WORKSPACE_METRICS_DATA!, metrics); - this.refresh(); } } +// Helper functions const contextPriority: { [key: string]: number } = { folder: 1, file: 2, @@ -332,25 +272,16 @@ const contextPriority: { [key: string]: number } = { 'folder-stats': 4, }; -function compareTreeItems(a: MetricItem, b: MetricItem): number { - // Sort by contextValue priority first +function compareTreeItems(a: MetricTreeItem, b: MetricTreeItem): number { const priorityA = contextPriority[a.contextValue] || 0; const priorityB = contextPriority[b.contextValue] || 0; - if (priorityA < priorityB) return -1; - if (priorityA > priorityB) return 1; - - // If contextValue is the same, sort by label - if (a.label < b.label) return -1; - if (a.label > b.label) return 1; - - return 0; + if (priorityA !== priorityB) return priorityA - priorityB; + return a.label.localeCompare(b.label); } function formatNumber(number: number, decimalPlaces: number = 2): string { const threshold = 0.001; - if (Math.abs(number) < threshold) { - return number.toExponential(decimalPlaces); - } else { - return number.toFixed(decimalPlaces); - } + return Math.abs(number) < threshold + ? number.toExponential(decimalPlaces) + : number.toFixed(decimalPlaces); } diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 879220a..0068b68 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -4,6 +4,8 @@ import * as path from 'path'; import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; import { buildPythonTree } from '../utils/TreeStructureBuilder'; import { getAcronymByMessageId } from '../utils/smellsData'; +import { normalizePath } from '../utils/normalizePath'; +import { envConfig } from '../utils/envConfig'; export class SmellsViewProvider implements vscode.TreeDataProvider @@ -25,7 +27,7 @@ export class SmellsViewProvider } setStatus(filePath: string, status: string): void { - this.fileStatuses.set(filePath, status); + this.fileStatuses.set(normalizePath(filePath), status); if (status === 'outdated') { this.fileSmells.delete(filePath); @@ -40,10 +42,11 @@ export class SmellsViewProvider } public removeFile(filePath: string): boolean { - const exists = this.fileStatuses.has(filePath); + const normalizedPath = normalizePath(filePath); + const exists = this.fileStatuses.has(normalizedPath); if (exists) { - this.fileStatuses.delete(filePath); - this.fileSmells.delete(filePath); + this.fileStatuses.delete(normalizedPath); + this.fileSmells.delete(normalizedPath); } return exists; } @@ -62,7 +65,7 @@ export class SmellsViewProvider element?: TreeItem | SmellTreeItem, ): Promise<(TreeItem | SmellTreeItem)[]> { const rootPath = this.context.workspaceState.get( - 'workspaceConfiguredPath', + envConfig.WORKSPACE_CONFIGURED_PATH!, ); if (!rootPath) { return []; @@ -105,7 +108,8 @@ export class SmellsViewProvider private createTreeItem(filePath: string, isFile: boolean): TreeItem { const label = path.basename(filePath); - const status = this.fileStatuses.get(filePath) ?? 'not_yet_detected'; + const status = + this.fileStatuses.get(normalizePath(filePath)) ?? 'not_yet_detected'; const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); const tooltip = isFile ? getStatusMessage(status) : undefined; @@ -137,7 +141,7 @@ export class SmellsViewProvider } } -class TreeItem extends vscode.TreeItem { +export class TreeItem extends vscode.TreeItem { constructor( label: string, public readonly fullPath: string, diff --git a/src/ui/FileHighlighter.ts b/src/ui/FileHighlighter.ts index 1bfd226..23098fb 100644 --- a/src/ui/FileHighlighter.ts +++ b/src/ui/FileHighlighter.ts @@ -44,6 +44,10 @@ export class FileHighlighter { * @param filePath - The file path of the target file to update highlights for. */ private updateHighlightsForFile(filePath: string): void { + if (!filePath.endsWith('.py')) { + return; + } + const editor = vscode.window.visibleTextEditors.find( (e) => e.document.uri.fsPath === filePath, ); @@ -57,6 +61,9 @@ export class FileHighlighter { */ public updateHighlightsForVisibleEditors(): void { vscode.window.visibleTextEditors.forEach((editor) => { + if (!editor.document.fileName.endsWith('.py')) { + return; + } this.highlightSmells(editor); }); } diff --git a/src/ui/hoverManager.ts b/src/ui/hoverManager.ts index 2ee09e7..271739e 100644 --- a/src/ui/hoverManager.ts +++ b/src/ui/hoverManager.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; -import { getDescriptionByMessageId, getNameByMessageId } from '../utils/smellsData'; /** * Displays smell information on hover when hovering over lines in Python files. @@ -26,6 +25,9 @@ export class HoverManager implements vscode.HoverProvider { _token: vscode.CancellationToken, ): vscode.ProviderResult { const filePath = document.uri.fsPath; + + if (!filePath.endsWith('.py')) return; + const smells = this.smellsCacheManager.getCachedSmells(filePath); if (!smells || smells.length === 0) return; @@ -37,25 +39,33 @@ export class HoverManager implements vscode.HoverProvider { if (smellsAtLine.length === 0) return; - const wrap = (text: string, width = 50): string => - text.replace(new RegExp(`(.{1,${width}})(\\s+|$)`, 'g'), '$1\n').trim(); - - const hoverSections = smellsAtLine.map((smell) => { - const name = - getNameByMessageId(smell.messageId) ?? `Unknown Smell (${smell.messageId})`; - const description = - getDescriptionByMessageId(smell.messageId) ?? 'No description available.'; - const message = smell.message ?? 'No message provided.'; - - return [ - `🍂 **${name}**`, - `- \`${wrap(message)}\``, - `- _${wrap(description)}_`, - ].join('\n'); - }); + const escape = (text: string): string => { + return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + }; - const markdown = new vscode.MarkdownString(hoverSections.join('\n\n---\n\n')); + const markdown = new vscode.MarkdownString(); markdown.isTrusted = true; + markdown.supportHtml = true; + markdown.supportThemeIcons = true; + + smellsAtLine.forEach((smell) => { + const messageLine = `${escape(smell.message)} (**${escape(smell.messageId)}**)`; + const divider = '\n\n---\n\n'; + const refactorSmellCmd = `command:ecooptimizer.refactorSmell?${encodeURIComponent(JSON.stringify(smell))} "Fix this specific smell"`; + const refactorTypeCmd = `command:ecooptimizer.refactorAllSmellsOfType?${encodeURIComponent( + JSON.stringify({ + fullPath: filePath, + smellType: smell.messageId, + }), + )} "Fix all similar smells"`; + + markdown.appendMarkdown(messageLine); + markdown.appendMarkdown(divider); + markdown.appendMarkdown(`[$(tools) Refactor Smell](${refactorSmellCmd}) | `); + markdown.appendMarkdown( + `[$(tools) Refactor All of This Type](${refactorTypeCmd})`, + ); + }); return new vscode.Hover(markdown); } diff --git a/src/utils/envConfig.ts b/src/utils/envConfig.ts index 85f9ad1..fc2bb37 100644 --- a/src/utils/envConfig.ts +++ b/src/utils/envConfig.ts @@ -7,6 +7,8 @@ export interface EnvConfig { SMELL_CACHE_KEY?: string; HASH_PATH_MAP_KEY?: string; WORKSPACE_METRICS_DATA?: string; + WORKSPACE_CONFIGURED_PATH?: string; + UNFINISHED_REFACTORING?: string; } export const envConfig: EnvConfig = { @@ -14,4 +16,6 @@ export const envConfig: EnvConfig = { SMELL_CACHE_KEY: process.env.SMELL_CACHE_KEY, HASH_PATH_MAP_KEY: process.env.FILE_HASH_CACHE_KEY, WORKSPACE_METRICS_DATA: process.env.WORKSPACE_METRICS_DATA, + WORKSPACE_CONFIGURED_PATH: process.env.WORKSPACE_CONFIGURED_PATH, + UNFINISHED_REFACTORING: process.env.UNFINISHED_REFACTORING, }; diff --git a/src/utils/initializeStatusesFromCache.ts b/src/utils/initializeStatusesFromCache.ts index 9093f0e..a786fb6 100644 --- a/src/utils/initializeStatusesFromCache.ts +++ b/src/utils/initializeStatusesFromCache.ts @@ -3,6 +3,8 @@ import * as fs from 'fs/promises'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; import { ecoOutput } from '../extension'; +import { normalizePath } from './normalizePath'; +import { envConfig } from './envConfig'; /** * Initializes file statuses and smells in the SmellsViewProvider from the smell cache. @@ -13,35 +15,38 @@ export async function initializeStatusesFromCache( smellsCacheManager: SmellsCacheManager, smellsViewProvider: SmellsViewProvider, ): Promise { - const configuredPath = context.workspaceState.get( - 'workspaceConfiguredPath', + ecoOutput.info('workspace key: ', envConfig.WORKSPACE_CONFIGURED_PATH); + let configuredPath = context.workspaceState.get( + envConfig.WORKSPACE_CONFIGURED_PATH!, ); if (!configuredPath) { - ecoOutput.appendLine( + ecoOutput.warn( '[CacheInit] No configured workspace path found - skipping cache initialization', ); return; } - ecoOutput.appendLine( + configuredPath = normalizePath(configuredPath); + + ecoOutput.info( `[CacheInit] Starting cache initialization for workspace: ${configuredPath}`, ); const pathMap = smellsCacheManager.getAllFilePaths(); - ecoOutput.appendLine(`[CacheInit] Found ${pathMap.length} files in cache`); - ecoOutput.appendLine(`[CacheInit] Found ${pathMap} files in cache`); + ecoOutput.trace(`[CacheInit] Found ${pathMap.length} files in cache`); + ecoOutput.trace(`[CacheInit] Found ${pathMap} files in cache`); let validFiles = 0; let removedFiles = 0; let filesWithSmells = 0; let cleanFiles = 0; for (const filePath of pathMap) { - ecoOutput.appendLine(`[CacheInit] Processing cache entry: ${filePath}`); + ecoOutput.trace(`[CacheInit] Processing cache entry: ${filePath}`); // Ignore files outside the configured workspace if (!filePath.startsWith(configuredPath)) { - ecoOutput.appendLine( + ecoOutput.trace( `[CacheInit] File outside workspace - removing from cache: ${filePath}`, ); await smellsCacheManager.clearCachedSmellsForFile(filePath); @@ -52,9 +57,9 @@ export async function initializeStatusesFromCache( // Verify file still exists try { await fs.access(filePath); - ecoOutput.appendLine(`[CacheInit] File verified: ${filePath}`); + ecoOutput.trace(`[CacheInit] File verified: ${filePath}`); } catch { - ecoOutput.appendLine( + ecoOutput.trace( `[CacheInit] File not found - removing from cache: ${filePath}`, ); await smellsCacheManager.clearCachedSmellsForFile(filePath); @@ -67,26 +72,26 @@ export async function initializeStatusesFromCache( validFiles++; if (smells.length > 0) { - ecoOutput.appendLine( + ecoOutput.trace( `[CacheInit] Found ${smells.length} smells for file: ${filePath}`, ); smellsViewProvider.setStatus(filePath, 'passed'); smellsViewProvider.setSmells(filePath, smells); filesWithSmells++; } else { - ecoOutput.appendLine(`[CacheInit] File has no smells: ${filePath}`); + ecoOutput.trace(`[CacheInit] File has no smells: ${filePath}`); smellsViewProvider.setStatus(filePath, 'no_issues'); cleanFiles++; } } else { - ecoOutput.appendLine( + ecoOutput.trace( `[CacheInit] No cache data found for file (should not happen): ${filePath}`, ); } } // Summary statistics - ecoOutput.appendLine( + ecoOutput.info( `[CacheInit] Cache initialization complete. ` + `Results: ${validFiles} valid files (${filesWithSmells} with smells, ${cleanFiles} clean), ` + `${removedFiles} files removed from cache`, diff --git a/src/utils/normalizePath.ts b/src/utils/normalizePath.ts new file mode 100644 index 0000000..7071fcb --- /dev/null +++ b/src/utils/normalizePath.ts @@ -0,0 +1,8 @@ +/** + * Normalizes file paths for consistent comparison and caching + * @param filePath - The file path to normalize + * @returns Lowercase version of the path for case-insensitive operations + */ +export function normalizePath(filePath: string): string { + return filePath.toLowerCase(); +} diff --git a/src/utils/refactorActionButtons.ts b/src/utils/refactorActionButtons.ts index 00a6e27..f4c9a93 100644 --- a/src/utils/refactorActionButtons.ts +++ b/src/utils/refactorActionButtons.ts @@ -10,7 +10,7 @@ let rejectButton: vscode.StatusBarItem | undefined; export function initializeRefactorActionButtons( context: vscode.ExtensionContext, ): void { - ecoOutput.appendLine('Initializing refactor action buttons...'); + ecoOutput.trace('Initializing refactor action buttons...'); acceptButton = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, @@ -33,7 +33,7 @@ export function initializeRefactorActionButtons( context.subscriptions.push(acceptButton, rejectButton); - ecoOutput.appendLine('Status bar buttons created and registered.'); + ecoOutput.trace('Status bar buttons created and registered.'); } /** @@ -41,13 +41,13 @@ export function initializeRefactorActionButtons( */ export function showRefactorActionButtons(): void { if (!acceptButton || !rejectButton) { - ecoOutput.appendLine( + ecoOutput.trace( '❌ Tried to show refactor buttons but they are not initialized.', ); return; } - ecoOutput.appendLine('Showing refactor action buttons...'); + ecoOutput.trace('Showing refactor action buttons...'); acceptButton.show(); rejectButton.show(); vscode.commands.executeCommand('setContext', 'refactoringInProgress', true); @@ -58,13 +58,13 @@ export function showRefactorActionButtons(): void { */ export function hideRefactorActionButtons(): void { if (!acceptButton || !rejectButton) { - ecoOutput.appendLine( + ecoOutput.trace( '❌ Tried to hide refactor buttons but they are not initialized.', ); return; } - ecoOutput.appendLine('Hiding refactor action buttons...'); + ecoOutput.replace('Hiding refactor action buttons...'); acceptButton.hide(); rejectButton.hide(); vscode.commands.executeCommand('setContext', 'refactoringInProgress', false); diff --git a/src/utils/smellsData.ts b/src/utils/smellsData.ts index 8621060..655fbe2 100644 --- a/src/utils/smellsData.ts +++ b/src/utils/smellsData.ts @@ -33,10 +33,16 @@ let enabledSmells: Record; /** * Loads the full smells configuration from smells.json. + * @param version - The version of the smells configuration to load. * @returns A dictionary of smells with their respective configuration. */ -export function loadSmells(): void { - const filePath = path.join(__dirname, '..', 'data', 'smells.json'); +export function loadSmells(version: 'working' | 'default' = 'working'): void { + const filePath = path.join( + __dirname, + '..', + 'data', + `${version}_smells_config.json`, + ); if (!fs.existsSync(filePath)) { vscode.window.showErrorMessage( @@ -48,7 +54,7 @@ export function loadSmells(): void { filterSmells = JSON.parse(fs.readFileSync(filePath, 'utf-8')); enabledSmells = parseSmells(filterSmells); - ecoOutput.appendLine('\nSmells loaded\n'); + ecoOutput.info(`[smellsData.ts] Loaded smells configuration: ${version}`); } catch (error) { vscode.window.showErrorMessage( 'Error loading smells.json. Please check the file format.', @@ -64,11 +70,11 @@ export function loadSmells(): void { export function saveSmells(smells: Record): void { filterSmells = smells; - const filePath = path.join(__dirname, '..', 'data', 'smells.json'); + const filePath = path.join(__dirname, '..', 'data', 'working_smells_config.json'); + + enabledSmells = parseSmells(filterSmells); try { fs.writeFileSync(filePath, JSON.stringify(smells, null, 2)); - - enabledSmells = parseSmells(filterSmells); } catch (error) { vscode.window.showErrorMessage('Error saving smells.json.'); console.error('ERROR: Failed to write smells.json', error); diff --git a/src/utils/trackedDiffEditors.ts b/src/utils/trackedDiffEditors.ts index 8155c40..6d84967 100644 --- a/src/utils/trackedDiffEditors.ts +++ b/src/utils/trackedDiffEditors.ts @@ -3,7 +3,10 @@ import * as vscode from 'vscode'; const trackedDiffs = new Set(); -export function registerDiffEditor(original: vscode.Uri, modified: vscode.Uri) { +export function registerDiffEditor( + original: vscode.Uri, + modified: vscode.Uri, +): void { trackedDiffs.add(`${original.toString()}::${modified.toString()}`); } @@ -14,7 +17,7 @@ export function isTrackedDiffEditor( return trackedDiffs.has(`${original.toString()}::${modified.toString()}`); } -export async function closeAllTrackedDiffEditors() { +export async function closeAllTrackedDiffEditors(): Promise { const tabs = vscode.window.tabGroups.all.flatMap((group) => group.tabs); for (const tab of tabs) { From fa3659fca34fa1b9ecd84864eab57325434a2e15 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:48:59 -0400 Subject: [PATCH 089/121] small fixes --- src/commands/configureWorkspace.ts | 14 +++-- src/commands/views/exportMetricsData.ts | 3 +- src/context/contextManager.ts | 69 ------------------------- 3 files changed, 10 insertions(+), 76 deletions(-) delete mode 100644 src/context/contextManager.ts diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index 2c0bc92..f9a931c 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; +import { envConfig } from '../utils/envConfig'; /** * Initializes workspace configuration by prompting user to select a Python project folder. @@ -8,7 +9,9 @@ import * as fs from 'fs'; * * @param context - VS Code extension context containing workspace state management */ -export async function configureWorkspace(context: vscode.ExtensionContext) { +export async function configureWorkspace( + context: vscode.ExtensionContext, +): Promise { await configurePythonFolder(context); } @@ -67,7 +70,9 @@ function findPythonFoldersRecursively(folderPath: string): string[] { * * @param context - Extension context for state persistence */ -async function configurePythonFolder(context: vscode.ExtensionContext) { +async function configurePythonFolder( + context: vscode.ExtensionContext, +): Promise { const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders?.length) { @@ -125,11 +130,10 @@ async function configurePythonFolder(context: vscode.ExtensionContext) { export async function updateWorkspace( context: vscode.ExtensionContext, workspacePath: string, -) { +): Promise { // Persist workspace path await context.workspaceState.update( - envConfig, - WORKSPACE_CONFIGURED_PATH!, + envConfig.WORKSPACE_CONFIGURED_PATH!, workspacePath, ); diff --git a/src/commands/views/exportMetricsData.ts b/src/commands/views/exportMetricsData.ts index 0859a82..95f7df2 100644 --- a/src/commands/views/exportMetricsData.ts +++ b/src/commands/views/exportMetricsData.ts @@ -18,8 +18,7 @@ export async function exportMetricsData( } const configuredWorkspacePath = context.workspaceState.get( - envConfig, - WORKSPACE_CONFIGURED_PATH!, + envConfig.WORKSPACE_CONFIGURED_PATH!, ); if (!configuredWorkspacePath) { diff --git a/src/context/contextManager.ts b/src/context/contextManager.ts deleted file mode 100644 index 067b008..0000000 --- a/src/context/contextManager.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as vscode from 'vscode'; - -/** - * Manages persistent data storage within VS Code's workspace state. - * This includes global and workspace-specific data. - */ -export class ContextManager { - public context: vscode.ExtensionContext; - - constructor(context: vscode.ExtensionContext) { - this.context = context; - } - - // ============================ - // Global State (Persists across VS Code sessions) - // ============================ - - /** - * Retrieves globally stored data that persists across VS Code sessions. - * - * @param key - The key associated with the stored value. - * @param defaultVal - The default value to return if the key does not exist. - * @returns The stored data or the default value. - */ - public getGlobalData( - key: string, - defaultVal: any = undefined, - ): T | undefined { - return this.context.globalState.get(key, defaultVal); - } - - /** - * Updates global data that persists across VS Code sessions. - * - * @param key - The key for storing the value. - * @param value - The value to store. - */ - public setGlobalData(key: string, value: any): Thenable { - return this.context.globalState.update(key, value); - } - - // ============================ - // Workspace State (Resets per workspace) - // ============================ - - /** - * Retrieves workspace-specific data that resets when the user changes workspaces. - * - * @param key - The key associated with the stored value. - * @param defaultVal - The default value to return if the key does not exist. - * @returns The stored data or the default value. - */ - public getWorkspaceData( - key: string, - defaultVal: any = undefined, - ): T | undefined { - return this.context.workspaceState.get(key, defaultVal); - } - - /** - * Updates workspace-specific data. - * - * @param key - The key for storing the value. - * @param value - The value to store. - */ - public setWorkspaceData(key: string, value: any): Thenable { - return this.context.workspaceState.update(key, value); - } -} From ec24542a6bdee5194d57656362f43a47dc28215b Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:31:07 -0400 Subject: [PATCH 090/121] made structuring fixes --- package.json | 2 +- src/commands/refactor/acceptRefactoring.ts | 1 + src/commands/refactor/refactor.ts | 8 --- src/commands/views/exportMetricsData.ts | 4 ++ src/providers/SmellsViewProvider.ts | 58 ++++++++++++++++++- src/ui/FileHighlighter.ts | 10 ++++ src/utils/fileStatus.ts | 65 ---------------------- test/setup.ts | 6 +- tsconfig.json | 2 +- 9 files changed, 79 insertions(+), 77 deletions(-) delete mode 100644 src/utils/fileStatus.ts diff --git a/package.json b/package.json index 5b22331..45ced94 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "vscode:prepublish": "npm run package", "compile": "webpack", "test": "jest --verbose", - "test:watch": "jest --watch --silent --verbose", + "test:watch": "jest --watch --verbose", "watch": "webpack --watch", "package": "webpack --mode production --devtool hidden-source-map", "compile-tests": "tsc -p . --outDir out", diff --git a/src/commands/refactor/acceptRefactoring.ts b/src/commands/refactor/acceptRefactoring.ts index d5f5042..eca8b47 100644 --- a/src/commands/refactor/acceptRefactoring.ts +++ b/src/commands/refactor/acceptRefactoring.ts @@ -36,6 +36,7 @@ export async function acceptRefactoring( // Validate refactoring data exists if (!targetFile || !affectedFiles) { + console.log('no data'); ecoOutput.error('[refactorActions.ts] Error: No refactoring data available'); vscode.window.showErrorMessage('No refactoring data available.'); return; diff --git a/src/commands/refactor/refactor.ts b/src/commands/refactor/refactor.ts index b6d984b..a57c5a2 100644 --- a/src/commands/refactor/refactor.ts +++ b/src/commands/refactor/refactor.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { promises } from 'fs'; import { backendRefactorSmell, backendRefactorSmellType } from '../../api/backend'; import { SmellsViewProvider } from '../../providers/SmellsViewProvider'; @@ -131,10 +130,3 @@ export async function startRefactorSession( `Refactoring complete. Estimated savings: ${refactoredData.energySaved ?? 'N/A'} kg CO2`, ); } - -export async function cleanTemps(pastData: RefactoredData): Promise { - console.log('Cleaning up stale artifacts'); - const tempDirs = pastData.tempDir; - - await promises.rm(tempDirs, { recursive: true, force: true }); -} diff --git a/src/commands/views/exportMetricsData.ts b/src/commands/views/exportMetricsData.ts index 95f7df2..2e095e2 100644 --- a/src/commands/views/exportMetricsData.ts +++ b/src/commands/views/exportMetricsData.ts @@ -12,6 +12,8 @@ export async function exportMetricsData( [path: string]: MetricsDataItem; }>(envConfig.WORKSPACE_METRICS_DATA!, {}); + console.log('metrics data:', metricsData); + if (Object.keys(metricsData).length === 0) { vscode.window.showInformationMessage('No metrics data available to export.'); return; @@ -21,6 +23,8 @@ export async function exportMetricsData( envConfig.WORKSPACE_CONFIGURED_PATH!, ); + console.log('configured path:', configuredWorkspacePath); + if (!configuredWorkspacePath) { vscode.window.showErrorMessage('No configured workspace path found.'); return; diff --git a/src/providers/SmellsViewProvider.ts b/src/providers/SmellsViewProvider.ts index 0068b68..d6dab2d 100644 --- a/src/providers/SmellsViewProvider.ts +++ b/src/providers/SmellsViewProvider.ts @@ -1,7 +1,6 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { getStatusIcon, getStatusMessage } from '../utils/fileStatus'; import { buildPythonTree } from '../utils/TreeStructureBuilder'; import { getAcronymByMessageId } from '../utils/smellsData'; import { normalizePath } from '../utils/normalizePath'; @@ -188,3 +187,60 @@ export class SmellTreeItem extends vscode.TreeItem { } } } + +export function getStatusIcon(status: string): vscode.ThemeIcon { + switch (status) { + case 'queued': + return new vscode.ThemeIcon( + 'sync~spin', + new vscode.ThemeColor('charts.yellow'), + ); + case 'passed': + return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.green')); + case 'no_issues': + return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.blue')); + case 'failed': + return new vscode.ThemeIcon('error', new vscode.ThemeColor('charts.red')); + case 'outdated': + return new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.orange')); + case 'server_down': + return new vscode.ThemeIcon( + 'server-process', + new vscode.ThemeColor('charts.red'), + ); + case 'refactoring': + return new vscode.ThemeIcon('robot', new vscode.ThemeColor('charts.purple')); + case 'accept-refactoring': + return new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.yellow')); + default: + return new vscode.ThemeIcon('circle-outline'); + } +} + +/** + * Retrieves the status message corresponding to the smell analysis state. + * @param status - The analysis status. + * @returns A descriptive status message. + */ +export function getStatusMessage(status: string): string { + switch (status) { + case 'queued': + return 'Analyzing Smells'; + case 'passed': + return 'Smells Successfully Detected'; + case 'failed': + return 'Error Detecting Smells'; + case 'no_issues': + return 'No Smells Found'; + case 'outdated': + return 'File Outdated - Needs Reanalysis'; + case 'server_down': + return 'Server Unavailable'; + case 'refactoring': + return 'Refactoring Currently Ongoing'; + case 'accept-refactoring': + return 'Successfully Refactored - Needs Reanalysis'; + default: + return 'Smells Not Yet Detected'; + } +} diff --git a/src/ui/FileHighlighter.ts b/src/ui/FileHighlighter.ts index 23098fb..68cd271 100644 --- a/src/ui/FileHighlighter.ts +++ b/src/ui/FileHighlighter.ts @@ -61,7 +61,9 @@ export class FileHighlighter { */ public updateHighlightsForVisibleEditors(): void { vscode.window.visibleTextEditors.forEach((editor) => { + console.log('editor:', editor); if (!editor.document.fileName.endsWith('.py')) { + console.log('not a python file'); return; } this.highlightSmells(editor); @@ -83,12 +85,18 @@ export class FileHighlighter { * @param editor - The text editor to apply highlights to. */ public highlightSmells(editor: vscode.TextEditor): void { + console.log('reseting decorations'); this.resetHighlights(); + console.log('highlighting smells'); + console.log('cache manager:', this.smellsCacheManager); + const smells = this.smellsCacheManager.getCachedSmells( editor.document.uri.fsPath, ); + console.log('smells', smells); + if (!smells) { return; } @@ -108,6 +116,8 @@ export class FileHighlighter { const enabledSmells = getEnabledSmells(); + console.log(enabledSmells); + activeSmells.forEach((smellType) => { const smellColour = smellColours[smellType]; diff --git a/src/utils/fileStatus.ts b/src/utils/fileStatus.ts deleted file mode 100644 index 214d3bb..0000000 --- a/src/utils/fileStatus.ts +++ /dev/null @@ -1,65 +0,0 @@ -// src/utils/fileStatus.ts - -import * as vscode from 'vscode'; - -/** - * Retrieves the appropriate VS Code icon based on the smell analysis status. - * @param status - The analysis status. - * @returns The corresponding VS Code theme icon. - */ -export function getStatusIcon(status: string): vscode.ThemeIcon { - switch (status) { - case 'queued': - return new vscode.ThemeIcon( - 'sync~spin', - new vscode.ThemeColor('charts.yellow'), - ); - case 'passed': - return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.green')); - case 'no_issues': - return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.blue')); - case 'failed': - return new vscode.ThemeIcon('error', new vscode.ThemeColor('charts.red')); - case 'outdated': - return new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.orange')); - case 'server_down': - return new vscode.ThemeIcon( - 'server-process', - new vscode.ThemeColor('charts.red'), - ); - case 'refactoring': - return new vscode.ThemeIcon('robot', new vscode.ThemeColor('charts.purple')); - case 'accept-refactoring': - return new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.yellow')); - default: - return new vscode.ThemeIcon('circle-outline'); - } -} - -/** - * Retrieves the status message corresponding to the smell analysis state. - * @param status - The analysis status. - * @returns A descriptive status message. - */ -export function getStatusMessage(status: string): string { - switch (status) { - case 'queued': - return 'Analyzing Smells'; - case 'passed': - return 'Smells Successfully Detected'; - case 'failed': - return 'Error Detecting Smells'; - case 'no_issues': - return 'No Smells Found'; - case 'outdated': - return 'File Outdated - Needs Reanalysis'; - case 'server_down': - return 'Server Unavailable'; - case 'refactoring': - return 'Refactoring Currently Ongoing'; - case 'accept-refactoring': - return 'Successfully Refactored - Needs Reanalysis'; - default: - return 'Smells Not Yet Detected'; - } -} diff --git a/test/setup.ts b/test/setup.ts index bb487a4..f201390 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,3 +1,7 @@ +import mockEnvConfig from './mocks/env-config-mock'; + jest.mock('vscode'); -jest.mock('./../src/utils/envConfig'); +jest.mock('../src/utils/envConfig', () => ({ + envConfig: mockEnvConfig, +})); diff --git a/tsconfig.json b/tsconfig.json index 705af40..5250b08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "forceConsistentCasingInFileNames": true, "allowSyntheticDefaultImports": true }, - "include": ["./src/global.d.ts", "./src/types.d.ts", "src/**/*.ts", "test/**/*.ts", "media/script.js"], + "include": ["./src/global.d.ts", "./src/types.d.ts", "src/**/*.ts", "test/**/*.ts"], "exclude": ["node_modules", "dist"] } \ No newline at end of file From b86247c7c6bb20fce80ac7961d4746c0a72b3b1b Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:03:19 -0400 Subject: [PATCH 091/121] fixed tests for detect, refactor and wipe work cache modules --- test/commands/detectSmells.test.ts | 442 ++++++++++++-------- test/commands/refactorSmell.test.ts | 601 ++++++++++++++++------------ test/commands/wipeWorkCache.test.ts | 165 ++++---- 3 files changed, 701 insertions(+), 507 deletions(-) diff --git a/test/commands/detectSmells.test.ts b/test/commands/detectSmells.test.ts index 4767c45..8475e64 100644 --- a/test/commands/detectSmells.test.ts +++ b/test/commands/detectSmells.test.ts @@ -1,243 +1,343 @@ -// test/detect-smells.test.ts -import { ContextManager } from '../../src/context/contextManager'; -import { FileHighlighter } from '../../src/ui/fileHighlighter'; +// test/detection.test.ts +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { + detectSmellsFile, + detectSmellsFolder, +} from '../../src/commands/detection/detectSmells'; +import { SmellsViewProvider } from '../../src/providers/SmellsViewProvider'; +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; +import { serverStatus, ServerStatusType } from '../../src/emitters/serverStatus'; +import { ecoOutput } from '../../src/extension'; + +import context from '../mocks/context-mock'; + +// Mock the external dependencies +jest.mock('fs'); +jest.mock('path'); +jest.mock('../../src/api/backend'); +jest.mock('../../src/utils/smellsData'); +jest.mock('../../src/providers/SmellsViewProvider'); +jest.mock('../../src/context/SmellsCacheManager'); +jest.mock('../../src/emitters/serverStatus'); +jest.mock('../../src/extension'); + +describe('detectSmellsFile', () => { + let smellsViewProvider: SmellsViewProvider; + let smellsCacheManager: SmellsCacheManager; + const mockFilePath = '/path/to/file.py'; -import vscode from '../mocks/vscode-mock'; + beforeEach(() => { + // Reset all mocks before each test + jest.clearAllMocks(); -import * as backend from '../../src/api/backend'; -import * as hashDocs from '../../src/utils/hashDocs'; -import * as editorUtils from '../../src/utils/editorUtils'; -import * as SmellSettings from '../../src/utils/handleSmellSettings'; + // Setup mock instances + smellsViewProvider = new SmellsViewProvider( + context as unknown as vscode.ExtensionContext, + ); + smellsCacheManager = new SmellsCacheManager( + context as unknown as vscode.ExtensionContext, + ); -import { detectSmells } from '../../src/commands/detectSmells'; -import { serverStatus, ServerStatusType } from '../../src/utils/serverStatus'; -import { wipeWorkCache } from '../../src/commands/wipeWorkCache'; -import { envConfig } from '../../src/utils/envConfig'; + // Mock vscode.Uri + (vscode.Uri.file as jest.Mock).mockImplementation((path) => ({ + scheme: 'file', + path, + })); -jest.mock('../../src/commands/wipeWorkCache', () => ({ - wipeWorkCache: jest.fn(), -})); + // Mock path.basename + (path.basename as jest.Mock).mockImplementation((p) => p.split('/').pop()); + }); -jest.mock('../../src/utils/handleSmellSettings.ts', () => ({ - getEnabledSmells: jest.fn().mockImplementation(() => ({ - smell1: true, - smell2: true, - })), -})); + it('should skip non-file URIs', async () => { + (vscode.Uri.file as jest.Mock).mockReturnValueOnce({ scheme: 'untitled' }); -describe('detectSmells', () => { - let contextManagerMock: ContextManager; + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - beforeEach(() => { - // Reset all mocks before each test - jest.clearAllMocks(); + expect(smellsViewProvider.setStatus).not.toHaveBeenCalled(); + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); + }); - // Mock ContextManager - contextManagerMock = { - getWorkspaceData: jest.fn(), - setWorkspaceData: jest.fn(), - } as unknown as ContextManager; + it('should skip non-Python files', async () => { + const nonPythonPath = '/path/to/file.txt'; + + await detectSmellsFile(nonPythonPath, smellsViewProvider, smellsCacheManager); + + expect(smellsViewProvider.setStatus).not.toHaveBeenCalled(); + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); }); - it('should show an error if no active editor is found', async () => { - jest - .spyOn(editorUtils, 'getEditorAndFilePath') - .mockReturnValue({ editor: undefined, filePath: undefined }); + it('should use cached smells when available', async () => { + const mockCachedSmells = [{ id: 'smell1' }]; + (smellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(true); + (smellsCacheManager.getCachedSmells as jest.Mock).mockReturnValue( + mockCachedSmells, + ); - await detectSmells(contextManagerMock); + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - // Assert error message was shown - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: No active editor found.', + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining('Using cached results'), + ); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockFilePath, + 'passed', + ); + expect(smellsViewProvider.setSmells).toHaveBeenCalledWith( + mockFilePath, + mockCachedSmells, ); }); - it('should show an error if no file path is found', async () => { - jest.spyOn(editorUtils, 'getEditorAndFilePath').mockReturnValue({ - editor: vscode.window.activeTextEditor, - filePath: undefined, - }); + it('should handle server down state', async () => { + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.DOWN); + (smellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(false); - await detectSmells(contextManagerMock); + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - // Assert error message was shown - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: Active editor has no valid file path.', + expect(vscode.window.showWarningMessage).toHaveBeenCalled(); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockFilePath, + 'server_down', ); }); - it('should show a warning if no smells are enabled', async () => { - jest.spyOn(editorUtils, 'getEditorAndFilePath').mockReturnValueOnce({ - editor: vscode.window.activeTextEditor, - filePath: 'fake.path', - }); - - jest - .spyOn(SmellSettings, 'getEnabledSmells') - .mockReturnValueOnce({ smell1: false, smell2: false }); + it('should warn when no smells are enabled', async () => { + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.UP); + (smellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(false); + ( + require('../../src/utils/smellsData').getEnabledSmells as jest.Mock + ).mockReturnValue({}); - await detectSmells(contextManagerMock); + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - // Assert warning message was shown expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( - 'Eco: No smells are enabled! Detection skipped.', + 'No smell detectors enabled in settings', + ); + expect(smellsViewProvider.setStatus).not.toHaveBeenCalledWith( + mockFilePath, + 'queued', ); }); - it('should use cached smells when hash is unchanged, same smells enabled', async () => { - jest.spyOn(editorUtils, 'getEditorAndFilePath').mockReturnValueOnce({ - editor: vscode.window.activeTextEditor, - filePath: 'fake.path', + it('should fetch and process smells successfully', async () => { + const mockSmells = [{ id: 'smell1' }]; + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.UP); + (smellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(false); + ( + require('../../src/utils/smellsData').getEnabledSmells as jest.Mock + ).mockReturnValue({ + smell1: { options: {} }, + }); + (require('../../src/api/backend').fetchSmells as jest.Mock).mockResolvedValue({ + smells: mockSmells, + status: 200, }); - jest.spyOn(hashDocs, 'hashContent').mockReturnValue('someHash'); - - jest - .spyOn(contextManagerMock, 'getWorkspaceData') - .mockReturnValueOnce({ smell1: true, smell2: true }) - .mockReturnValueOnce({ - 'fake.path': { - hash: 'someHash', - smells: [], - }, - }); - - jest.spyOn(serverStatus, 'getStatus').mockReturnValue(ServerStatusType.UP); - - await detectSmells(contextManagerMock); + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - expect(vscode.window.showInformationMessage).toHaveBeenNthCalledWith( - 1, - 'Eco: Using cached smells for fake.path', + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockFilePath, + 'queued', + ); + expect(smellsCacheManager.setCachedSmells).toHaveBeenCalledWith( + mockFilePath, + mockSmells, + ); + expect(smellsViewProvider.setSmells).toHaveBeenCalledWith( + mockFilePath, + mockSmells, + ); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockFilePath, + 'passed', + ); + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining('Detected 1 smells'), ); }); - it('should fetch new smells on changed enabled smells', async () => { - jest.spyOn(editorUtils, 'getEditorAndFilePath').mockReturnValueOnce({ - editor: vscode.window.activeTextEditor, - filePath: 'fake.path', + it('should handle no smells found', async () => { + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.UP); + (smellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(false); + ( + require('../../src/utils/smellsData').getEnabledSmells as jest.Mock + ).mockReturnValue({ + smell1: { options: {} }, + }); + (require('../../src/api/backend').fetchSmells as jest.Mock).mockResolvedValue({ + smells: [], + status: 200, }); - jest.spyOn(hashDocs, 'hashContent').mockReturnValue('someHash'); - jest.spyOn(hashDocs, 'updateHash').mockResolvedValue(); - - jest - .spyOn(contextManagerMock, 'getWorkspaceData') - .mockReturnValueOnce({ smell1: true, smell2: false }) - .mockReturnValueOnce({}); + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - jest.spyOn(backend, 'fetchSmells').mockResolvedValueOnce([]); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockFilePath, + 'no_issues', + ); + expect(smellsCacheManager.setCachedSmells).toHaveBeenCalledWith( + mockFilePath, + [], + ); + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining('File has no detectable smells'), + ); + }); - jest.spyOn(serverStatus, 'getStatus').mockReturnValue(ServerStatusType.UP); + it('should handle API errors', async () => { + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.UP); + (smellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(false); + ( + require('../../src/utils/smellsData').getEnabledSmells as jest.Mock + ).mockReturnValue({ + smell1: { options: {} }, + }); + (require('../../src/api/backend').fetchSmells as jest.Mock).mockResolvedValue({ + smells: [], + status: 500, + }); - await detectSmells(contextManagerMock); + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - expect(wipeWorkCache).toHaveBeenCalled(); - expect(hashDocs.updateHash).toHaveBeenCalled(); - expect(backend.fetchSmells).toHaveBeenCalled(); - expect(contextManagerMock.setWorkspaceData).toHaveBeenCalledTimes(2); + expect(vscode.window.showErrorMessage).toHaveBeenCalled(); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockFilePath, + 'failed', + ); }); - it('should fetch new smells on hash change, same enabled smells', async () => { - jest.spyOn(editorUtils, 'getEditorAndFilePath').mockReturnValueOnce({ - editor: vscode.window.activeTextEditor, - filePath: 'fake.path', + it('should handle unexpected errors', async () => { + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.UP); + (smellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(false); + ( + require('../../src/utils/smellsData').getEnabledSmells as jest.Mock + ).mockReturnValue({ + smell1: { options: {} }, }); + (require('../../src/api/backend').fetchSmells as jest.Mock).mockRejectedValue( + new Error('API failed'), + ); - jest.spyOn(hashDocs, 'hashContent').mockReturnValue('someHash'); - jest.spyOn(hashDocs, 'updateHash').mockResolvedValue(); + await detectSmellsFile(mockFilePath, smellsViewProvider, smellsCacheManager); - jest - .spyOn(contextManagerMock, 'getWorkspaceData') - .mockReturnValueOnce({ smell1: true, smell2: true }) - .mockReturnValueOnce({ - 'fake.path': { - hash: 'differentHash', - smells: [], - }, - }); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Analysis failed: API failed', + ); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockFilePath, + 'failed', + ); + expect(ecoOutput.error).toHaveBeenCalled(); + }); +}); - jest.spyOn(serverStatus, 'getStatus').mockReturnValue(ServerStatusType.UP); +describe('detectSmellsFolder', () => { + let smellsViewProvider: SmellsViewProvider; + let smellsCacheManager: SmellsCacheManager; + const mockFolderPath = '/path/to/folder'; - jest.spyOn(backend, 'fetchSmells').mockResolvedValueOnce([]); + beforeEach(() => { + jest.clearAllMocks(); - await detectSmells(contextManagerMock); + smellsViewProvider = new SmellsViewProvider( + context as unknown as vscode.ExtensionContext, + ); + smellsCacheManager = new SmellsCacheManager( + context as unknown as vscode.ExtensionContext, + ); - expect(hashDocs.updateHash).toHaveBeenCalled(); - expect(backend.fetchSmells).toHaveBeenCalled(); - expect(contextManagerMock.setWorkspaceData).toHaveBeenCalledTimes(1); + // Mock vscode.window.withProgress + (vscode.window.withProgress as jest.Mock).mockImplementation((_, callback) => { + return callback(); + }); + + // Mock path.basename + (path.basename as jest.Mock).mockImplementation((p) => p.split('/').pop()); }); - it('should return if no cached smells and server down', async () => { - jest.spyOn(editorUtils, 'getEditorAndFilePath').mockReturnValueOnce({ - editor: vscode.window.activeTextEditor, - filePath: 'fake.path', - }); + it('should show progress notification', async () => { + (fs.readdirSync as jest.Mock).mockReturnValue([]); - jest.spyOn(hashDocs, 'hashContent').mockReturnValue('someHash'); - jest.spyOn(hashDocs, 'updateHash').mockResolvedValue(); + await detectSmellsFolder(mockFolderPath, smellsViewProvider, smellsCacheManager); - jest - .spyOn(contextManagerMock, 'getWorkspaceData') - .mockReturnValueOnce({ smell1: true, smell2: true }) - .mockReturnValueOnce({}); + expect(vscode.window.withProgress).toHaveBeenCalled(); + }); - jest.spyOn(serverStatus, 'getStatus').mockReturnValue(ServerStatusType.DOWN); + it('should handle empty folder', async () => { + (fs.readdirSync as jest.Mock).mockReturnValue([]); - await detectSmells(contextManagerMock); + await detectSmellsFolder(mockFolderPath, smellsViewProvider, smellsCacheManager); - expect(vscode.window.showWarningMessage).toHaveBeenLastCalledWith( - 'Action blocked: Server is down and no cached smells exist for this file version.', + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + expect.stringContaining('No Python files found'), + ); + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining('Found 0 files to analyze'), ); }); - it('should highlight smells if smells are found', async () => { - jest.spyOn(editorUtils, 'getEditorAndFilePath').mockReturnValueOnce({ - editor: vscode.window.activeTextEditor, - filePath: 'fake.path', - }); + it('should process Python files in folder', async () => { + const mockFiles = ['file1.py', 'subdir/file2.py', 'ignore.txt']; - jest.spyOn(hashDocs, 'hashContent').mockReturnValue('someHash'); + (fs.readdirSync as jest.Mock).mockImplementation((dir) => { + if (dir === mockFolderPath) return mockFiles; + if (dir === mockFolderPath + 'subdir') return ['file2.py']; + console.log('Here'); + return mockFiles; + }); jest - .spyOn(contextManagerMock, 'getWorkspaceData') - .mockReturnValueOnce({ smell1: true, smell2: true }) + .spyOn(fs, 'statSync') + .mockReturnValueOnce({ + isDirectory: (): boolean => false, + isFile: (): boolean => true, + } as unknown as fs.Stats) .mockReturnValueOnce({ - 'fake.path': { - hash: 'someHash', - smells: [{} as unknown as Smell], - }, - }); + isDirectory: (): boolean => true, + } as unknown as fs.Stats) + .mockReturnValueOnce({ + isDirectory: (): boolean => false, + isFile: (): boolean => true, + } as unknown as fs.Stats) + .mockReturnValueOnce({ + isDirectory: (): boolean => false, + isFile: (): boolean => true, + } as unknown as fs.Stats); - jest.spyOn(serverStatus, 'getStatus').mockReturnValue(ServerStatusType.UP); + jest + .spyOn(String.prototype, 'endsWith') + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); - const mockHighlightSmells = jest.fn(); jest - .spyOn(FileHighlighter.prototype, 'highlightSmells') - .mockImplementation(mockHighlightSmells); + .spyOn(path, 'join') + .mockReturnValueOnce(mockFolderPath + '/file1.py') + .mockReturnValueOnce(mockFolderPath + '/subdir') + .mockReturnValueOnce(mockFolderPath + '/subdir' + '/file2.py') + .mockReturnValueOnce(mockFolderPath + 'ignore.txt'); - await detectSmells(contextManagerMock); + await detectSmellsFolder(mockFolderPath, smellsViewProvider, smellsCacheManager); - expect(vscode.window.showInformationMessage).toHaveBeenCalledTimes(2); - expect(vscode.window.showInformationMessage).toHaveBeenNthCalledWith( - 1, - 'Eco: Using cached smells for fake.path', + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Analyzing 2 Python files...', ); - - expect(vscode.window.showInformationMessage).toHaveBeenNthCalledWith( - 2, - 'Eco: Highlighted 1 smells.', + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining('Found 2 files to analyze'), ); + }); - expect(mockHighlightSmells).toHaveBeenCalled(); - expect(contextManagerMock.setWorkspaceData).toHaveBeenCalledWith( - envConfig.SMELL_LINTING_ENABLED_KEY, - true, - ); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - 'eco.smellLintingEnabled', - true, + it('should handle directory scan errors', async () => { + (fs.readdirSync as jest.Mock).mockImplementation(() => { + throw new Error('Permission denied'); + }); + + await detectSmellsFolder(mockFolderPath, smellsViewProvider, smellsCacheManager); + + expect(ecoOutput.error).toHaveBeenCalledWith( + expect.stringContaining('Scan error: Permission denied'), ); }); }); diff --git a/test/commands/refactorSmell.test.ts b/test/commands/refactorSmell.test.ts index 0cfb772..451492d 100644 --- a/test/commands/refactorSmell.test.ts +++ b/test/commands/refactorSmell.test.ts @@ -1,323 +1,402 @@ +// test/refactor.test.ts import * as vscode from 'vscode'; import * as fs from 'fs'; - -import { refactorSelectedSmell, cleanTemps } from '../../src/commands/refactorSmell'; -import { ContextManager } from '../../src/context/contextManager'; -import { refactorSmell } from '../../src/api/backend'; -import { FileHighlighter } from '../../src/ui/fileHighlighter'; +import * as path from 'path'; +import { + refactor, + startRefactorSession, +} from '../../src/commands/refactor/refactor'; +import { SmellsViewProvider } from '../../src/providers/SmellsViewProvider'; +import { RefactoringDetailsViewProvider } from '../../src/providers/RefactoringDetailsViewProvider'; +import { serverStatus, ServerStatusType } from '../../src/emitters/serverStatus'; +import { ecoOutput } from '../../src/extension'; import { envConfig } from '../../src/utils/envConfig'; -import { Smell } from '../../src/types'; - -// mock VSCode APIs -jest.mock('vscode', () => ({ - window: { - showErrorMessage: jest.fn(), - showWarningMessage: jest.fn(), - showInformationMessage: jest.fn(), - withProgress: jest.fn((options, task) => task()), - activeTextEditor: undefined, - showTextDocument: jest.fn().mockResolvedValue(undefined), - }, - workspace: { - save: jest.fn(), - getConfiguration: jest.fn(), - openTextDocument: jest.fn().mockImplementation(async (uri) => ({ - // Mock TextDocument object - uri: typeof uri === 'string' ? { fsPath: uri } : uri, - fileName: typeof uri === 'string' ? uri : uri.fsPath, - getText: jest.fn().mockReturnValue('mock content'), - })), - }, - ProgressLocation: { - Notification: 1, - }, - Uri: { - file: jest.fn((path) => ({ - toString: (): string => `file://${path}`, - fsPath: path, - })), - }, - commands: { - executeCommand: jest.fn(), - }, - ViewColumn: { - Beside: 2, - }, -})); - -// mock backend API -jest.mock('../../src/api/backend', () => ({ - refactorSmell: jest.fn(), -})); - -// mock setTimeout -jest.mock('timers/promises', () => ({ - setTimeout: jest.fn().mockResolvedValue(undefined), -})); - -describe('refactorSmell', () => { - let mockContextManager: jest.Mocked; - let fileHighlighterSpy: jest.SpyInstance; - let mockEditor: any; - let mockDocument: any; - let mockSelection: any; - - const createMockSmell = (line: number): Smell => ({ - messageId: 'R0913', - type: 'refactor', - message: 'Too many arguments (8/6)', - confidence: 'HIGH', - path: 'fake.py', - symbol: 'too-many-arguments', - module: 'test-module', - occurences: [ - { - line, - column: 1, - }, - ], - additionalInfo: {}, - }); +import context from '../mocks/context-mock'; +import { MetricsViewProvider } from '../../src/providers/MetricsViewProvider'; +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; +import { acceptRefactoring } from '../../src/commands/refactor/acceptRefactoring'; +import { rejectRefactoring } from '../../src/commands/refactor/rejectRefactoring'; + +// Mock all external dependencies +jest.mock('vscode'); +jest.mock('path'); +jest.mock('fs'); +jest.mock('../../src/api/backend'); +jest.mock('../../src/providers/SmellsViewProvider'); +jest.mock('../../src/providers/RefactoringDetailsViewProvider'); +jest.mock('../../src/emitters/serverStatus'); +jest.mock('../../src/extension'); +jest.mock('../../src/utils/refactorActionButtons'); +jest.mock('../../src/utils/trackedDiffEditors'); + +const mockContext = context as unknown as vscode.ExtensionContext; + +describe('refactor', () => { + let smellsViewProvider: SmellsViewProvider; + let refactoringDetailsViewProvider: RefactoringDetailsViewProvider; + const mockSmell = { + symbol: 'testSmell', + path: '/path/to/file.py', + type: 'testType', + } as unknown as Smell; beforeEach(() => { - // reset all mocks jest.clearAllMocks(); - // setup mock context manager - mockContextManager = { - getWorkspaceData: jest.fn(), - setWorkspaceData: jest.fn(), - } as any; + smellsViewProvider = new SmellsViewProvider({} as vscode.ExtensionContext); + refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); - // setup mock selection - mockSelection = { - start: { line: 0 }, // Line 1 in VS Code's 0-based indexing - end: { line: 0 }, - }; + (path.basename as jest.Mock).mockImplementation((p) => p.split('/').pop()); + + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.UP); + + context.workspaceState.get.mockImplementation((key: string) => { + if (key === envConfig.WORKSPACE_CONFIGURED_PATH) { + return '/workspace/path'; + } + return undefined; + }); + }); + + it('should show error when no workspace is configured', async () => { + (context.workspaceState.get as jest.Mock).mockReturnValue(undefined); + + await refactor( + smellsViewProvider, + refactoringDetailsViewProvider, + mockSmell, + mockContext, + ); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Please configure workspace first', + ); + expect(ecoOutput.error).toHaveBeenCalledWith( + expect.stringContaining('Refactoring aborted: No workspace configured'), + ); + }); + + it('should show warning when backend is down', async () => { + (serverStatus.getStatus as jest.Mock).mockReturnValue(ServerStatusType.DOWN); + + await refactor( + smellsViewProvider, + refactoringDetailsViewProvider, + mockSmell, + mockContext, + ); + + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + 'Cannot refactor - backend service unavailable', + ); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockSmell.path, + 'server_down', + ); + }); - // setup mock document - mockDocument = { - getText: jest.fn().mockReturnValue('mock content'), - uri: { fsPath: '/test/file.ts' }, + it('should initiate single smell refactoring', async () => { + const mockRefactoredData = { + targetFile: { + original: '/original/path', + refactored: '/refactored/path', + }, + affectedFiles: [], + energySaved: 0.5, + tempDir: '/temp/dir', }; - fileHighlighterSpy = jest.spyOn(FileHighlighter, 'getInstance').mockReturnValue({ - highlightSmells: jest.fn(), - } as any); + ( + require('../../src/api/backend').backendRefactorSmell as jest.Mock + ).mockResolvedValue(mockRefactoredData); + + await refactor( + smellsViewProvider, + refactoringDetailsViewProvider, + mockSmell, + mockContext, + ); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining('Refactoring testSmell...'), + ); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockSmell.path, + 'queued', + ); + expect(mockContext.workspaceState.update).toHaveBeenCalled(); + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining('Refactoring completed for file.py'), + ); + }); - // setup mock editor - mockEditor = { - document: mockDocument, - selection: mockSelection, + it('should initiate refactoring all smells of type', async () => { + const mockRefactoredData = { + targetFile: { + original: '/original/path', + refactored: '/refactored/path', + }, + affectedFiles: [], + energySaved: 1.2, + tempDir: '/temp/dir', }; - // reset vscode.window.activeTextEditor - (vscode.window as any).activeTextEditor = mockEditor; + ( + require('../../src/api/backend').backendRefactorSmellType as jest.Mock + ).mockResolvedValue(mockRefactoredData); + + await refactor( + smellsViewProvider, + refactoringDetailsViewProvider, + mockSmell, + mockContext, + true, + ); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining('Refactoring all smells of type testSmell...'), + ); + expect( + require('../../src/api/backend').backendRefactorSmellType, + ).toHaveBeenCalled(); + }); - // reset commands mock - (vscode.commands.executeCommand as jest.Mock).mockResolvedValue(undefined); + it('should handle refactoring failure', async () => { + const error = new Error('Backend error'); + ( + require('../../src/api/backend').backendRefactorSmell as jest.Mock + ).mockRejectedValue(error); + + await refactor( + smellsViewProvider, + refactoringDetailsViewProvider, + mockSmell, + mockContext, + ); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Refactoring failed. See output for details.', + ); + expect(ecoOutput.error).toHaveBeenCalledWith( + expect.stringContaining('Refactoring failed: Backend error'), + ); + expect( + refactoringDetailsViewProvider.resetRefactoringDetails, + ).toHaveBeenCalled(); + expect( + require('../../src/utils/refactorActionButtons').hideRefactorActionButtons, + ).toHaveBeenCalled(); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + mockSmell.path, + 'failed', + ); }); - describe('refactorSelectedSmell', () => { - it('should show error when no active editor', async () => { - (vscode.window as any).activeTextEditor = undefined; + describe('startRefactorSession', () => { + let refactoringDetailsViewProvider: RefactoringDetailsViewProvider; + const mockSmell = { + symbol: 'testSmell', + path: 'original/path/to/file.py', + } as unknown as Smell; + const mockRefactoredData = { + targetFile: { + original: 'original/path/to/file.py', + refactored: 'refactored/path/to/file.py', + }, + affectedFiles: [], + energySaved: 0.5, + tempDir: '/refactored', + }; - await refactorSelectedSmell(mockContextManager); + beforeEach(() => { + jest.clearAllMocks(); + refactoringDetailsViewProvider = new RefactoringDetailsViewProvider(); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: Unable to proceed as no active editor or file path found.', - ); + // Mock path.basename + (path.basename as jest.Mock).mockImplementation((p) => p.split('/').pop()); + + // Mock vscode.Uri.file + (vscode.Uri.file as jest.Mock).mockImplementation((path) => ({ path })); }); - it('should show error when no smells detected', async () => { - mockContextManager.getWorkspaceData.mockImplementation((key) => { - if (key === envConfig.SMELL_MAP_KEY) { - return { - '/test/file.ts': { - smells: [], - }, - }; - } - return undefined; - }); + it('should update refactoring details and show diff', async () => { + await startRefactorSession( + mockSmell, + mockRefactoredData, + refactoringDetailsViewProvider, + ); - await refactorSelectedSmell(mockContextManager); + expect( + refactoringDetailsViewProvider.updateRefactoringDetails, + ).toHaveBeenCalledWith( + mockSmell, + mockRefactoredData.targetFile, + mockRefactoredData.affectedFiles, + mockRefactoredData.energySaved, + ); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: No smells detected in the file for refactoring.', + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'vscode.diff', + expect.anything(), + expect.anything(), + 'Refactoring Comparison (file.py)', + { preview: false }, + ); + + expect( + require('../../src/utils/trackedDiffEditors').registerDiffEditor, + ).toHaveBeenCalled(); + + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'ecooptimizer.refactorView.focus', + ); + + expect( + require('../../src/utils/refactorActionButtons').showRefactorActionButtons, + ).toHaveBeenCalled(); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Refactoring complete. Estimated savings: 0.5 kg CO2', ); }); - it('should show error when no matching smell found for selected line', async () => { - const mockSmells = [createMockSmell(5)]; - - mockContextManager.getWorkspaceData.mockImplementation((key) => { - if (key === envConfig.SMELL_MAP_KEY) { - return { - '/test/file.ts': { - smells: mockSmells, - }, - }; - } - return undefined; - }); + it('should handle missing energy data', async () => { + const dataWithoutEnergy = { ...mockRefactoredData, energySaved: undefined }; - await refactorSelectedSmell(mockContextManager); + await startRefactorSession( + mockSmell, + dataWithoutEnergy, + refactoringDetailsViewProvider, + ); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: No matching smell found for refactoring.', + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Refactoring complete. Estimated savings: N/A kg CO2', ); }); + }); - it('should successfully refactor a smell when found', async () => { - const mockSmells = [createMockSmell(1)]; - - const mockRefactorResult = { - refactoredData: { - tempDir: '/tmp/test', - targetFile: { - original: '/test/file.ts', - refactored: '/test/file.refactored.ts', - }, - affectedFiles: [ - { - original: '/test/other.ts', - refactored: '/test/other.refactored.ts', - }, - ], - energySaved: 10, - }, - updatedSmells: [ - { - ...createMockSmell(1), - messageId: 'updated-smell', - symbol: 'UpdatedSmell', - message: 'Updated message', - }, - ], - }; + describe('acceptRefactoring', () => { + let metricsDataProvider: { updateMetrics: jest.Mock }; + let smellsCacheManager: { clearCachedSmellsForFile: jest.Mock }; - mockContextManager.getWorkspaceData.mockImplementation((key) => { - if (key === envConfig.SMELL_MAP_KEY) { - return { - '/test/file.ts': { - smells: mockSmells, - }, - }; - } - return undefined; - }); + beforeEach(() => { + metricsDataProvider = { + updateMetrics: jest.fn(), + }; + smellsCacheManager = { + clearCachedSmellsForFile: jest.fn(), + }; - (refactorSmell as jest.Mock).mockResolvedValue(mockRefactorResult); + // Mock refactoring details + refactoringDetailsViewProvider.targetFile = { + original: '/original/path', + refactored: '/refactored/path', + }; + refactoringDetailsViewProvider.affectedFiles = [ + { original: '/affected/original', refactored: '/affected/refactored' }, + ]; + refactoringDetailsViewProvider.energySaved = 0.5; + refactoringDetailsViewProvider.targetSmell = mockSmell; + }); - await refactorSelectedSmell(mockContextManager); + it('should apply refactoring changes successfully', async () => { + await acceptRefactoring( + mockContext, + refactoringDetailsViewProvider, + metricsDataProvider as unknown as MetricsViewProvider, + smellsCacheManager as unknown as SmellsCacheManager, + smellsViewProvider, + ); - expect(vscode.workspace.save).toHaveBeenCalled(); - expect(refactorSmell).toHaveBeenCalledWith('/test/file.ts', mockSmells[0]); - expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( - 'Refactoring report available in sidebar.', + expect(fs.copyFileSync).toHaveBeenCalledTimes(2); + expect(metricsDataProvider.updateMetrics).toHaveBeenCalled(); + expect(smellsCacheManager.clearCachedSmellsForFile).toHaveBeenCalledTimes(2); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + '/original/path', + 'outdated', ); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'extension.refactorSidebar.focus', + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Refactoring successfully applied', ); - expect(vscode.workspace.openTextDocument).toHaveBeenCalled(); - expect(vscode.window.showTextDocument).toHaveBeenCalled(); - expect(fileHighlighterSpy).toHaveBeenCalled(); }); - it('should handle refactoring failure', async () => { - const mockSmells = [createMockSmell(1)]; - - mockContextManager.getWorkspaceData.mockImplementation((key) => { - if (key === envConfig.SMELL_MAP_KEY) { - return { - '/test/file.ts': { - smells: mockSmells, - }, - }; - } - return undefined; - }); + it('should handle missing refactoring data', async () => { + refactoringDetailsViewProvider.targetFile = undefined; - (refactorSmell as jest.Mock).mockRejectedValue( - new Error('Refactoring failed'), + await acceptRefactoring( + mockContext, + refactoringDetailsViewProvider, + metricsDataProvider as unknown as MetricsViewProvider, + smellsCacheManager as unknown as SmellsCacheManager, + smellsViewProvider, ); - await refactorSelectedSmell(mockContextManager); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: Refactoring failed. See console for details.', + 'No refactoring data available.', ); }); - it('should handle given smell parameter', async () => { - const givenSmell = createMockSmell(3); - const mockSmells = [givenSmell]; - - mockContextManager.getWorkspaceData.mockImplementation((key) => { - if (key === envConfig.SMELL_MAP_KEY) { - return { - '/test/file.ts': { - smells: mockSmells, - }, - }; - } - return undefined; + it('should handle filesystem errors', async () => { + (fs.copyFileSync as jest.Mock).mockImplementation(() => { + throw new Error('Filesystem error'); }); - const mockRefactorResult = { - refactoredData: { - tempDir: '/tmp/test', - targetFile: { - original: '/test/file.ts', - refactored: '/test/file.refactored.ts', - }, - affectedFiles: [ - { - original: '/test/other.ts', - refactored: '/test/other.refactored.ts', - }, - ], - energySaved: 10, - }, - updatedSmells: [], - }; - - (refactorSmell as jest.Mock).mockResolvedValue(mockRefactorResult); - - await refactorSelectedSmell(mockContextManager, givenSmell); - - expect(refactorSmell).toHaveBeenCalledWith('/test/file.ts', givenSmell); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'extension.refactorSidebar.focus', + await acceptRefactoring( + mockContext, + refactoringDetailsViewProvider, + metricsDataProvider as unknown as MetricsViewProvider, + smellsCacheManager as unknown as SmellsCacheManager, + smellsViewProvider, ); - expect(vscode.workspace.openTextDocument).toHaveBeenCalled(); - expect(vscode.window.showTextDocument).toHaveBeenCalled(); - expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( - 'Eco: No updated smells detected after refactoring.', + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Failed to apply refactoring. Please try again.', ); }); }); - describe('Clean Temp Directory', () => { - it('removes one temp directory', async () => { - const mockPastData = { tempDir: 'mock/temp/dir' }; - - jest.spyOn(fs.promises, 'rm').mockResolvedValueOnce(); + describe('rejectRefactoring', () => { + beforeEach(() => { + refactoringDetailsViewProvider.targetFile = { + original: '/original/path', + refactored: '/refactored/path', + }; + }); - await cleanTemps(mockPastData); + it('should clean up after rejecting refactoring', async () => { + await rejectRefactoring( + mockContext, + refactoringDetailsViewProvider, + smellsViewProvider, + ); - expect(fs.promises.rm).toHaveBeenCalled(); + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith( + '/original/path', + 'passed', + ); + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Refactoring changes discarded', + ); + expect(mockContext.workspaceState.update).toHaveBeenCalledWith( + envConfig.UNFINISHED_REFACTORING!, + undefined, + ); }); - it('removes multiple temp directory', async () => { - const mockPastData = { tempDirs: ['mock/temp/dir1', 'mock/temp/dir2'] }; - - jest.spyOn(fs.promises, 'rm').mockResolvedValueOnce(); + it('should handle errors during cleanup', async () => { + (smellsViewProvider.setStatus as jest.Mock).mockImplementation(() => { + throw new Error('Status update failed'); + }); - await cleanTemps(mockPastData); + await rejectRefactoring( + mockContext, + refactoringDetailsViewProvider, + smellsViewProvider, + ); - expect(fs.promises.rm).toHaveBeenCalledTimes(2); + expect(ecoOutput.error).toHaveBeenCalledWith( + expect.stringContaining('Error during rejection cleanup'), + ); }); }); }); diff --git a/test/commands/wipeWorkCache.test.ts b/test/commands/wipeWorkCache.test.ts index 38ff42e..ac85d1b 100644 --- a/test/commands/wipeWorkCache.test.ts +++ b/test/commands/wipeWorkCache.test.ts @@ -1,115 +1,130 @@ -import mockContextManager from '../mocks/contextManager-mock'; -import { wipeWorkCache } from '../../src/commands/wipeWorkCache'; -import vscode from '../mocks/vscode-mock'; -import { envConfig } from '../mocks/env-config-mock'; -import { updateHash } from '../../src/utils/hashDocs'; - -// mock updateHash function -jest.mock('../../src/utils/hashDocs', () => ({ - updateHash: jest.fn(), -})); +// test/wipeWorkCache.test.ts +import * as vscode from 'vscode'; +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; +import { SmellsViewProvider } from '../../src/providers/SmellsViewProvider'; +import { wipeWorkCache } from '../../src/commands/detection/wipeWorkCache'; +import context from '../mocks/context-mock'; + +// Mock the external dependencies +jest.mock('vscode'); +jest.mock('../../src/context/SmellsCacheManager'); +jest.mock('../../src/providers/SmellsViewProvider'); describe('wipeWorkCache', () => { - beforeEach(() => { - jest.clearAllMocks(); // reset mocks before each test - }); + let smellsCacheManager: SmellsCacheManager; + let smellsViewProvider: SmellsViewProvider; - test('should clear stored smells cache with no reason provided', async () => { - // call wipeWorkCache with contextManagerMock - await wipeWorkCache(mockContextManager); + beforeEach(() => { + // Reset all mocks before each test + jest.clearAllMocks(); - expect(mockContextManager.setWorkspaceData).toHaveBeenCalledWith( - envConfig.SMELL_MAP_KEY, - {}, + // Setup mock instances + smellsCacheManager = new SmellsCacheManager( + context as unknown as vscode.ExtensionContext, + ); + smellsViewProvider = new SmellsViewProvider( + context as unknown as vscode.ExtensionContext, ); - expect(mockContextManager.setWorkspaceData).toHaveBeenCalledTimes(1); // only the smells cache should be cleared when no reason is provided + + // Mock the showWarningMessage to return undefined by default + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue(undefined); }); - test('should clear stored smells cache when reason is settings', async () => { - // call wipeWorkCache with contextManagerMock - await wipeWorkCache(mockContextManager, 'settings'); + it('should show confirmation dialog before clearing cache', async () => { + await wipeWorkCache(smellsCacheManager, smellsViewProvider); - expect(mockContextManager.setWorkspaceData).toHaveBeenCalledWith( - envConfig.SMELL_MAP_KEY, - {}, + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + 'Are you sure you want to clear the entire workspace analysis? This action cannot be undone.', + { modal: true }, + 'Confirm', ); - expect(mockContextManager.setWorkspaceData).toHaveBeenCalledTimes(1); // only the smells cache should be cleared when reason is settings }); - test('should clear file changes when reason is manual', async () => { - // call wipeWorkCache with contextManagerMock - await wipeWorkCache(mockContextManager, 'manual'); + it('should clear cache and refresh UI when user confirms', async () => { + // Mock user confirming the action + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue('Confirm'); - expect(mockContextManager.setWorkspaceData).toHaveBeenCalledWith( - envConfig.SMELL_MAP_KEY, - {}, - ); - expect(mockContextManager.setWorkspaceData).toHaveBeenCalledWith( - envConfig.FILE_CHANGES_KEY, - {}, + await wipeWorkCache(smellsCacheManager, smellsViewProvider); + + expect(smellsCacheManager.clearAllCachedSmells).toHaveBeenCalled(); + expect(smellsViewProvider.clearAllStatuses).toHaveBeenCalled(); + expect(smellsViewProvider.refresh).toHaveBeenCalled(); + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Workspace analysis cleared successfully.', ); - expect(mockContextManager.setWorkspaceData).toHaveBeenCalledTimes(2); // both caches should be cleared when reason is manul }); - test('should log when there are no visible text editors', async () => { - vscode.window.visibleTextEditors = []; // simulate no open editors + it('should not clear cache when user cancels', async () => { + // Mock user cancelling the action + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue(undefined); - const consoleSpy = jest.spyOn(console, 'log'); - await wipeWorkCache(mockContextManager); + await wipeWorkCache(smellsCacheManager, smellsViewProvider); - expect(consoleSpy).toHaveBeenCalledWith('Eco: No open files to update hash.'); + expect(smellsCacheManager.clearAllCachedSmells).not.toHaveBeenCalled(); + expect(smellsViewProvider.clearAllStatuses).not.toHaveBeenCalled(); + expect(smellsViewProvider.refresh).not.toHaveBeenCalled(); + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Operation cancelled.', + ); }); - test('should update hashes for visible text editors', async () => { - vscode.window.visibleTextEditors = [ - { - document: { fileName: 'file1.py', getText: jest.fn(() => 'file1 content') }, - } as any, - { - document: { fileName: 'file2.py', getText: jest.fn(() => 'file2 content') }, - } as any, - ]; - - await wipeWorkCache(mockContextManager); - expect(updateHash).toHaveBeenCalledTimes(2); // should call updateHash for each open document - }); - test('should display the correct message for default wipe', async () => { - await wipeWorkCache(mockContextManager); + it('should not clear cache when user dismisses dialog', async () => { + // Mock user dismissing the dialog (different from cancelling) + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue(undefined); + + await wipeWorkCache(smellsCacheManager, smellsViewProvider); + expect(smellsCacheManager.clearAllCachedSmells).not.toHaveBeenCalled(); + expect(smellsViewProvider.clearAllStatuses).not.toHaveBeenCalled(); + expect(smellsViewProvider.refresh).not.toHaveBeenCalled(); expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( - 'Eco: Successfully wiped workspace cache! ✅', + 'Operation cancelled.', ); }); - test('should display the correct message when reason is "settings"', async () => { - await wipeWorkCache(mockContextManager, 'settings'); + it('should handle case where user clicks something other than Confirm', async () => { + // Mock user clicking something else (e.g., a different button if more were added) + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue( + 'Other Option', + ); + + await wipeWorkCache(smellsCacheManager, smellsViewProvider); + expect(smellsCacheManager.clearAllCachedSmells).not.toHaveBeenCalled(); + expect(smellsViewProvider.clearAllStatuses).not.toHaveBeenCalled(); + expect(smellsViewProvider.refresh).not.toHaveBeenCalled(); expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( - 'Eco: Smell detection settings changed. Cache wiped to apply updates. ✅', + 'Operation cancelled.', ); }); - test('should display the correct message when reason is "manual"', async () => { - await wipeWorkCache(mockContextManager, 'manual'); + it('should show success message only after successful cache clearing', async () => { + // Mock user confirming the action + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue('Confirm'); + + // Mock a successful cache clearing + (smellsCacheManager.clearAllCachedSmells as jest.Mock).mockImplementation(() => { + // Simulate successful clearing + }); + + await wipeWorkCache(smellsCacheManager, smellsViewProvider); expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( - 'Eco: Workspace cache manually wiped by user. ✅', + 'Workspace analysis cleared successfully.', ); }); - test('should handle errors and display an error message', async () => { - mockContextManager.setWorkspaceData.mockRejectedValue(new Error('Mocked Error')); + it('should still show cancellation message if confirmation is aborted', async () => { + // Simulate the confirmation dialog being closed without any selection + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue(undefined); - const consoleErrorSpy = jest.spyOn(console, 'error'); + await wipeWorkCache(smellsCacheManager, smellsViewProvider); - await wipeWorkCache(mockContextManager); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Eco: Error while wiping workspace cache:', - expect.any(Error), + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Operation cancelled.', ); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: Failed to wipe workspace cache. See console for details.', + expect(vscode.window.showInformationMessage).not.toHaveBeenCalledWith( + 'Workspace analysis cleared successfully.', ); }); }); From e56b260bbcb780c0bf14dea7268e7f2a6bed1573 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:04:09 -0400 Subject: [PATCH 092/121] added mocks for tests --- test/mocks/context-mock.ts | 31 +++++ test/mocks/contextManager-mock.ts | 53 -------- test/mocks/env-config-mock.ts | 25 ++-- test/mocks/vscode-mock.ts | 193 +++++++++++++++++++++++++++--- 4 files changed, 220 insertions(+), 82 deletions(-) create mode 100644 test/mocks/context-mock.ts delete mode 100644 test/mocks/contextManager-mock.ts diff --git a/test/mocks/context-mock.ts b/test/mocks/context-mock.ts new file mode 100644 index 0000000..6cd7580 --- /dev/null +++ b/test/mocks/context-mock.ts @@ -0,0 +1,31 @@ +// test/mocks/contextManager-mock.ts +interface ContextStorage { + globalState: Record; + workspaceState: Record; +} + +const contextStorage: ContextStorage = { + globalState: {}, + workspaceState: {}, +}; + +const mockExtensionContext = { + globalState: { + get: jest.fn((key: string, defaultVal?: any) => { + return contextStorage.globalState[key] ?? defaultVal; + }), + update: jest.fn(async (key: string, value: any) => { + contextStorage.globalState[key] = value; + }), + } as any, + workspaceState: { + get: jest.fn((key: string, defaultVal?: any) => { + return contextStorage.workspaceState[key] ?? defaultVal; + }), + update: jest.fn(async (key: string, value: any) => { + contextStorage.workspaceState[key] = value; + }), + } as any, +}; + +export default mockExtensionContext; diff --git a/test/mocks/contextManager-mock.ts b/test/mocks/contextManager-mock.ts deleted file mode 100644 index 81b4f7f..0000000 --- a/test/mocks/contextManager-mock.ts +++ /dev/null @@ -1,53 +0,0 @@ -// test/mocks/contextManager-mock.ts -import * as vscode from 'vscode'; - -interface ContextStorage { - globalState: Record; - workspaceState: Record; -} - -const contextStorage: ContextStorage = { - globalState: {}, - workspaceState: {}, -}; - -const mockExtensionContext: Partial = { - globalState: { - get: jest.fn((key: string, defaultVal?: any) => { - ecoOutput.appendLine(`MOCK getGlobalData: ${key}`); - return contextStorage.globalState[key] ?? defaultVal; - }), - update: jest.fn(async (key: string, value: any) => { - ecoOutput.appendLine(`MOCK setGlobalData: ${key}:${value}`); - contextStorage.globalState[key] = value; - }), - } as any, // Casting to `any` to satisfy `vscode.ExtensionContext` - workspaceState: { - get: jest.fn((key: string, defaultVal?: any) => { - ecoOutput.appendLine(`MOCK getWorkspaceData: ${key}`); - return contextStorage.workspaceState[key] ?? defaultVal; - }), - update: jest.fn(async (key: string, value: any) => { - ecoOutput.appendLine(`MOCK setWorkspaceData ${key}:${value}`); - contextStorage.workspaceState[key] = value; - }), - } as any, // Casting to `any` to satisfy `vscode.ExtensionContext` -}; - -const mockContextManager = { - context: mockExtensionContext as vscode.ExtensionContext, - getGlobalData: jest.fn((key: string, defaultVal?: any) => { - return contextStorage.globalState[key] ?? defaultVal; - }), - setGlobalData: jest.fn(async (key: string, value: any) => { - contextStorage.globalState[key] = value; - }), - getWorkspaceData: jest.fn((key: string, defaultVal?: any) => { - return contextStorage.workspaceState[key] ?? defaultVal; - }), - setWorkspaceData: jest.fn(async (key: string, value: any) => { - contextStorage.workspaceState[key] = value; - }), -}; - -export default mockContextManager; diff --git a/test/mocks/env-config-mock.ts b/test/mocks/env-config-mock.ts index c108fc2..45b980a 100644 --- a/test/mocks/env-config-mock.ts +++ b/test/mocks/env-config-mock.ts @@ -1,17 +1,12 @@ -import { envConfig, EnvConfig } from '../../src/utils/envConfig'; +import { EnvConfig } from '../../src/utils/envConfig'; -jest.mock('../../src/utils/envConfig', () => { - const mockEnvConfig: EnvConfig = { - SERVER_URL: 'server-url', - SMELL_MAP_KEY: 'smell-map-key', - FILE_CHANGES_KEY: 'file-changes-key', - LAST_USED_SMELLS_KEY: 'last-used-smells-key', - CURRENT_REFACTOR_DATA_KEY: 'current-refactor-data-key', - ACTIVE_DIFF_KEY: 'active-diff-key', - SMELL_LINTING_ENABLED_KEY: 'smellLintingEnabledKey', - }; +const mockEnvConfig: EnvConfig = { + SERVER_URL: 'value1', + SMELL_CACHE_KEY: 'value2', + HASH_PATH_MAP_KEY: 'value3', + WORKSPACE_METRICS_DATA: 'value4', + WORKSPACE_CONFIGURED_PATH: 'value5', + UNFINISHED_REFACTORING: 'value6', +}; - return { envConfig: mockEnvConfig }; -}); - -export { envConfig }; +export default mockEnvConfig; diff --git a/test/mocks/vscode-mock.ts b/test/mocks/vscode-mock.ts index 45c4504..c3ac2ee 100644 --- a/test/mocks/vscode-mock.ts +++ b/test/mocks/vscode-mock.ts @@ -3,6 +3,7 @@ interface Config { configGet: any; filePath: any; docText: any; + workspacePath: any; } // Configuration object to dynamically change values during tests @@ -10,6 +11,7 @@ export const config: Config = { configGet: { smell1: true, smell2: true }, filePath: 'fake.py', docText: 'Mock document text', + workspacePath: '/workspace/path', }; export const TextDocument = { @@ -17,12 +19,15 @@ export const TextDocument = { fileName: config.filePath, languageId: 'python', lineAt: jest.fn((line: number) => { - ecoOutput.appendLine('MOCK lineAt:', line); return { text: 'Mock line text', }; }), lineCount: 10, + uri: { + scheme: 'file', + fsPath: config.filePath, + }, }; // Mock for `vscode.TextEditor` @@ -34,6 +39,7 @@ export const TextEditor = { isSingleLine: true, }, setDecorations: jest.fn(), + revealRange: jest.fn(), }; export interface TextEditorDecorationType { @@ -49,39 +55,97 @@ interface Window { showErrorMessage: jest.Mock; showWarningMessage: jest.Mock; createTextEditorDecorationType: jest.Mock; + createOutputChannel: jest.Mock; activeTextEditor: any; visibleTextEditors: any[]; + withProgress: jest.Mock; + showQuickPick: jest.Mock; } export const window: Window = { - showInformationMessage: jest.fn(async (message: string) => { - ecoOutput.appendLine('MOCK showInformationMessage:', message); - return message; + showInformationMessage: jest.fn(async (message: string, options?: any) => { + return options?.modal ? 'Confirm' : message; }), showErrorMessage: jest.fn(async (message: string) => { - ecoOutput.appendLine('MOCK showErrorMessage:', message); return message; }), - showWarningMessage: jest.fn(async (message: string) => { - ecoOutput.appendLine('MOCK showWarningMessage:', message); - return message; + showWarningMessage: jest.fn(async (message: string, options?: any) => { + return options?.modal ? 'Confirm' : message; }), activeTextEditor: TextEditor, visibleTextEditors: [], createTextEditorDecorationType: jest.fn((_options: any) => { - ecoOutput.appendLine('MOCK createTextEditorDecorationType:'); return textEditorDecorationType; }), + createOutputChannel: jest.fn(() => ({ + appendLine: jest.fn(), + show: jest.fn(), + clear: jest.fn(), + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + })), + withProgress: jest.fn((options, task) => { + return task({ + report: jest.fn(), + }); + }), + showQuickPick: jest.fn(), +}; + +export enum FileType { + Directory = 1, + File = 2, +} + +export const fileStat = { + type: FileType.Directory, }; interface Workspace { getConfiguration: jest.Mock; + findFiles: jest.Mock; + fs: { + readFile: jest.Mock; + writeFile: jest.Mock; + stat: typeof fileStat; + }; } export const workspace: Workspace = { getConfiguration: jest.fn((section?: string) => ({ - get: jest.fn(), + get: jest.fn(() => config.configGet), + update: jest.fn(), })), + findFiles: jest.fn(), + fs: { + readFile: jest.fn(), + writeFile: jest.fn(), + stat: fileStat, + }, +}; + +interface MockCommand { + title: string; + command: string; + arguments?: any[]; + tooltip?: string; +} + +export const Command = jest + .fn() + .mockImplementation((title: string, command: string, ...args: any[]) => { + return { + title, + command, + arguments: args, + tooltip: title, + }; + }) as jest.Mock & { + prototype: MockCommand; }; export const OverviewRulerLane = { @@ -97,19 +161,66 @@ export const Range = class MockRange { ) {} }; -// New mocks for hover functionality export const languages = { registerHoverProvider: jest.fn(() => ({ dispose: jest.fn(), })), + registerCodeActionsProvider: jest.fn(), }; +export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15, +} + +// ProgressOptions interface +interface ProgressOptions { + location: ProgressLocation | { viewId: string }; + title?: string; + cancellable?: boolean; +} + +// Progress mock +interface Progress { + report(value: T): void; +} + +// Window.withProgress mock implementation +window.withProgress = jest.fn( + ( + options: ProgressOptions, + task: ( + progress: Progress<{ message?: string; increment?: number }>, + ) => Promise, + ) => { + const progress = { + report: jest.fn(), + }; + return task(progress); + }, +); + export const commands = { registerCommand: jest.fn(), - executeCommand: jest.fn(), + executeCommand: jest.fn((command: string) => { + if (command === 'setContext') { + return Promise.resolve(); + } + return Promise.resolve(); + }), +}; + +export const Uri = { + file: jest.fn((path: string) => ({ + scheme: 'file', + path, + fsPath: path, + toString: () => path, + })), + parse: jest.fn(), }; -// Mock VS Code classes export const Position = class MockPosition { constructor( public line: number, @@ -123,7 +234,6 @@ interface MockMarkdownString { isTrusted: boolean; } -// Create a constructor function mock export const MarkdownString = jest.fn().mockImplementation(() => { return { appendMarkdown: jest.fn(function (this: any, value: string) { @@ -141,8 +251,49 @@ export class MockHover { constructor(public contents: MockMarkdownString) {} } +export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2, +} + +export class MockTreeItem { + constructor( + public label: string, + public collapsibleState?: TreeItemCollapsibleState, + public command?: MockCommand, + ) {} + + iconPath?: + | string + | typeof Uri + | { light: string | typeof Uri; dark: string | typeof Uri }; + description?: string; + tooltip?: string; + contextValue?: string; +} + +export const TreeItem = MockTreeItem; + export const Hover = MockHover; +export const ExtensionContext = { + subscriptions: [], + workspaceState: { + get: jest.fn((key: string) => { + if (key === 'workspaceConfiguredPath') { + return config.workspacePath; + } + return undefined; + }), + update: jest.fn(), + }, + globalState: { + get: jest.fn(), + update: jest.fn(), + }, +}; + export interface Vscode { window: Window; workspace: Workspace; @@ -152,9 +303,16 @@ export interface Vscode { languages: typeof languages; commands: typeof commands; OverviewRulerLane: typeof OverviewRulerLane; + ProgressLocation: typeof ProgressLocation; + FileType: typeof FileType; Range: typeof Range; Position: typeof Position; Hover: typeof Hover; + Command: typeof Command; + Uri: typeof Uri; + TreeItem: typeof TreeItem; + TreeItemCollapsibleState: typeof TreeItemCollapsibleState; + ExtensionContext: typeof ExtensionContext; } const vscode: Vscode = { @@ -166,9 +324,16 @@ const vscode: Vscode = { languages, commands, OverviewRulerLane, + ProgressLocation, + FileType, Range, Position, Hover, + Command, + Uri, + TreeItem, + TreeItemCollapsibleState, + ExtensionContext, }; export default vscode; From f0bfa0394c2c2e0bc95cd9c11b3284d72b7d0f3c Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Tue, 25 Mar 2025 17:11:05 -0400 Subject: [PATCH 093/121] removed providers and showLogs from test coverage --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 45ced94..2fe9349 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,9 @@ "!src/**/index.ts", "!test/mocks/*", "!src/extension.ts", - "!src/context/*" + "!src/context/*", + "!src/providers/*", + "!src/commands/showLogs.ts" ] }, "lint-staged": { From afbe81d9b6e93b6b870afc33384b7b415f6ef6ba Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Wed, 26 Mar 2025 15:09:36 -0400 Subject: [PATCH 094/121] got initializeStatusesFromCache.test.ts to work --- .../RefactoringDetailsViewProvider.ts | 1 + .../utils/initializeStatusesFromCache.test.ts | 167 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 test/utils/initializeStatusesFromCache.test.ts diff --git a/src/providers/RefactoringDetailsViewProvider.ts b/src/providers/RefactoringDetailsViewProvider.ts index 22ab02e..d5c8b45 100644 --- a/src/providers/RefactoringDetailsViewProvider.ts +++ b/src/providers/RefactoringDetailsViewProvider.ts @@ -185,6 +185,7 @@ class RefactoringDetailItem extends vscode.TreeItem { ); } else if (itemType === 'info') { this.iconPath = new vscode.ThemeIcon('info'); + this.tooltip = this.description; } // Make files clickable to open diffs diff --git a/test/utils/initializeStatusesFromCache.test.ts b/test/utils/initializeStatusesFromCache.test.ts new file mode 100644 index 0000000..89b1ec6 --- /dev/null +++ b/test/utils/initializeStatusesFromCache.test.ts @@ -0,0 +1,167 @@ +// test/cacheInitialization.test.ts +import * as vscode from 'vscode'; +import * as fs from 'fs/promises'; +import { initializeStatusesFromCache } from '../../src/utils/initializeStatusesFromCache'; +import { SmellsViewProvider } from '../../src/providers/SmellsViewProvider'; +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; +import { ecoOutput } from '../../src/extension'; +import { envConfig } from '../../src/utils/envConfig'; + +// Mock the external dependencies +jest.mock('fs/promises'); +jest.mock('../../src/extension'); +jest.mock('../../src/utils/envConfig'); +jest.mock('../../src/providers/SmellsViewProvider'); +jest.mock('../../src/context/SmellsCacheManager'); + +describe('initializeStatusesFromCache', () => { + let context: vscode.ExtensionContext; + let smellsViewProvider: SmellsViewProvider; + let smellsCacheManager: SmellsCacheManager; + const mockWorkspacePath = '/workspace/path'; + + beforeEach(() => { + // Reset all mocks before each test + jest.clearAllMocks(); + + // Setup mock instances + context = { + workspaceState: { + get: jest.fn(), + }, + } as unknown as vscode.ExtensionContext; + + smellsViewProvider = new SmellsViewProvider(context); + smellsCacheManager = new SmellsCacheManager(context); + + // Mock envConfig + (envConfig.WORKSPACE_CONFIGURED_PATH as any) = 'WORKSPACE_PATH_KEY'; + }); + + it('should skip initialization when no workspace path is configured', async () => { + (context.workspaceState.get as jest.Mock).mockReturnValue(undefined); + + await initializeStatusesFromCache( + context, + smellsCacheManager, + smellsViewProvider, + ); + + expect(ecoOutput.warn).toHaveBeenCalledWith( + expect.stringContaining('No configured workspace path found'), + ); + expect(smellsCacheManager.getAllFilePaths).not.toHaveBeenCalled(); + }); + + it('should remove files outside the workspace from cache', async () => { + const outsidePath = '/other/path/file.py'; + (context.workspaceState.get as jest.Mock).mockReturnValue(mockWorkspacePath); + (smellsCacheManager.getAllFilePaths as jest.Mock).mockReturnValue([outsidePath]); + + await initializeStatusesFromCache( + context, + smellsCacheManager, + smellsViewProvider, + ); + + expect(smellsCacheManager.clearCachedSmellsForFile).toHaveBeenCalledWith( + outsidePath, + ); + expect(ecoOutput.trace).toHaveBeenCalledWith( + expect.stringContaining('File outside workspace'), + ); + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining('1 files removed from cache'), + ); + }); + + it('should remove non-existent files from cache', async () => { + const filePath = `${mockWorkspacePath}/file.py`; + (context.workspaceState.get as jest.Mock).mockReturnValue(mockWorkspacePath); + (smellsCacheManager.getAllFilePaths as jest.Mock).mockReturnValue([filePath]); + (fs.access as jest.Mock).mockRejectedValue(new Error('File not found')); + + await initializeStatusesFromCache( + context, + smellsCacheManager, + smellsViewProvider, + ); + + expect(smellsCacheManager.clearCachedSmellsForFile).toHaveBeenCalledWith( + filePath, + ); + expect(ecoOutput.trace).toHaveBeenCalledWith( + expect.stringContaining('File not found - removing from cache'), + ); + }); + + it('should set status for files with smells', async () => { + const filePath = `${mockWorkspacePath}/file.py`; + const mockSmells = [{ id: 'smell1' }]; + (context.workspaceState.get as jest.Mock).mockReturnValue(mockWorkspacePath); + (smellsCacheManager.getAllFilePaths as jest.Mock).mockReturnValue([filePath]); + (fs.access as jest.Mock).mockResolvedValue(undefined); + (smellsCacheManager.getCachedSmells as jest.Mock).mockReturnValue(mockSmells); + + await initializeStatusesFromCache( + context, + smellsCacheManager, + smellsViewProvider, + ); + + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith(filePath, 'passed'); + expect(smellsViewProvider.setSmells).toHaveBeenCalledWith(filePath, mockSmells); + expect(ecoOutput.trace).toHaveBeenCalledWith( + expect.stringContaining('Found 1 smells for file'), + ); + }); + + it('should set status for clean files', async () => { + const filePath = `${mockWorkspacePath}/clean.py`; + (context.workspaceState.get as jest.Mock).mockReturnValue(mockWorkspacePath); + (smellsCacheManager.getAllFilePaths as jest.Mock).mockReturnValue([filePath]); + (fs.access as jest.Mock).mockResolvedValue(undefined); + (smellsCacheManager.getCachedSmells as jest.Mock).mockReturnValue([]); + + await initializeStatusesFromCache( + context, + smellsCacheManager, + smellsViewProvider, + ); + + expect(smellsViewProvider.setStatus).toHaveBeenCalledWith(filePath, 'no_issues'); + expect(ecoOutput.trace).toHaveBeenCalledWith( + expect.stringContaining('File has no smells'), + ); + }); + + it('should log correct summary statistics', async () => { + const files = [ + `${mockWorkspacePath}/file1.py`, // with smells + `${mockWorkspacePath}/file2.py`, // clean + '/outside/path/file3.py', // outside workspace + `${mockWorkspacePath}/missing.py`, // will fail access + ]; + (context.workspaceState.get as jest.Mock).mockReturnValue(mockWorkspacePath); + (smellsCacheManager.getAllFilePaths as jest.Mock).mockReturnValue(files); + (fs.access as jest.Mock) + .mockResolvedValueOnce(undefined) // file1.py exists + .mockResolvedValueOnce(undefined) // file2.py exists + .mockRejectedValueOnce(new Error('File not found')); // missing.py doesn't exist + (smellsCacheManager.getCachedSmells as jest.Mock) + .mockReturnValueOnce([{ id: 'smell1' }]) // file1.py has smells + .mockReturnValueOnce([]); // file2.py is clean + + await initializeStatusesFromCache( + context, + smellsCacheManager, + smellsViewProvider, + ); + + expect(ecoOutput.info).toHaveBeenCalledWith( + expect.stringContaining( + '2 valid files (1 with smells, 1 clean), 2 files removed from cache', + ), + ); + }); +}); From 6b9fc0c0ba3a32488e7811e2f58d9049c303553c Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Wed, 26 Mar 2025 15:40:54 -0400 Subject: [PATCH 095/121] added trackedDiffEditors.test.ts file --- src/utils/trackedDiffEditors.ts | 2 +- test/utils/handleSmellSettings.test.ts | 134 ------------------------- test/utils/hashDocs.test.ts | 56 ----------- test/utils/trackedDiffEditors.test.ts | 115 +++++++++++++++++++++ 4 files changed, 116 insertions(+), 191 deletions(-) delete mode 100644 test/utils/handleSmellSettings.test.ts delete mode 100644 test/utils/hashDocs.test.ts create mode 100644 test/utils/trackedDiffEditors.test.ts diff --git a/src/utils/trackedDiffEditors.ts b/src/utils/trackedDiffEditors.ts index 6d84967..be54e61 100644 --- a/src/utils/trackedDiffEditors.ts +++ b/src/utils/trackedDiffEditors.ts @@ -1,7 +1,7 @@ // utils/trackedDiffEditors.ts import * as vscode from 'vscode'; -const trackedDiffs = new Set(); +export const trackedDiffs = new Set(); export function registerDiffEditor( original: vscode.Uri, diff --git a/test/utils/handleSmellSettings.test.ts b/test/utils/handleSmellSettings.test.ts deleted file mode 100644 index 0598534..0000000 --- a/test/utils/handleSmellSettings.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - handleSmellFilterUpdate, - getEnabledSmells, - formatSmellName, -} from '../../src/utils/handleSmellSettings'; -import { wipeWorkCache } from '../../src/commands/wipeWorkCache'; -import { ContextManager } from '../../src/context/contextManager'; -import vscode from '../mocks/vscode-mock'; - -jest.mock('../../src/commands/wipeWorkCache', () => ({ - wipeWorkCache: jest.fn(), -})); - -describe('Settings Page - handleSmellSettings.ts', () => { - let contextManagerMock: ContextManager; - - beforeEach(() => { - jest.clearAllMocks(); - contextManagerMock = { - getWorkspaceData: jest.fn(), - setWorkspaceData: jest.fn(), - } as unknown as ContextManager; - }); - - describe('getEnabledSmells', () => { - it('should return the current enabled smells from settings', () => { - const currentConfig = { - 'cached-repeated-calls': { - enabled: true, - colour: 'rgba(255, 204, 0, 0.5)', - }, - 'long-element-chain': { - enabled: false, - colour: 'rgba(255, 204, 0, 0.5)', - }, - }; - - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValueOnce({ - get: jest.fn().mockReturnValue(currentConfig), - } as any); - - const enabledSmells = getEnabledSmells(); - - expect(enabledSmells).toEqual({ - 'cached-repeated-calls': true, - 'long-element-chain': false, - }); - }); - - it('should return an empty object if no smells are set', () => { - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValueOnce({ - get: jest.fn().mockReturnValue({}), - } as any); - - const enabledSmells = getEnabledSmells(); - expect(enabledSmells).toEqual({}); - }); - }); - - describe('handleSmellFilterUpdate', () => { - it('should detect when a smell is enabled and notify the user', () => { - const previousSmells = { 'cached-repeated-calls': false }; - const currentConfig = { - 'cached-repeated-calls': { - enabled: true, - colour: 'rgba(255, 204, 0, 0.5)', - }, - }; - - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValueOnce({ - get: jest.fn().mockReturnValue(currentConfig), - } as any); - - handleSmellFilterUpdate(previousSmells, contextManagerMock); - - expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( - 'Eco: Enabled detection of Cached Repeated Calls.', - ); - expect(wipeWorkCache).toHaveBeenCalledWith(contextManagerMock, 'settings'); - }); - - it('should detect when a smell is disabled and notify the user', () => { - const previousSmells = { 'long-element-chain': true }; - const currentConfig = { - 'long-element-chain': { - enabled: false, - colour: 'rgba(255, 204, 0, 0.5)', - }, - }; - - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValueOnce({ - get: jest.fn().mockReturnValue(currentConfig), - } as any); - - handleSmellFilterUpdate(previousSmells, contextManagerMock); - - expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( - 'Eco: Disabled detection of Long Element Chain.', - ); - expect(wipeWorkCache).toHaveBeenCalledWith(contextManagerMock, 'settings'); - }); - - it('should not wipe cache if no smells changed', () => { - const previousSmells = { 'cached-repeated-calls': true }; - const currentConfig = { - 'cached-repeated-calls': { - enabled: true, - colour: 'rgba(255, 204, 0, 0.5)', - }, - }; - - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValueOnce({ - get: jest.fn().mockReturnValue(currentConfig), - } as any); - - handleSmellFilterUpdate(previousSmells, contextManagerMock); - - expect(wipeWorkCache).not.toHaveBeenCalled(); - expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); - }); - }); - - describe('formatSmellName', () => { - it('should format kebab-case smell names to a readable format', () => { - expect(formatSmellName('cached-repeated-calls')).toBe('Cached Repeated Calls'); - expect(formatSmellName('long-element-chain')).toBe('Long Element Chain'); - expect(formatSmellName('string-concat-loop')).toBe('String Concat Loop'); - }); - - it('should return an empty string if given an empty input', () => { - expect(formatSmellName('')).toBe(''); - }); - }); -}); diff --git a/test/utils/hashDocs.test.ts b/test/utils/hashDocs.test.ts deleted file mode 100644 index 8f6aa27..0000000 --- a/test/utils/hashDocs.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ContextManager } from '../../src/context/contextManager'; - -import { TextDocument } from '../mocks/vscode-mock'; -import { updateHash } from '../../src/utils/hashDocs'; - -import crypto from 'crypto'; - -jest.mock('crypto'); - -describe('Hashing Tools', () => { - let contextManagerMock: ContextManager; - - beforeEach(() => { - // Reset all mocks before each test - jest.clearAllMocks(); - - // Mock ContextManager - contextManagerMock = { - getWorkspaceData: jest.fn(), - setWorkspaceData: jest.fn(), - } as unknown as ContextManager; - }); - - it('should do nothing if the document hash has not changed', async () => { - jest.spyOn(contextManagerMock, 'getWorkspaceData').mockReturnValueOnce({ - 'fake.py': 'mocked-hash', - }); - - await updateHash(contextManagerMock, TextDocument as any); - - expect(crypto.createHash).toHaveBeenCalled(); - expect(contextManagerMock.setWorkspaceData).not.toHaveBeenCalled(); - }); - - it('should update the workspace storage if the doc hash changed', async () => { - jest.spyOn(contextManagerMock, 'getWorkspaceData').mockReturnValueOnce({ - 'fake.py': 'someHash', - }); - - await updateHash(contextManagerMock, TextDocument as any); - - expect(crypto.createHash).toHaveBeenCalled(); - expect(contextManagerMock.setWorkspaceData).toHaveBeenCalled(); - }); - - it('should update the workspace storage if no hash exists for the doc', async () => { - jest.spyOn(contextManagerMock, 'getWorkspaceData').mockReturnValueOnce({ - 'otherFake.py': 'someHash', - }); - - await updateHash(contextManagerMock, TextDocument as any); - - expect(crypto.createHash).toHaveBeenCalled(); - expect(contextManagerMock.setWorkspaceData).toHaveBeenCalled(); - }); -}); diff --git a/test/utils/trackedDiffEditors.test.ts b/test/utils/trackedDiffEditors.test.ts new file mode 100644 index 0000000..252f58b --- /dev/null +++ b/test/utils/trackedDiffEditors.test.ts @@ -0,0 +1,115 @@ +// utils/trackedDiffEditors.test.ts +import * as vscode from 'vscode'; +import { + registerDiffEditor, + isTrackedDiffEditor, + closeAllTrackedDiffEditors, + trackedDiffs, +} from '../../src/utils/trackedDiffEditors'; + +// Mock the vscode API +jest.mock('vscode', () => ({ + window: { + tabGroups: { + close: jest.fn().mockResolvedValue(true), + all: [], + }, + }, + Uri: { + parse: jest.fn(), + }, +})); + +describe('trackedDiffEditors', () => { + const mockUri1 = { toString: () => 'file:///test1.txt' } as vscode.Uri; + const mockUri2 = { toString: () => 'file:///test2.txt' } as vscode.Uri; + const mockUri3 = { toString: () => 'file:///test3.txt' } as vscode.Uri; + + beforeEach(() => { + // Clear the trackedDiffs set before each test + trackedDiffs.clear(); + // Reset the mock implementation + (vscode.window.tabGroups.close as jest.Mock).mockClear().mockResolvedValue(true); + // Reset tab groups mock + (vscode.window.tabGroups.all as any) = []; + }); + + describe('registerDiffEditor', () => { + it('should register a diff editor with given URIs', () => { + registerDiffEditor(mockUri1, mockUri2); + expect(isTrackedDiffEditor(mockUri1, mockUri2)).toBe(true); + }); + + it('should not register unrelated URIs', () => { + registerDiffEditor(mockUri1, mockUri2); + expect(isTrackedDiffEditor(mockUri1, mockUri3)).toBe(false); + expect(isTrackedDiffEditor(mockUri2, mockUri3)).toBe(false); + }); + }); + + describe('isTrackedDiffEditor', () => { + it('should return true for registered diff editors', () => { + registerDiffEditor(mockUri1, mockUri2); + expect(isTrackedDiffEditor(mockUri1, mockUri2)).toBe(true); + }); + + it('should return false for unregistered diff editors', () => { + expect(isTrackedDiffEditor(mockUri1, mockUri2)).toBe(false); + }); + + it('should be case sensitive for URIs', () => { + const mockUriLower = { toString: () => 'file:///test1.txt' } as vscode.Uri; + const mockUriUpper = { toString: () => 'FILE:///TEST1.TXT' } as vscode.Uri; + registerDiffEditor(mockUriLower, mockUri2); + expect(isTrackedDiffEditor(mockUriUpper, mockUri2)).toBe(false); + }); + }); + + describe('closeAllTrackedDiffEditors', () => { + it('should close all tracked diff editors', async () => { + // Setup mock tabs + const mockTab1 = { + input: { original: mockUri1, modified: mockUri2 }, + }; + const mockTab2 = { + input: { original: mockUri3, modified: mockUri2 }, + }; + const mockTab3 = { + input: { somethingElse: true }, + }; + + // Mock the tabGroups.all + (vscode.window.tabGroups.all as any) = [ + { tabs: [mockTab1, mockTab2] }, + { tabs: [mockTab3] }, + ]; + + registerDiffEditor(mockUri1, mockUri2); + + await closeAllTrackedDiffEditors(); + + expect(vscode.window.tabGroups.close).toHaveBeenCalledTimes(1); + expect(vscode.window.tabGroups.close).toHaveBeenCalledWith(mockTab1, true); + }); + + it('should clear all tracked diffs after closing', async () => { + registerDiffEditor(mockUri1, mockUri2); + (vscode.window.tabGroups.all as any) = []; + + await closeAllTrackedDiffEditors(); + + expect(isTrackedDiffEditor(mockUri1, mockUri2)).toBe(false); + }); + + it('should handle empty tabs', async () => { + // Ensure no tabs exist + (vscode.window.tabGroups.all as any) = []; + // Don't register any editors for this test + + await closeAllTrackedDiffEditors(); + + expect(vscode.window.tabGroups.close).not.toHaveBeenCalled(); + expect(trackedDiffs.size).toBe(0); + }); + }); +}); From ec18c8dd8281a26aacfddd2bdd8ebda840ebd971 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Wed, 26 Mar 2025 15:49:55 -0400 Subject: [PATCH 096/121] added refactorActionButtons.test.ts file --- test/utils/smellsData.test.ts | 211 ++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 test/utils/smellsData.test.ts diff --git a/test/utils/smellsData.test.ts b/test/utils/smellsData.test.ts new file mode 100644 index 0000000..fdff91b --- /dev/null +++ b/test/utils/smellsData.test.ts @@ -0,0 +1,211 @@ +// smellsData.test.ts +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { + loadSmells, + saveSmells, + getFilterSmells, + getEnabledSmells, + getAcronymByMessageId, + getNameByMessageId, + getDescriptionByMessageId, + FilterSmellConfig, +} from '../../src/utils/smellsData'; + +// Mock the modules +jest.mock('vscode'); +jest.mock('fs'); +jest.mock('path'); + +const mockSmellsConfig: Record = { + 'long-parameter-list': { + name: 'Long Parameter List', + message_id: 'R0913', + acronym: 'LPL', + smell_description: 'Method has too many parameters', + enabled: true, + analyzer_options: { + max_params: { + label: 'Maximum Parameters', + description: 'Maximum allowed parameters', + value: 5, + }, + }, + }, + 'duplicate-code': { + name: 'Duplicate Code', + message_id: 'R0801', + acronym: 'DC', + smell_description: 'Code duplication detected', + enabled: false, + }, +}; + +// Mock console.error to prevent test output pollution +const originalConsoleError = console.error; +beforeAll(() => { + console.error = jest.fn(); +}); + +afterAll(() => { + console.error = originalConsoleError; +}); + +describe('smellsData', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mocks + (fs.existsSync as jest.Mock).mockReturnValue(true); + (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockSmellsConfig)); + (fs.writeFileSync as jest.Mock).mockImplementation(() => {}); + + // Mock path.join to return predictable paths + (path.join as jest.Mock).mockImplementation((...args: string[]) => + args.join('/').replace(/\\/g, '/'), + ); + }); + + describe('loadSmells', () => { + it('should load smells configuration successfully', () => { + loadSmells('working'); + + // Update path expectation to match actual implementation + expect(fs.readFileSync).toHaveBeenCalledWith( + expect.stringContaining('data/working_smells_config.json'), + 'utf-8', + ); + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); + }); + + it('should show error message when file is missing', () => { + (fs.existsSync as jest.Mock).mockReturnValue(false); + + loadSmells('working'); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Configuration file missing: smells.json could not be found.', + ); + }); + + it('should show error message when file parsing fails', () => { + (fs.readFileSync as jest.Mock).mockImplementation(() => { + throw new Error('Parse error'); + }); + + loadSmells('working'); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Error loading smells.json. Please check the file format.', + ); + expect(console.error).toHaveBeenCalledWith( + 'ERROR: Failed to parse smells.json', + expect.any(Error), + ); + }); + }); + + describe('saveSmells', () => { + it('should save smells configuration successfully', () => { + saveSmells(mockSmellsConfig); + + // Update path expectation to match actual implementation + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('data/working_smells_config.json'), + JSON.stringify(mockSmellsConfig, null, 2), + ); + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); + }); + + it('should show error message when file write fails', () => { + (fs.writeFileSync as jest.Mock).mockImplementation(() => { + throw new Error('Write error'); + }); + + saveSmells(mockSmellsConfig); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Error saving smells.json.', + ); + expect(console.error).toHaveBeenCalledWith( + 'ERROR: Failed to write smells.json', + expect.any(Error), + ); + }); + }); + + describe('getFilterSmells', () => { + it('should return the loaded filter smells', () => { + loadSmells('working'); + const result = getFilterSmells(); + + expect(result).toEqual(mockSmellsConfig); + }); + }); + + describe('getEnabledSmells', () => { + it('should return only enabled smells with parsed options', () => { + loadSmells('working'); + const result = getEnabledSmells(); + + expect(result).toEqual({ + 'long-parameter-list': { + message_id: 'R0913', + acronym: 'LPL', + options: { + max_params: 5, + }, + }, + }); + }); + }); + + describe('getAcronymByMessageId', () => { + it('should return the correct acronym for a message ID', () => { + loadSmells('working'); + const result = getAcronymByMessageId('R0913'); + + expect(result).toBe('LPL'); + }); + + it('should return undefined for unknown message ID', () => { + loadSmells('working'); + const result = getAcronymByMessageId('UNKNOWN'); + + expect(result).toBeUndefined(); + }); + }); + + describe('getNameByMessageId', () => { + it('should return the correct name for a message ID', () => { + loadSmells('working'); + const result = getNameByMessageId('R0913'); + + expect(result).toBe('Long Parameter List'); + }); + + it('should return undefined for unknown message ID', () => { + loadSmells('working'); + const result = getNameByMessageId('UNKNOWN'); + + expect(result).toBeUndefined(); + }); + }); + + describe('getDescriptionByMessageId', () => { + it('should return the correct description for a message ID', () => { + loadSmells('working'); + const result = getDescriptionByMessageId('R0913'); + + expect(result).toBe('Method has too many parameters'); + }); + + it('should return undefined for unknown message ID', () => { + loadSmells('working'); + const result = getDescriptionByMessageId('UNKNOWN'); + + expect(result).toBeUndefined(); + }); + }); +}); From 7204c561a82df640383bae01ded0467b5df5c043 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:38:05 -0400 Subject: [PATCH 097/121] Added fields to the package.json to prepare for publishing --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 45ced94..ae5d2fa 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "ecooptimizer", + "publisher": "ecooptimizers", "displayName": "EcoOptimizer VS Code Plugin", "contributors": [ "Sevhena Walker", @@ -9,6 +10,10 @@ "Nivetha Kuruparan" ], "description": "VS Code Plugin for EcoOptimizer Refactoring Tool", + "repository": { + "type": "git", + "url": "https://github.com/ssm-lab/capstone--sco-vs-code-plugin" + }, "version": "0.0.1", "engines": { "vscode": "^1.92.0" @@ -25,6 +30,7 @@ "test": "./test" }, "scripts": { + "deploy": "vsce publish --yarn", "vscode:prepublish": "npm run package", "compile": "webpack", "test": "jest --verbose", From af5224483f2185d1025ae40e0d371c8ba956938d Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Fri, 28 Mar 2025 18:22:59 -0400 Subject: [PATCH 098/121] added more tests --- src/ui/FileHighlighter.ts | 12 +- test/commands/exportMetricsData.test.ts | 186 +++++++++++++ test/mocks/vscode-mock.ts | 23 +- test/ui/fileHighlighter.test.ts | 348 ++++++++++++++++++------ 4 files changed, 461 insertions(+), 108 deletions(-) create mode 100644 test/commands/exportMetricsData.test.ts diff --git a/src/ui/FileHighlighter.ts b/src/ui/FileHighlighter.ts index 68cd271..e91d62b 100644 --- a/src/ui/FileHighlighter.ts +++ b/src/ui/FileHighlighter.ts @@ -9,7 +9,7 @@ import { getEnabledSmells } from '../utils/smellsData'; * lines to highlight and applies decorations to the editor accordingly. */ export class FileHighlighter { - private static instance: FileHighlighter; + private static instance: FileHighlighter | undefined; private decorations: vscode.TextEditorDecorationType[] = []; private constructor(private smellsCacheManager: SmellsCacheManager) { @@ -61,9 +61,7 @@ export class FileHighlighter { */ public updateHighlightsForVisibleEditors(): void { vscode.window.visibleTextEditors.forEach((editor) => { - console.log('editor:', editor); if (!editor.document.fileName.endsWith('.py')) { - console.log('not a python file'); return; } this.highlightSmells(editor); @@ -85,18 +83,12 @@ export class FileHighlighter { * @param editor - The text editor to apply highlights to. */ public highlightSmells(editor: vscode.TextEditor): void { - console.log('reseting decorations'); this.resetHighlights(); - console.log('highlighting smells'); - console.log('cache manager:', this.smellsCacheManager); - const smells = this.smellsCacheManager.getCachedSmells( editor.document.uri.fsPath, ); - console.log('smells', smells); - if (!smells) { return; } @@ -116,8 +108,6 @@ export class FileHighlighter { const enabledSmells = getEnabledSmells(); - console.log(enabledSmells); - activeSmells.forEach((smellType) => { const smellColour = smellColours[smellType]; diff --git a/test/commands/exportMetricsData.test.ts b/test/commands/exportMetricsData.test.ts new file mode 100644 index 0000000..84a59ba --- /dev/null +++ b/test/commands/exportMetricsData.test.ts @@ -0,0 +1,186 @@ +// test/exportMetrics.test.ts +import * as vscode from 'vscode'; +import { dirname } from 'path'; +import { writeFileSync } from 'fs'; +import { exportMetricsData } from '../../src/commands/views/exportMetricsData'; +import { envConfig } from '../../src/utils/envConfig'; +import * as fs from 'fs'; + +// Mock dependencies +jest.mock('path'); +jest.mock('fs'); + +describe('exportMetricsData', () => { + let mockContext: vscode.ExtensionContext; + const mockMetricsData = { + '/path/to/file1.py': { + energySaved: 0.5, + smellType: 'test-smell', + timestamp: Date.now(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + // Setup mock context + mockContext = { + workspaceState: { + get: jest.fn(), + update: jest.fn(), + }, + } as unknown as vscode.ExtensionContext; + + // Mock path.dirname + (dirname as jest.Mock).mockImplementation((path) => `/parent/${path}`); + + // Mock fs.writeFileSync + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + }); + + it('should show info message when no metrics data exists', async () => { + (mockContext.workspaceState.get as jest.Mock).mockImplementation((key) => { + console.log('Mock:', key, envConfig.WORKSPACE_METRICS_DATA); + if (key === envConfig.WORKSPACE_METRICS_DATA) return {}; + return undefined; + }); + + await exportMetricsData(mockContext); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'No metrics data available to export.', + ); + }); + + it('should show error when no workspace path is configured', async () => { + (mockContext.workspaceState.get as jest.Mock).mockImplementation((key) => { + if (key === envConfig.WORKSPACE_CONFIGURED_PATH) return undefined; + if (key === envConfig.WORKSPACE_METRICS_DATA) return mockMetricsData; + return undefined; // No workspace path + }); + + await exportMetricsData(mockContext); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'No configured workspace path found.', + ); + }); + + it('should export to workspace directory when path is a directory', async () => { + const workspacePath = '/workspace/path'; + + (mockContext.workspaceState.get as jest.Mock).mockImplementation((key) => { + if (key === envConfig.WORKSPACE_METRICS_DATA) return mockMetricsData; + if (key === envConfig.WORKSPACE_CONFIGURED_PATH) return workspacePath; + return undefined; + }); + + // Mock fs.stat to return directory + (vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ + type: vscode.FileType.Directory, + }); + + await exportMetricsData(mockContext); + + expect(vscode.Uri.joinPath).toHaveBeenCalledWith( + expect.anything(), + 'metrics-data.json', + ); + expect(writeFileSync).toHaveBeenCalled(); + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining('metrics-data.json'), + ); + }); + + it('should export to parent directory when path is a file', async () => { + const workspacePath = '/workspace/path/file.txt'; + + (mockContext.workspaceState.get as jest.Mock).mockImplementation((key) => { + if (key === envConfig.WORKSPACE_METRICS_DATA) return mockMetricsData; + if (key === envConfig.WORKSPACE_CONFIGURED_PATH) return workspacePath; + return undefined; + }); + + // Mock fs.stat to return file + (vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ + type: vscode.FileType.File, + }); + + await exportMetricsData(mockContext); + + expect(dirname).toHaveBeenCalledWith(workspacePath); + expect(vscode.Uri.joinPath).toHaveBeenCalledWith( + expect.anything(), + 'metrics-data.json', + ); + expect(writeFileSync).toHaveBeenCalled(); + }); + + it('should show error for invalid workspace path type', async () => { + const workspacePath = '/workspace/path'; + + (mockContext.workspaceState.get as jest.Mock).mockImplementation((key) => { + if (key === envConfig.WORKSPACE_METRICS_DATA) return mockMetricsData; + if (key === envConfig.WORKSPACE_CONFIGURED_PATH) return workspacePath; + return undefined; + }); + + // Mock fs.stat to return unknown type + (vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ + type: vscode.FileType.Unknown, + }); + + await exportMetricsData(mockContext); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Invalid workspace path type.', + ); + }); + + it('should handle filesystem access errors', async () => { + const workspacePath = '/workspace/path'; + + (mockContext.workspaceState.get as jest.Mock).mockImplementation((key) => { + if (key === envConfig.WORKSPACE_METRICS_DATA) return mockMetricsData; + if (key === envConfig.WORKSPACE_CONFIGURED_PATH) return workspacePath; + return undefined; + }); + + // Mock fs.stat to throw error + (vscode.workspace.fs.stat as jest.Mock).mockRejectedValue( + new Error('Access denied'), + ); + + await exportMetricsData(mockContext); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + expect.stringContaining('Failed to access workspace path'), + ); + }); + + it('should handle file write errors', async () => { + const workspacePath = '/workspace/path'; + + (mockContext.workspaceState.get as jest.Mock).mockImplementation((key) => { + if (key === envConfig.WORKSPACE_METRICS_DATA) return mockMetricsData; + if (key === envConfig.WORKSPACE_CONFIGURED_PATH) return workspacePath; + return undefined; + }); + + // Mock fs.stat to return directory + (vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ + type: vscode.FileType.Directory, + }); + + // Mock writeFileSync to throw error + (writeFileSync as jest.Mock).mockImplementation(() => { + throw new Error('Write failed'); + }); + + await exportMetricsData(mockContext); + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + expect.stringContaining('Failed to export metrics data'), + ); + }); +}); diff --git a/test/mocks/vscode-mock.ts b/test/mocks/vscode-mock.ts index c3ac2ee..52f9771 100644 --- a/test/mocks/vscode-mock.ts +++ b/test/mocks/vscode-mock.ts @@ -18,7 +18,7 @@ export const TextDocument = { getText: jest.fn(() => config.docText), fileName: config.filePath, languageId: 'python', - lineAt: jest.fn((line: number) => { + lineAt: jest.fn((_line: number) => { return { text: 'Mock line text', }; @@ -101,22 +101,18 @@ export enum FileType { File = 2, } -export const fileStat = { - type: FileType.Directory, -}; - interface Workspace { getConfiguration: jest.Mock; findFiles: jest.Mock; fs: { readFile: jest.Mock; writeFile: jest.Mock; - stat: typeof fileStat; + stat: jest.Mock; }; } export const workspace: Workspace = { - getConfiguration: jest.fn((section?: string) => ({ + getConfiguration: jest.fn((_section?: string) => ({ get: jest.fn(() => config.configGet), update: jest.fn(), })), @@ -124,7 +120,7 @@ export const workspace: Workspace = { fs: { readFile: jest.fn(), writeFile: jest.fn(), - stat: fileStat, + stat: jest.fn(), }, }; @@ -216,8 +212,17 @@ export const Uri = { scheme: 'file', path, fsPath: path, - toString: () => path, + toString: (): string => path, })), + joinPath: jest.fn((start: string, end: string) => { + const newPath = start + end; + return { + scheme: 'file', + path: newPath, + fsPath: newPath, + toString: (): string => newPath, + }; + }), parse: jest.fn(), }; diff --git a/test/ui/fileHighlighter.test.ts b/test/ui/fileHighlighter.test.ts index db45873..88e02ef 100644 --- a/test/ui/fileHighlighter.test.ts +++ b/test/ui/fileHighlighter.test.ts @@ -1,122 +1,294 @@ +// test/fileHighlighter.test.ts +import * as vscode from 'vscode'; import { FileHighlighter } from '../../src/ui/fileHighlighter'; -import { ContextManager } from '../../src/context/contextManager'; -import vscode from '../mocks/vscode-mock'; -import { HoverManager } from '../../src/ui/hoverManager'; -import { MarkdownString } from 'vscode'; +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; +import { ConfigManager } from '../../src/context/configManager'; +import * as smellsData from '../../src/utils/smellsData'; +// Mock dependencies jest.mock('vscode'); +jest.mock('../../src/context/SmellsCacheManager'); +jest.mock('../../src/context/configManager'); +jest.mock('../../src/utils/smellsData'); -describe('File Highlighter', () => { - let contextManagerMock: ContextManager; +describe('FileHighlighter', () => { + let smellsCacheManager: { getCachedSmells: jest.Mock; onSmellsUpdated: jest.Mock }; let fileHighlighter: FileHighlighter; beforeEach(() => { - // Reset all mocks before each test jest.clearAllMocks(); - // Mock ContextManager - contextManagerMock = { - getWorkspaceData: jest.fn(), - setWorkspaceData: jest.fn(), - } as unknown as ContextManager; + // Setup mock instances + smellsCacheManager = { + getCachedSmells: jest.fn(), + onSmellsUpdated: jest.fn(), + }; + FileHighlighter['instance'] = undefined; + fileHighlighter = FileHighlighter.getInstance( + smellsCacheManager as unknown as SmellsCacheManager, + ); + + // Mock ConfigManager + (ConfigManager.get as jest.Mock).mockImplementation((key: string) => { + switch (key) { + case 'smellsColours': + return { smell1: 'rgba(255,0,0,0.5)', smell2: 'rgba(0,0,255,0.5)' }; + case 'useSingleColour': + return false; + case 'singleHighlightColour': + return 'rgba(255,204,0,0.5)'; + case 'highlightStyle': + return 'underline'; + default: + return undefined; + } + }); - fileHighlighter = FileHighlighter.getInstance(contextManagerMock); + // Mock createTextEditorDecorationType + (vscode.window.createTextEditorDecorationType as jest.Mock).mockImplementation( + () => ({ + dispose: jest.fn(), + }), + ); }); - it('should not reset highlight decorations on first init', () => { - const smells = [ - { - symbol: 'smell1', - occurences: [{ line: 1 }], - }, - ] as unknown as Smell[]; - const currentConfig = { - smell1: { - enabled: true, - colour: 'rgba(1, 50, 0, 0.5)', - }, - }; + afterEach(() => { + jest.restoreAllMocks(); // Cleans up all spy mocks + (vscode.window.createTextEditorDecorationType as jest.Mock).mockClear(); + }); + + describe('getInstance', () => { + it('should return singleton instance', () => { + const instance1 = FileHighlighter.getInstance( + smellsCacheManager as unknown as SmellsCacheManager, + ); + const instance2 = FileHighlighter.getInstance( + smellsCacheManager as unknown as SmellsCacheManager, + ); + expect(instance1).toBe(instance2); + }); + }); + + describe('updateHighlightsForVisibleEditors', () => { + it('should call highlightSmells for each visible Python editor', () => { + // Mock highlightSmells to track calls + const highlightSpy = jest.spyOn(fileHighlighter, 'highlightSmells'); + + // Create a non-Python editor + const nonPythonEditor = { + document: { + fileName: '/path/to/file.js', + uri: { fsPath: '/path/to/file.js' }, + }, + } as unknown as vscode.TextEditor; - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValueOnce({ - get: jest.fn().mockReturnValue(currentConfig), - } as any); + vscode.window.visibleTextEditors = [ + nonPythonEditor, + vscode.window.activeTextEditor!, + ]; - jest.spyOn(HoverManager, 'getInstance').mockReturnValueOnce({ - hoverContent: 'hover content' as unknown as MarkdownString, - } as unknown as HoverManager); + fileHighlighter.updateHighlightsForVisibleEditors(); - fileHighlighter.highlightSmells(vscode.window.activeTextEditor, smells); + // Verify highlightSmells was called exactly once (for the Python editor) + expect(highlightSpy).toHaveBeenCalledTimes(1); - // Assert decorations were set - expect(fileHighlighter['decorations'][0].dispose).not.toHaveBeenCalled(); + // Clean up spy + highlightSpy.mockRestore(); + }); }); - it('should create decorations', () => { - const color = 'red'; - const decoration = fileHighlighter['getDecoration'](color, 'underline'); + describe('updateHighlightsForFile', () => { + it('should call highlightSmells when matching Python file is visible', () => { + const highlightSpy = jest.spyOn(fileHighlighter, 'highlightSmells'); + + vscode.window.visibleTextEditors = [vscode.window.activeTextEditor!]; + + fileHighlighter['updateHighlightsForFile']('fake.py'); - // Assert decoration was created - expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalled(); - expect(decoration).toBeDefined(); + expect(highlightSpy).toHaveBeenCalledTimes(1); + highlightSpy.mockRestore(); + }); + + it('should not call highlightSmells for non-matching files', () => { + const highlightSpy = jest.spyOn(fileHighlighter, 'highlightSmells'); + + fileHighlighter['updateHighlightsForFile']('/path/to/other.py'); + + expect(highlightSpy).not.toHaveBeenCalled(); + highlightSpy.mockRestore(); + }); + + it('should not call highlightSmells for non-Python files', () => { + const highlightSpy = jest.spyOn(fileHighlighter, 'highlightSmells'); + + fileHighlighter['updateHighlightsForFile']('/path/to/file.js'); + + expect(highlightSpy).not.toHaveBeenCalled(); + highlightSpy.mockRestore(); + }); }); - it('should highlight smells', () => { - const smells = [ - { - symbol: 'smell1', - occurences: [{ line: 1 }], - }, - ] as unknown as Smell[]; - const currentConfig = { - smell1: { - enabled: true, - colour: 'rgba(88, 101, 200, 0.5)', - }, - }; + describe('highlightSmells', () => { + const mockEditor = vscode.window.activeTextEditor; + it('should highlight smells when cache has data', () => { + const mockSmells = [ + { + symbol: 'smell1', + occurences: [{ line: 1 }, { line: 2 }], + }, + { + symbol: 'smell2', + occurences: [{ line: 3 }], + }, + ] as unknown as Smell[]; + + jest.spyOn(smellsData, 'getEnabledSmells').mockReturnValueOnce({ + smell1: {} as any, + smell2: {} as any, + }); + + (smellsCacheManager.getCachedSmells as jest.Mock).mockReturnValueOnce( + mockSmells, + ); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValueOnce({ - get: jest.fn().mockReturnValue(currentConfig), - } as any); + console.log( + 'Mock getCachedSmells implementation:', + smellsCacheManager.getCachedSmells.mock.results, + ); - jest.spyOn(HoverManager, 'getInstance').mockReturnValueOnce({ - hoverContent: 'hover content' as unknown as MarkdownString, - } as unknown as HoverManager); + const editor = vscode.window.activeTextEditor; - fileHighlighter.highlightSmells(vscode.window.activeTextEditor, smells); + fileHighlighter.highlightSmells(editor!); - expect(vscode.window.activeTextEditor.setDecorations).toHaveBeenCalled(); - expect( - vscode.window.activeTextEditor.setDecorations.mock.calls[0][1], - ).toHaveLength(1); + expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledTimes(2); + expect(editor!.setDecorations).toHaveBeenCalledTimes(2); + }); + + it('should not highlight when cache has no data', () => { + smellsCacheManager.getCachedSmells.mockReturnValueOnce(undefined); + fileHighlighter.highlightSmells(mockEditor!); + expect(mockEditor!.setDecorations).not.toHaveBeenCalled(); + }); + + it('should only highlight enabled smells', () => { + jest.spyOn(smellsData, 'getEnabledSmells').mockReturnValueOnce({ + smell1: {} as any, + }); + + const mockSmells = [ + { + symbol: 'smell1', + occurences: [{ line: 1 }], + }, + { + symbol: 'smell2', + occurences: [{ line: 2 }], + }, + ]; + + smellsCacheManager.getCachedSmells.mockReturnValueOnce(mockSmells); + + fileHighlighter.highlightSmells(mockEditor!); + + expect( + (mockEditor?.setDecorations as jest.Mock).mock.calls[0][1], + ).toHaveLength(1); + }); + + it('should skip invalid line numbers', () => { + const mockSmells = [ + { + symbol: 'smell1', + occurences: [{ line: 100 }], // Invalid line number + }, + ]; + + jest.spyOn(smellsData, 'getEnabledSmells').mockReturnValueOnce({ + smell1: {} as any, + smell2: {} as any, + }); + + smellsCacheManager.getCachedSmells.mockReturnValueOnce(mockSmells); + + fileHighlighter.highlightSmells(mockEditor!); + + expect(mockEditor?.setDecorations).toHaveBeenCalledWith(expect.anything(), []); + }); }); - it('should reset highlight decorations on subsequent calls', () => { - const smells = [ - { - symbol: 'smell1', - occurences: [{ line: 1 }], - }, - ] as unknown as Smell[]; - const currentConfig = { - smell1: { - enabled: true, - colour: 'rgba(255, 204, 0, 0.5)', - }, - }; + describe('resetHighlights', () => { + it('should dispose all decorations', () => { + const mockEditor = vscode.window.activeTextEditor; + const mockDecoration = { dispose: jest.fn() }; + ( + vscode.window.createTextEditorDecorationType as jest.Mock + ).mockReturnValueOnce(mockDecoration); + + jest.spyOn(smellsData, 'getEnabledSmells').mockReturnValueOnce({ + smell1: {} as any, + smell2: {} as any, + }); + + const mockSmells = [{ symbol: 'smell1', occurences: [{ line: 1 }] }]; + smellsCacheManager.getCachedSmells.mockReturnValueOnce(mockSmells); + + fileHighlighter.highlightSmells(mockEditor!); + fileHighlighter.resetHighlights(); + + expect(mockDecoration.dispose).toHaveBeenCalled(); + expect(fileHighlighter['decorations']).toHaveLength(0); + }); + }); + + describe('getDecoration', () => { + it('should create underline decoration', () => { + (ConfigManager.get as jest.Mock).mockImplementation((key: string) => + key === 'highlightStyle' ? 'underline' : undefined, + ); + + expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledWith({ + textDecoration: 'wavy rgba(255,0,0,0.5) underline 1px', + }); + }); - jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue({ - get: jest.fn().mockReturnValue(currentConfig), - } as any); + it('should create flashlight decoration', () => { + (ConfigManager.get as jest.Mock).mockImplementation((key: string) => + key === 'highlightStyle' ? 'flashlight' : undefined, + ); - jest.spyOn(HoverManager, 'getInstance').mockReturnValue({ - hoverContent: 'hover content' as unknown as MarkdownString, - } as unknown as HoverManager); + fileHighlighter['getDecoration']('rgba(255,0,0,0.5)', 'flashlight'); + expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledWith({ + isWholeLine: true, + backgroundColor: 'rgba(255,0,0,0.5)', + }); + }); - fileHighlighter.highlightSmells(vscode.window.activeTextEditor, smells); + it('should create border-arrow decoration', () => { + (ConfigManager.get as jest.Mock).mockImplementation((key: string) => + key === 'highlightStyle' ? 'border-arrow' : undefined, + ); - fileHighlighter.highlightSmells(vscode.window.activeTextEditor, smells); + fileHighlighter['getDecoration']('rgba(255,0,0,0.5)', 'border-arrow'); + expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledWith({ + borderWidth: '1px 2px 1px 0', + borderStyle: 'solid', + borderColor: 'rgba(255,0,0,0.5)', + after: { + contentText: '▶', + margin: '0 0 0 5px', + color: 'rgba(255,0,0,0.5)', + fontWeight: 'bold', + }, + overviewRulerColor: 'rgba(255,0,0,0.5)', + overviewRulerLane: vscode.OverviewRulerLane.Right, + }); + }); - // Assert decorations were set - expect(fileHighlighter['decorations'][0].dispose).toHaveBeenCalled(); + it('should default to underline for unknown styles', () => { + fileHighlighter['getDecoration']('rgba(255,0,0,0.5)', 'unknown'); + expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledWith({ + textDecoration: 'wavy rgba(255,0,0,0.5) underline 1px', + }); + }); }); }); From 2fea22b17557255356ba4cfea16c3df11ca8a064 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Fri, 28 Mar 2025 20:52:14 -0400 Subject: [PATCH 099/121] added more tests --- package.json | 4 +- src/api/backend.ts | 55 ++- src/extension.ts | 2 +- src/listeners/workspaceModifiedListener.ts | 6 +- test/api/backend.test.ts | 339 +++++++++++------- test/commands/filterSmells.test.ts | 150 ++++++++ .../workspaceModifiedListener.test.ts | 269 ++++++++++++++ test/mocks/env-config-mock.ts | 2 +- test/mocks/vscode-mock.ts | 31 +- test/ui/fileHighlighter.test.ts | 1 + 10 files changed, 711 insertions(+), 148 deletions(-) create mode 100644 test/commands/filterSmells.test.ts create mode 100644 test/listeners/workspaceModifiedListener.test.ts diff --git a/package.json b/package.json index 3f5696c..3a29d98 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,9 @@ "!src/extension.ts", "!src/context/*", "!src/providers/*", - "!src/commands/showLogs.ts" + "!src/commands/showLogs.ts", + "!src/emitters/serverStatus.ts", + "!src/utils/envConfig.ts" ] }, "lint-staged": { diff --git a/src/api/backend.ts b/src/api/backend.ts index 5078a4a..00a8e23 100644 --- a/src/api/backend.ts +++ b/src/api/backend.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import {basename} from 'path'; import { envConfig } from '../utils/envConfig'; import { serverStatus } from '../emitters/serverStatus'; import { ServerStatusType } from '../emitters/serverStatus'; @@ -16,7 +16,7 @@ const BASE_URL = `http://${envConfig.SERVER_URL}`; */ export async function checkServerStatus(): Promise { try { - ecoOutput.trace('[backend.ts] Checking backend server health status...'); + ecoOutput.info('[backend.ts] Checking backend server health status...'); const response = await fetch(`${BASE_URL}/health`); if (response.ok) { @@ -24,7 +24,7 @@ export async function checkServerStatus(): Promise { ecoOutput.trace('[backend.ts] Backend server is healthy'); } else { serverStatus.setStatus(ServerStatusType.DOWN); - ecoOutput.trace(`[backend.ts] Backend server unhealthy status: ${response.status}`); + ecoOutput.warn(`[backend.ts] Backend server unhealthy status: ${response.status}`); } } catch (error) { serverStatus.setStatus(ServerStatusType.DOWN); @@ -62,6 +62,9 @@ export async function initLogs(log_dir: string): Promise { if (!response.ok) { console.error(`Unable to initialize logging: ${JSON.stringify(response)}`); + ecoOutput.error( + `Unable to initialize logging: ${JSON.stringify(response)}`, + ); return false; } @@ -69,7 +72,7 @@ export async function initLogs(log_dir: string): Promise { return true; } catch (error: any) { console.error(`Eco: Unable to initialize logging: ${error.message}`); - ecoOutput.warn( + ecoOutput.error( 'Eco: Unable to reach the backend. Please check your connection.', ); return false; @@ -91,9 +94,15 @@ export async function fetchSmells( enabledSmells: Record>, ): Promise<{ smells: Smell[]; status: number }> { const url = `${BASE_URL}/smells`; - ecoOutput.info(`[backend.ts] Starting smell detection for: ${path.basename(filePath)}`); + const fileName = basename(filePath); + ecoOutput.info(`[backend.ts] Starting smell detection for: ${fileName}`); try { + ecoOutput.debug(`[backend.ts] Request payload for ${fileName}:`, { + file_path: filePath, + enabled_smells: enabledSmells + }); + const response = await fetch(url, { method: 'POST', headers: { @@ -108,15 +117,47 @@ export async function fetchSmells( if (!response.ok) { const errorMsg = `Backend request failed (${response.status})`; ecoOutput.error(`[backend.ts] ${errorMsg}`); + try { + const errorBody = await response.json(); + ecoOutput.error(`[backend.ts] Backend error details:`, errorBody); + } catch (e: any) { + ecoOutput.error(`[backend.ts] Could not parse error response`); + } throw new Error(errorMsg); } const smellsList = await response.json(); - ecoOutput.info(`[backend.ts] Detected ${smellsList.length} smells in ${path.basename(filePath)}`); + + // Detailed logging of the response + ecoOutput.info(`[backend.ts] Detection complete for ${fileName}`); + ecoOutput.debug(`[backend.ts] Raw response headers for ${fileName}:`, Object.fromEntries(response.headers.entries())); + ecoOutput.debug(`[backend.ts] Full response for ${fileName}:`, { + status: response.status, + statusText: response.statusText, + body: smellsList + }); + + // Detailed smell listing + ecoOutput.info(`[backend.ts] Detected ${smellsList.length} smells in ${fileName}`); + if (smellsList.length > 0) { + ecoOutput.debug(`[backend.ts] Complete smells list for ${fileName}:`, smellsList); + ecoOutput.debug(`[backend.ts] Verbose smell details for ${fileName}:`, + smellsList.map((smell: Smell) => ({ + type: smell.symbol, + location: `${smell.path}:${smell.occurences}`, + message: smell.message, + context: smell.messageId + })) + ); + } + return { smells: smellsList, status: response.status }; } catch (error: any) { - ecoOutput.error(`[backend.ts] Smell detection failed: ${error.message}`); + ecoOutput.error(`[backend.ts] Smell detection failed for ${fileName}: ${error.message}`); + if (error instanceof Error && error.stack) { + ecoOutput.trace(`[backend.ts] Error stack info:`, error.stack); + } throw new Error(`Detection failed: ${error.message}`); } } diff --git a/src/extension.ts b/src/extension.ts index ad896ad..aaf407d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -72,7 +72,7 @@ export function activate(context: vscode.ExtensionContext): void { // === Start periodic backend status checks === checkServerStatus(); - setInterval(checkServerStatus, 50000); + setInterval(checkServerStatus, 10000); // === Initialize Refactor Action Buttons === initializeRefactorActionButtons(context); diff --git a/src/listeners/workspaceModifiedListener.ts b/src/listeners/workspaceModifiedListener.ts index d267455..14eb386 100644 --- a/src/listeners/workspaceModifiedListener.ts +++ b/src/listeners/workspaceModifiedListener.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import path from 'path'; +import { basename } from 'path'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from '../providers/SmellsViewProvider'; @@ -127,7 +127,7 @@ export class WorkspaceModifiedListener { `[WorkspaceListener] Invalidated cache for modified file: ${filePath}`, ); vscode.window.showInformationMessage( - `Analysis data marked outdated for ${path.basename(filePath)}`, + `Analysis data marked outdated for ${basename(filePath)}`, { modal: false }, ); @@ -171,7 +171,7 @@ export class WorkspaceModifiedListener { if (removed) { vscode.window.showInformationMessage( - `Removed analysis data for deleted file: ${path.basename(filePath)}`, + `Removed analysis data for deleted file: ${basename(filePath)}`, { modal: false }, ); } diff --git a/test/api/backend.test.ts b/test/api/backend.test.ts index 2b7428f..1dde1d0 100644 --- a/test/api/backend.test.ts +++ b/test/api/backend.test.ts @@ -1,207 +1,278 @@ +/* eslint-disable unused-imports/no-unused-imports */ +import path from 'path'; + +import { envConfig } from '../../src/utils/envConfig'; import { checkServerStatus, initLogs, fetchSmells, - refactorSmell, + backendRefactorSmell, + backendRefactorSmellType, } from '../../src/api/backend'; -import { serverStatus } from '../../src/utils/serverStatus'; -import { ServerStatusType } from '../../src/utils/serverStatus'; -import * as vscode from '../mocks/vscode-mock'; +import { serverStatus, ServerStatusType } from '../../src/emitters/serverStatus'; +import { ecoOutput } from '../../src/extension'; + +// Mock dependencies +jest.mock('../../src/emitters/serverStatus'); +jest.mock('../../src/extension'); +jest.mock('../../src/utils/envConfig'); +jest.mock('path', () => ({ + basename: jest.fn((path) => path.split('/').pop()), +})); + +// Mock global fetch +global.fetch = jest.fn() as jest.Mock; + +describe('Backend Service', () => { + const mockServerUrl = 'localhost:8000'; + const mockLogDir = '/path/to/logs'; + const mockFilePath = '/project/src/file.py'; + const mockWorkspacePath = '/project'; + const mockSmell = { + symbol: 'test-smell', + path: mockFilePath, + occurences: [{ line: 1 }], + message: 'Test smell message', + messageId: 'test-001', + } as unknown as Smell; -describe('backend', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('checkServerStatus', () => { - test('checkServerStatus should update serverStatus to UP on success', async () => { - global.fetch = jest.fn(() => Promise.resolve({ ok: true })) as jest.Mock; - - const setStatusSpy = jest.spyOn(serverStatus, 'setStatus'); + it('should set status UP when server is healthy', async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ ok: true }); await checkServerStatus(); - expect(setStatusSpy).toHaveBeenCalledWith(ServerStatusType.UP); + expect(fetch).toHaveBeenCalledWith(`http://${mockServerUrl}/health`); + expect(serverStatus.setStatus).toHaveBeenCalledWith(ServerStatusType.UP); + expect(ecoOutput.trace).toHaveBeenCalledWith( + '[backend.ts] Backend server is healthy', + ); }); - test('checkServerStatus should update serverStatus to DOWN on non-success', async () => { - global.fetch = jest.fn(() => Promise.resolve({ ok: false })) as jest.Mock; - - const setStatusSpy = jest.spyOn(serverStatus, 'setStatus'); + it('should set status DOWN when server responds with error', async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 500 }); await checkServerStatus(); - expect(setStatusSpy).toHaveBeenCalledWith(ServerStatusType.DOWN); + expect(serverStatus.setStatus).toHaveBeenCalledWith(ServerStatusType.DOWN); + expect(ecoOutput.warn).toHaveBeenCalledWith( + '[backend.ts] Backend server unhealthy status: 500', + ); }); - test('checkServerStatus should update serverStatus to DOWN on error', async () => { - global.fetch = jest.fn(() => - Promise.reject("Can't connect to server"), - ) as jest.Mock; - - const setStatusSpy = jest.spyOn(serverStatus, 'setStatus'); + it('should set status DOWN and log error when request fails', async () => { + const mockError = new Error('Network error'); + (fetch as jest.Mock).mockRejectedValueOnce(mockError); await checkServerStatus(); - expect(setStatusSpy).toHaveBeenCalledWith(ServerStatusType.DOWN); + expect(serverStatus.setStatus).toHaveBeenCalledWith(ServerStatusType.DOWN); + expect(ecoOutput.error).toHaveBeenCalledWith( + '[backend.ts] Server connection failed: Network error', + ); }); }); describe('initLogs', () => { - test('initLogs should return true on success', async () => { - global.fetch = jest.fn(() => Promise.resolve({ ok: true })) as jest.Mock; - const result = await initLogs('/path/to/logs'); + it('should successfully initialize logs', async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ ok: true }); + + const result = await initLogs(mockLogDir); + + expect(fetch).toHaveBeenCalledWith(`http://${mockServerUrl}/logs/init`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ log_dir: mockLogDir }), + }); expect(result).toBe(true); }); - test('initLogs should return false on non success', async () => { - global.fetch = jest.fn(() => Promise.resolve({ ok: false })) as jest.Mock; - const result = await initLogs('/path/to/logs'); + it('should return false when server responds with not ok', async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ ok: false }); + + const result = await initLogs(mockLogDir); + expect(result).toBe(false); + expect(ecoOutput.error).toHaveBeenCalledWith( + expect.stringContaining('Unable to initialize logging'), + ); }); - test('initLogs should return false on error', async () => { - global.fetch = jest.fn(() => { - throw new Error('Some error'); - }) as jest.Mock; - const result = await initLogs('/path/to/logs'); + it('should handle network errors', async () => { + (fetch as jest.Mock).mockRejectedValueOnce(new Error('Network failed')); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: Unable to reach the backend. Please check your connection.', - ); + const result = await initLogs(mockLogDir); expect(result).toBe(false); + expect(ecoOutput.error).toHaveBeenCalledWith( + 'Eco: Unable to reach the backend. Please check your connection.', + ); }); }); describe('fetchSmells', () => { - test('fetchSmells should return smells array on success', async () => { - const mockSmells = [{ symbol: 'LongMethod', severity: 'HIGH' }]; - global.fetch = jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve(mockSmells) }), - ) as jest.Mock; - - const result = await fetchSmells('file.py', ['LongMethod']); - expect(result).toEqual(mockSmells); - }); - - test('fetchSmells should return an empty array on status not ok', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ ok: false, status: 400, json: () => Promise.resolve([]) }), - ) as jest.Mock; - - const result = await fetchSmells('file.py', ['LongMethod']); - - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - `Eco: Failed to fetch smells`, + const mockEnabledSmells = { 'test-smell': { threshold: 0.5 } }; + const mockSmellsResponse = [mockSmell]; + + it('should successfully fetch smells', async () => { + const mockResponse = { + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + json: jest.fn().mockResolvedValueOnce(mockSmellsResponse), + }; + (fetch as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await fetchSmells(mockFilePath, mockEnabledSmells); + + expect(fetch).toHaveBeenCalledWith(`http://${mockServerUrl}/smells`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + file_path: mockFilePath, + enabled_smells: mockEnabledSmells, + }), + }); + expect(result).toEqual({ smells: mockSmellsResponse, status: 200 }); + expect(ecoOutput.info).toHaveBeenCalledWith( + '[backend.ts] Starting smell detection for: file.py', + ); + expect(ecoOutput.info).toHaveBeenCalledWith( + '[backend.ts] Detection complete for file.py', ); - expect(result).toEqual([]); }); - test('fetchSmells should return an empty array on invalid response format', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(200) }), - ) as jest.Mock; + it('should throw error when server responds with error', async () => { + const mockResponse = { + ok: false, + status: 500, + json: jest.fn().mockResolvedValueOnce({ detail: 'Server error' }), + }; + (fetch as jest.Mock).mockResolvedValueOnce(mockResponse); - const result = await fetchSmells('file.py', ['LongMethod']); + await expect(fetchSmells(mockFilePath, mockEnabledSmells)).rejects.toThrow( + 'Backend request failed (500)', + ); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - `Eco: Failed to fetch smells`, + expect(ecoOutput.error).toHaveBeenCalledWith( + '[backend.ts] Backend error details:', + { detail: 'Server error' }, ); - expect(result).toEqual([]); }); - test('fetchSmells should return an empty array on error', async () => { - global.fetch = jest.fn(() => { - throw new Error('Some error'); - }) as jest.Mock; + it('should throw error when network fails', async () => { + (fetch as jest.Mock).mockRejectedValueOnce(new Error('Network failed')); - const result = await fetchSmells('file.py', ['LongMethod']); + await expect(fetchSmells(mockFilePath, mockEnabledSmells)).rejects.toThrow( + 'Detection failed: Network failed', + ); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - `Eco: Failed to fetch smells`, + expect(ecoOutput.error).toHaveBeenCalledWith( + '[backend.ts] Smell detection failed for file.py: Network failed', ); - expect(result).toEqual([]); }); }); - describe('refactorSmell', () => { - test('refactorSmell should return refactor result on success', async () => { - const mockRefactorOutput = { success: true }; - - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockRefactorOutput), + describe('backendRefactorSmell', () => { + it('should successfully refactor smell', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValueOnce({ success: true }), + }; + (fetch as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await backendRefactorSmell(mockSmell, mockWorkspacePath); + + expect(fetch).toHaveBeenCalledWith(`http://${mockServerUrl}/refactor`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sourceDir: mockWorkspacePath, + smell: mockSmell, }), - ) as jest.Mock; - - (vscode.workspace as any).workspaceFolders = [ - { uri: { fsPath: '/mock/workspace' } }, - ]; - - const result = await refactorSmell('/mock/workspace/file.py', { - symbol: 'LongMethod', - } as Smell); - expect(result).toEqual(mockRefactorOutput); + }); + expect(result).toEqual({ success: true }); + expect(ecoOutput.info).toHaveBeenCalledWith( + '[backend.ts] Starting refactoring for smell: test-smell', + ); }); - test('refactorSmell should throw and error if no workspace found', async () => { - const mockRefactorOutput = { success: true }; + it('should throw error when no workspace path', async () => { + await expect(backendRefactorSmell(mockSmell, '')).rejects.toThrow( + 'No workspace path provided', + ); - global.fetch = jest.fn(() => - Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockRefactorOutput), - }), - ) as jest.Mock; + expect(ecoOutput.error).toHaveBeenCalledWith( + '[backend.ts] Refactoring aborted: No workspace path', + ); + }); - (vscode.workspace as any).workspaceFolders = [ - { uri: { fsPath: '/mock/workspace' } }, - ]; + it('should throw error when server responds with error', async () => { + const mockResponse = { + ok: false, + json: jest.fn().mockResolvedValueOnce({ detail: 'Refactor failed' }), + }; + (fetch as jest.Mock).mockResolvedValueOnce(mockResponse); await expect( - refactorSmell('/mock/another-workspace/file.py', { - symbol: 'LongMethod', - } as Smell), - ).rejects.toThrow( - 'Eco: Unable to find a matching workspace folder for file: /mock/another-workspace/file.py', + backendRefactorSmell(mockSmell, mockWorkspacePath), + ).rejects.toThrow('Refactoring failed'); + + expect(ecoOutput.error).toHaveBeenCalledWith( + '[backend.ts] Refactoring failed: Refactor failed', ); }); + }); - test('refactorSmell should throw and error if not ok response', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: false, - text: jest.fn().mockReturnValue('Some error text'), - }), - ) as jest.Mock; - - (vscode.workspace as any).workspaceFolders = [ - { uri: { fsPath: '/mock/workspace' } }, - ]; - - await expect( - refactorSmell('/mock/workspace/file.py', { - symbol: 'LongMethod', - } as Smell), - ).rejects.toThrow('Some error text'); + describe('backendRefactorSmellType', () => { + it('should successfully refactor smell type', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValueOnce({ success: true }), + }; + (fetch as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await backendRefactorSmellType(mockSmell, mockWorkspacePath); + + expect(fetch).toHaveBeenCalledWith( + `http://${mockServerUrl}/refactor-by-type`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sourceDir: mockWorkspacePath, + smellType: 'test-smell', + firstSmell: mockSmell, + }), + }, + ); + expect(result).toEqual({ success: true }); + expect(ecoOutput.info).toHaveBeenCalledWith( + '[backend.ts] Starting refactoring for smells of type "test-smell" in "/project/src/file.py"', + ); }); - test('refactorSmell should throw and error if function returns an error', async () => { - global.fetch = jest.fn(() => { - throw new Error('Some error'); - }) as jest.Mock; + it('should throw error when no workspace path', async () => { + await expect(backendRefactorSmellType(mockSmell, '')).rejects.toThrow( + 'No workspace path provided', + ); + }); - (vscode.workspace as any).workspaceFolders = [ - { uri: { fsPath: '/mock/workspace' } }, - ]; + it('should throw error when server responds with error', async () => { + const mockResponse = { + ok: false, + json: jest.fn().mockResolvedValueOnce({ detail: 'Type refactor failed' }), + }; + (fetch as jest.Mock).mockResolvedValueOnce(mockResponse); await expect( - refactorSmell('/mock/workspace/file.py', { - symbol: 'LongMethod', - } as Smell), - ).rejects.toThrow('Some error'); + backendRefactorSmellType(mockSmell, mockWorkspacePath), + ).rejects.toThrow('Type refactor failed'); }); }); }); diff --git a/test/commands/filterSmells.test.ts b/test/commands/filterSmells.test.ts new file mode 100644 index 0000000..5939e58 --- /dev/null +++ b/test/commands/filterSmells.test.ts @@ -0,0 +1,150 @@ +// test/commands/registerFilterSmellCommands.test.ts +import * as vscode from 'vscode'; +import { registerFilterSmellCommands } from '../../src/commands/views/filterSmells'; +import { FilterViewProvider } from '../../src/providers/FilterViewProvider'; + +// Mock the FilterViewProvider +jest.mock('../../src/providers/FilterViewProvider'); + +describe('registerFilterSmellCommands', () => { + let mockContext: vscode.ExtensionContext; + let mockFilterProvider: jest.Mocked; + let mockCommands: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + + // Setup mock context + mockContext = { + subscriptions: [], + } as unknown as vscode.ExtensionContext; + + // Setup mock filter provider + mockFilterProvider = { + toggleSmell: jest.fn(), + updateOption: jest.fn(), + refresh: jest.fn(), + setAllSmellsEnabled: jest.fn(), + resetToDefaults: jest.fn(), + } as unknown as jest.Mocked; + + // Mock commands + mockCommands = vscode.commands as jest.Mocked; + }); + + it('should register toggleSmellFilter command', () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + // Verify command registration + expect(mockCommands.registerCommand).toHaveBeenCalledWith( + 'ecooptimizer.toggleSmellFilter', + expect.any(Function), + ); + + // Test the command handler + const [, handler] = (mockCommands.registerCommand as jest.Mock).mock.calls[0]; + handler('test-smell'); + expect(mockFilterProvider.toggleSmell).toHaveBeenCalledWith('test-smell'); + }); + + it('should register editSmellFilterOption command with valid input', async () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + // Mock showInputBox to return valid number + (vscode.window.showInputBox as jest.Mock).mockResolvedValue('42'); + + // Get the command handler + const editCommandCall = ( + mockCommands.registerCommand as jest.Mock + ).mock.calls.find((call) => call[0] === 'ecooptimizer.editSmellFilterOption'); + const [, handler] = editCommandCall; + + // Test with valid item + await handler({ smellKey: 'test-smell', optionKey: 'threshold', value: 10 }); + + expect(vscode.window.showInputBox).toHaveBeenCalledWith({ + prompt: 'Enter a new value for threshold', + value: '10', + validateInput: expect.any(Function), + }); + expect(mockFilterProvider.updateOption).toHaveBeenCalledWith( + 'test-smell', + 'threshold', + 42, + ); + expect(mockFilterProvider.refresh).toHaveBeenCalled(); + }); + + it('should handle editSmellFilterOption with invalid input', async () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + // Mock showInputBox to return invalid input + (vscode.window.showInputBox as jest.Mock).mockResolvedValue('not-a-number'); + + const editCommandCall = ( + mockCommands.registerCommand as jest.Mock + ).mock.calls.find((call) => call[0] === 'ecooptimizer.editSmellFilterOption'); + const [, handler] = editCommandCall; + + await handler({ smellKey: 'test-smell', optionKey: 'threshold', value: 10 }); + + expect(mockFilterProvider.updateOption).not.toHaveBeenCalled(); + }); + + it('should show error for editSmellFilterOption with missing keys', async () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + const editCommandCall = ( + mockCommands.registerCommand as jest.Mock + ).mock.calls.find((call) => call[0] === 'ecooptimizer.editSmellFilterOption'); + const [, handler] = editCommandCall; + + await handler({}); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + 'Error: Missing smell or option key.', + ); + }); + + it('should register selectAllFilterSmells command', () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + const selectAllCall = ( + mockCommands.registerCommand as jest.Mock + ).mock.calls.find((call) => call[0] === 'ecooptimizer.selectAllFilterSmells'); + const [, handler] = selectAllCall; + + handler(); + expect(mockFilterProvider.setAllSmellsEnabled).toHaveBeenCalledWith(true); + }); + + it('should register deselectAllFilterSmells command', () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + const deselectAllCall = ( + mockCommands.registerCommand as jest.Mock + ).mock.calls.find((call) => call[0] === 'ecooptimizer.deselectAllFilterSmells'); + const [, handler] = deselectAllCall; + + handler(); + expect(mockFilterProvider.setAllSmellsEnabled).toHaveBeenCalledWith(false); + }); + + it('should register setFilterDefaults command', () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + const setDefaultsCall = ( + mockCommands.registerCommand as jest.Mock + ).mock.calls.find((call) => call[0] === 'ecooptimizer.setFilterDefaults'); + const [, handler] = setDefaultsCall; + + handler(); + expect(mockFilterProvider.resetToDefaults).toHaveBeenCalled(); + }); + + it('should add all commands to context subscriptions', () => { + registerFilterSmellCommands(mockContext, mockFilterProvider); + + // Verify all commands were added to subscriptions + expect(mockContext.subscriptions).toHaveLength(5); + }); +}); diff --git a/test/listeners/workspaceModifiedListener.test.ts b/test/listeners/workspaceModifiedListener.test.ts new file mode 100644 index 0000000..8d90dca --- /dev/null +++ b/test/listeners/workspaceModifiedListener.test.ts @@ -0,0 +1,269 @@ +/* eslint-disable unused-imports/no-unused-imports */ +import * as vscode from 'vscode'; +import path from 'path'; + +import { envConfig } from '../../src/utils/envConfig'; +import { WorkspaceModifiedListener } from '../../src/listeners/workspaceModifiedListener'; +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; +import { SmellsViewProvider } from '../../src/providers/SmellsViewProvider'; +import { MetricsViewProvider } from '../../src/providers/MetricsViewProvider'; +import { ecoOutput } from '../../src/extension'; +import { detectSmellsFile } from '../../src/commands/detection/detectSmells'; + +// Mock dependencies +jest.mock('path', () => ({ + basename: jest.fn((path) => path), +})); +jest.mock('../../src/extension'); +jest.mock('../../src/commands/detection/detectSmells'); +jest.mock('../../src/utils/envConfig'); + +describe('WorkspaceModifiedListener', () => { + let mockContext: vscode.ExtensionContext; + let mockSmellsCacheManager: jest.Mocked; + let mockSmellsViewProvider: jest.Mocked; + let mockMetricsViewProvider: jest.Mocked; + let listener: WorkspaceModifiedListener; + + beforeEach(() => { + jest.clearAllMocks(); + + mockContext = { + workspaceState: { + get: jest.fn(), + }, + } as unknown as vscode.ExtensionContext; + + mockSmellsCacheManager = { + hasFileInCache: jest.fn(), + hasCachedSmells: jest.fn(), + clearCachedSmellsForFile: jest.fn(), + clearCachedSmellsByPath: jest.fn(), + getAllFilePaths: jest.fn(() => []), + } as unknown as jest.Mocked; + + mockSmellsViewProvider = { + setStatus: jest.fn(), + removeFile: jest.fn(), + refresh: jest.fn(), + } as unknown as jest.Mocked; + + mockMetricsViewProvider = { + refresh: jest.fn(), + } as unknown as jest.Mocked; + }); + + describe('Initialization', () => { + it('should initialize without workspace path', () => { + (mockContext.workspaceState.get as jest.Mock).mockReturnValue(undefined); + new WorkspaceModifiedListener( + mockContext, + mockSmellsCacheManager, + mockSmellsViewProvider, + mockMetricsViewProvider, + ); + expect(ecoOutput.trace).toHaveBeenCalledWith( + '[WorkspaceListener] No workspace configured - skipping file watcher', + ); + }); + + it('should initialize with workspace path', () => { + (mockContext.workspaceState.get as jest.Mock).mockReturnValue('/project/path'); + listener = new WorkspaceModifiedListener( + mockContext, + mockSmellsCacheManager, + mockSmellsViewProvider, + mockMetricsViewProvider, + ); + + console.log((ecoOutput.trace as jest.Mock).mock); + expect(vscode.workspace.createFileSystemWatcher).toHaveBeenCalled(); + expect(ecoOutput.trace).toHaveBeenCalledWith( + '[WorkspaceListener] Watching Python files in /project/path', + ); + }); + }); + + describe('File Change Handling', () => { + beforeEach(() => { + (mockContext.workspaceState.get as jest.Mock).mockReturnValue('/project/path'); + listener = new WorkspaceModifiedListener( + mockContext, + mockSmellsCacheManager, + mockSmellsViewProvider, + mockMetricsViewProvider, + ); + }); + + it('should handle file change with existing cache', async () => { + const filePath = '/project/path/file.py'; + (mockSmellsCacheManager.hasFileInCache as jest.Mock).mockReturnValue(true); + + await listener['handleFileChange'](filePath); + + expect(mockSmellsCacheManager.clearCachedSmellsForFile).toHaveBeenCalledWith( + filePath, + ); + expect(mockSmellsViewProvider.setStatus).toHaveBeenCalledWith( + filePath, + 'outdated', + ); + expect(vscode.window.showInformationMessage).toHaveBeenCalled(); + expect(mockSmellsViewProvider.refresh).toHaveBeenCalled(); + }); + + it('should skip file change without cache', async () => { + const filePath = '/project/path/file.py'; + (mockSmellsCacheManager.hasFileInCache as jest.Mock).mockReturnValue(false); + + await listener['handleFileChange'](filePath); + + expect(mockSmellsCacheManager.clearCachedSmellsForFile).not.toHaveBeenCalled(); + expect(ecoOutput.trace).toHaveBeenCalledWith( + '[WorkspaceListener] No cache to invalidate for /project/path/file.py', + ); + }); + + it('should handle file change errors', async () => { + const filePath = '/project/path/file.py'; + (mockSmellsCacheManager.hasFileInCache as jest.Mock).mockReturnValue(true); + ( + mockSmellsCacheManager.clearCachedSmellsForFile as jest.Mock + ).mockRejectedValue(new Error('Cache error')); + + await listener['handleFileChange'](filePath); + + expect(ecoOutput.error).toHaveBeenCalledWith( + expect.stringContaining('Error handling file change: Cache error'), + ); + }); + }); + + describe('File Deletion Handling', () => { + beforeEach(() => { + (mockContext.workspaceState.get as jest.Mock).mockReturnValue('/project/path'); + listener = new WorkspaceModifiedListener( + mockContext, + mockSmellsCacheManager, + mockSmellsViewProvider, + mockMetricsViewProvider, + ); + }); + + it('should handle file deletion with cache', async () => { + const filePath = '/project/path/file.py'; + (mockSmellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(true); + (mockSmellsViewProvider.removeFile as jest.Mock).mockReturnValue(true); + + await listener['handleFileDeletion'](filePath); + + expect(mockSmellsCacheManager.clearCachedSmellsByPath).toHaveBeenCalledWith( + filePath, + ); + expect(mockSmellsViewProvider.removeFile).toHaveBeenCalledWith(filePath); + // expect(vscode.window.showInformationMessage).toHaveBeenCalled(); + expect(mockSmellsViewProvider.refresh).toHaveBeenCalled(); + }); + + it('should handle file deletion without cache', async () => { + const filePath = '/project/path/file.py'; + (mockSmellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(false); + (mockSmellsViewProvider.removeFile as jest.Mock).mockReturnValue(false); + + await listener['handleFileDeletion'](filePath); + + expect(mockSmellsCacheManager.clearCachedSmellsByPath).not.toHaveBeenCalled(); + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); + }); + + it('should handle deletion errors', async () => { + const filePath = '/project/path/file.py'; + (mockSmellsCacheManager.hasCachedSmells as jest.Mock).mockReturnValue(true); + ( + mockSmellsCacheManager.clearCachedSmellsByPath as jest.Mock + ).mockRejectedValue(new Error('Deletion error')); + + await listener['handleFileDeletion'](filePath); + + expect(ecoOutput.error).toHaveBeenCalledWith( + expect.stringContaining('Error clearing cache: Deletion error'), + ); + }); + }); + + describe('Save Listener', () => { + it('should trigger smell detection on Python file save when enabled', () => { + (mockContext.workspaceState.get as jest.Mock).mockReturnValue('/project/path'); + ( + require('../../src/extension').isSmellLintingEnabled as jest.Mock + ).mockReturnValue(true); + + listener = new WorkspaceModifiedListener( + mockContext, + mockSmellsCacheManager, + mockSmellsViewProvider, + mockMetricsViewProvider, + ); + + // Trigger save event + const onDidSave = (vscode.workspace.onDidSaveTextDocument as jest.Mock).mock + .calls[0][0]; + const mockDocument = { + languageId: 'python', + uri: { fsPath: '/project/path/file.py' }, + }; + onDidSave(mockDocument); + + expect(detectSmellsFile).toHaveBeenCalledWith( + '/project/path/file.py', + mockSmellsViewProvider, + mockSmellsCacheManager, + ); + expect(ecoOutput.info).toHaveBeenCalledWith( + '[WorkspaceListener] Smell linting is ON — auto-detecting smells for /project/path/file.py', + ); + }); + + it('should skip non-Python files on save', () => { + (mockContext.workspaceState.get as jest.Mock).mockReturnValue('/project/path'); + + listener = new WorkspaceModifiedListener( + mockContext, + mockSmellsCacheManager, + mockSmellsViewProvider, + mockMetricsViewProvider, + ); + + // Trigger save event + const onDidSave = (vscode.workspace.onDidSaveTextDocument as jest.Mock).mock + .calls[0][0]; + const mockDocument = { + languageId: 'javascript', + uri: { fsPath: '/project/path/file.js' }, + }; + onDidSave(mockDocument); + + expect(detectSmellsFile).not.toHaveBeenCalled(); + }); + }); + + describe('Disposal', () => { + it('should clean up resources on dispose', () => { + (mockContext.workspaceState.get as jest.Mock).mockReturnValue('/project/path'); + listener = new WorkspaceModifiedListener( + mockContext, + mockSmellsCacheManager, + mockSmellsViewProvider, + mockMetricsViewProvider, + ); + + listener.dispose(); + + expect(listener['fileWatcher']?.dispose).toHaveBeenCalled(); + expect(listener['saveListener']?.dispose).toHaveBeenCalled(); + expect(ecoOutput.trace).toHaveBeenCalledWith( + '[WorkspaceListener] Disposed all listeners', + ); + }); + }); +}); diff --git a/test/mocks/env-config-mock.ts b/test/mocks/env-config-mock.ts index 45b980a..e98c4df 100644 --- a/test/mocks/env-config-mock.ts +++ b/test/mocks/env-config-mock.ts @@ -1,7 +1,7 @@ import { EnvConfig } from '../../src/utils/envConfig'; const mockEnvConfig: EnvConfig = { - SERVER_URL: 'value1', + SERVER_URL: 'localhost:8000', SMELL_CACHE_KEY: 'value2', HASH_PATH_MAP_KEY: 'value3', WORKSPACE_METRICS_DATA: 'value4', diff --git a/test/mocks/vscode-mock.ts b/test/mocks/vscode-mock.ts index 52f9771..7e97708 100644 --- a/test/mocks/vscode-mock.ts +++ b/test/mocks/vscode-mock.ts @@ -59,6 +59,7 @@ interface Window { activeTextEditor: any; visibleTextEditors: any[]; withProgress: jest.Mock; + showInputBox: jest.Mock; showQuickPick: jest.Mock; } @@ -93,6 +94,7 @@ export const window: Window = { report: jest.fn(), }); }), + showInputBox: jest.fn((val) => {}), showQuickPick: jest.fn(), }; @@ -103,6 +105,8 @@ export enum FileType { interface Workspace { getConfiguration: jest.Mock; + createFileSystemWatcher: jest.Mock; + onDidSaveTextDocument: jest.Mock; findFiles: jest.Mock; fs: { readFile: jest.Mock; @@ -116,6 +120,20 @@ export const workspace: Workspace = { get: jest.fn(() => config.configGet), update: jest.fn(), })), + createFileSystemWatcher: jest.fn( + () => + ({ + onDidCreate: jest.fn(), + onDidDelete: jest.fn(), + dispose: jest.fn(), + }) as unknown, + ), + onDidSaveTextDocument: jest.fn( + () => + ({ + dispose: jest.fn(), + }) as unknown, + ), findFiles: jest.fn(), fs: { readFile: jest.fn(), @@ -198,7 +216,9 @@ window.withProgress = jest.fn( ); export const commands = { - registerCommand: jest.fn(), + registerCommand: jest.fn((command: string, func: Function) => ({ + dispose: jest.fn(), + })), executeCommand: jest.fn((command: string) => { if (command === 'setContext') { return Promise.resolve(); @@ -239,6 +259,13 @@ interface MockMarkdownString { isTrusted: boolean; } +export class RelativePattern { + constructor( + public path: string, + pattern: string, + ) {} +} + export const MarkdownString = jest.fn().mockImplementation(() => { return { appendMarkdown: jest.fn(function (this: any, value: string) { @@ -310,6 +337,7 @@ export interface Vscode { OverviewRulerLane: typeof OverviewRulerLane; ProgressLocation: typeof ProgressLocation; FileType: typeof FileType; + RelativePattern: typeof RelativePattern; Range: typeof Range; Position: typeof Position; Hover: typeof Hover; @@ -331,6 +359,7 @@ const vscode: Vscode = { OverviewRulerLane, ProgressLocation, FileType, + RelativePattern, Range, Position, Hover, diff --git a/test/ui/fileHighlighter.test.ts b/test/ui/fileHighlighter.test.ts index 88e02ef..8fb7a67 100644 --- a/test/ui/fileHighlighter.test.ts +++ b/test/ui/fileHighlighter.test.ts @@ -246,6 +246,7 @@ describe('FileHighlighter', () => { key === 'highlightStyle' ? 'underline' : undefined, ); + fileHighlighter['getDecoration']('rgba(255,0,0,0.5)', 'underline'); expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledWith({ textDecoration: 'wavy rgba(255,0,0,0.5) underline 1px', }); From c50466f9181c9c0fa3246b1ff4c12baa9b043604 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Sat, 29 Mar 2025 14:48:18 -0400 Subject: [PATCH 100/121] Fixed ui tests and made compilation work --- test/commands/toggleSmellLinting.test.ts | 96 ------- test/ui/hoverManager.test.ts | 348 +++++++++++------------ test/ui/lineSelection.test.ts | 284 +++++++++++------- 3 files changed, 346 insertions(+), 382 deletions(-) delete mode 100644 test/commands/toggleSmellLinting.test.ts diff --git a/test/commands/toggleSmellLinting.test.ts b/test/commands/toggleSmellLinting.test.ts deleted file mode 100644 index 8aefb2a..0000000 --- a/test/commands/toggleSmellLinting.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import * as vscode from 'vscode'; -import { ContextManager } from '../../src/context/contextManager'; -import { toggleSmellLinting } from '../../src/commands/toggleSmellLinting'; -import { FileHighlighter } from '../../src/ui/fileHighlighter'; -import { detectSmells } from '../../src/commands/detectSmells'; -import { envConfig } from '../../src/utils/envConfig'; - -jest.mock('../../src/commands/detectSmells', () => ({ - detectSmells: jest.fn(), -})); - -jest.mock('../../src/ui/fileHighlighter', () => ({ - FileHighlighter: { - getInstance: jest.fn(), - }, -})); - -describe('toggleSmellLinting', () => { - let contextManagerMock: ContextManager; - let fileHighlighterMock: FileHighlighter; - - beforeEach(() => { - jest.clearAllMocks(); - - contextManagerMock = { - getWorkspaceData: jest.fn(), - setWorkspaceData: jest.fn(), - } as unknown as ContextManager; - - fileHighlighterMock = { - resetHighlights: jest.fn(), - } as unknown as FileHighlighter; - - (FileHighlighter.getInstance as jest.Mock).mockReturnValue(fileHighlighterMock); - }); - - it('should toggle from disabled to enabled state', async () => { - (contextManagerMock.getWorkspaceData as jest.Mock).mockReturnValue(false); - - await toggleSmellLinting(contextManagerMock); - - expect(detectSmells).toHaveBeenCalledWith(contextManagerMock); - - expect(contextManagerMock.setWorkspaceData).toHaveBeenCalledWith( - envConfig.SMELL_LINTING_ENABLED_KEY, - true, - ); - - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - 'eco.smellLintingEnabled', - true, - ); - }); - - it('should toggle from enabled to disabled state', async () => { - (contextManagerMock.getWorkspaceData as jest.Mock).mockReturnValue(true); - - await toggleSmellLinting(contextManagerMock); - - expect(fileHighlighterMock.resetHighlights).toHaveBeenCalled(); - - expect(contextManagerMock.setWorkspaceData).toHaveBeenCalledWith( - envConfig.SMELL_LINTING_ENABLED_KEY, - false, - ); - - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - 'eco.smellLintingEnabled', - false, - ); - - expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( - 'Eco: Smell linting turned off.', - ); - }); - - it('should handle errors and revert UI state', async () => { - (contextManagerMock.getWorkspaceData as jest.Mock).mockReturnValue(false); - - (detectSmells as jest.Mock).mockRejectedValue(new Error('Test error')); - - await toggleSmellLinting(contextManagerMock); - - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - 'Eco: Failed to toggle smell linting.', - ); - - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - 'eco.smellLintingEnabled', - false, - ); - }); -}); diff --git a/test/ui/hoverManager.test.ts b/test/ui/hoverManager.test.ts index 15af64e..57d22a9 100644 --- a/test/ui/hoverManager.test.ts +++ b/test/ui/hoverManager.test.ts @@ -1,202 +1,190 @@ -// test/hover-manager.test.ts -// import vscode from '../mocks/vscode-mock'; +import * as vscode from 'vscode'; import { HoverManager } from '../../src/ui/hoverManager'; -import { ContextManager } from '../../src/context/contextManager'; -import { Smell, Occurrence } from '../../src/types'; -import vscode from 'vscode'; - -jest.mock('vscode'); - -jest.mock('../../src/commands/refactorSmell', () => ({ - refactorSelectedSmell: jest.fn(), - refactorAllSmellsOfType: jest.fn(), -})); +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; + +// Create a simple mock Uri implementation +const mockUri = (path: string): vscode.Uri => ({ + scheme: 'file', + authority: '', + path, + fsPath: path, + query: '', + fragment: '', + with: jest.fn(), + toString: jest.fn(() => path), + toJSON: jest.fn(() => ({ path })), +}); -// Mock the vscode module using our custom mock -// jest.mock('vscode', () => vscode); +// Mock the vscode module with all required components +jest.mock('vscode', () => { + const actualVscode = jest.requireActual('vscode'); -describe('HoverManager', () => { - let contextManagerMock: ContextManager; - let mockSmells: Smell[]; - - const mockOccurrence: Occurrence = { - line: 5, - endLine: 7, - column: 1, - endColumn: 10, + // Mock MarkdownString implementation + const mockMarkdownString = { + isTrusted: true, + supportHtml: true, + supportThemeIcons: true, + appendMarkdown: jest.fn(), }; - beforeEach(() => { - jest.clearAllMocks(); - - contextManagerMock = { - context: { - subscriptions: [], - }, - getContext: () => ({ subscriptions: [] }), - } as unknown as ContextManager; + return { + ...actualVscode, + languages: { + registerHoverProvider: jest.fn(), + }, + MarkdownString: jest.fn(() => mockMarkdownString), + Hover: jest.fn(), + Position: jest.fn(), + Uri: { + file: jest.fn((path) => mockUri(path)), + parse: jest.fn((path) => mockUri(path)), + }, + }; +}); - mockSmells = [ +describe('HoverManager', () => { + let hoverManager: HoverManager; + let mockSmellsCacheManager: jest.Mocked; + let mockContext: vscode.ExtensionContext; + let mockDocument: vscode.TextDocument; + let mockPosition: vscode.Position; + + const createMockSmell = (messageId: string, line: number) => ({ + type: 'performance', + symbol: 'test-smell', + message: 'Test smell message', + messageId, + confidence: 'HIGH', + path: '/test/file.py', + module: 'test', + occurences: [ { - type: 'performance', - symbol: 'CRS-001', - message: 'Cached repeated calls', - messageId: 'cached-repeated-calls', - confidence: 'HIGH', - path: '/test/file.py', - module: 'test_module', - occurences: [mockOccurrence], - additionalInfo: {}, + line, + column: 1, + endLine: line, + endColumn: 10, }, - ]; - }); - - it('should register hover provider for Python files', () => { - new HoverManager(contextManagerMock, mockSmells); - - expect(vscode.languages.registerHoverProvider).toHaveBeenCalledWith( - { scheme: 'file', language: 'python' }, - expect.objectContaining({ - provideHover: expect.any(Function), - }), - ); - }); - - it('should subscribe hover provider correctly', () => { - const spy = jest.spyOn(contextManagerMock.context.subscriptions, 'push'); - new HoverManager(contextManagerMock, mockSmells); - expect(spy).toHaveBeenCalledWith(expect.anything()); + ], + additionalInfo: {}, }); - it('should return null for hover content if there are no smells', () => { - const manager = new HoverManager(contextManagerMock, []); - const document = { fileName: '/test/file.py', getText: jest.fn() } as any; - const position = { line: 4 } as any; - expect(manager.getHoverContent(document, position)).toBeNull(); - }); - - it('should update smells when getInstance is called again', () => { - const initialSmells = [ - { - type: 'performance', - symbol: 'CRS-001', - message: 'Cached repeated calls', - messageId: 'cached-repeated-calls', - confidence: 'HIGH', - path: '/test/file.py', - module: 'test_module', - occurences: [mockOccurrence], - additionalInfo: {}, - }, - ]; - - const newSmells = [ - { - type: 'memory', - symbol: 'MEM-002', - message: 'Memory leak detected', - messageId: 'memory-leak', - confidence: 'MEDIUM', - path: '/test/file2.py', - module: 'test_module_2', - occurences: [mockOccurrence], - additionalInfo: {}, - }, - ]; - - const manager1 = HoverManager.getInstance(contextManagerMock, initialSmells); - expect(manager1['smells']).toEqual(initialSmells); - - const manager2 = HoverManager.getInstance(contextManagerMock, newSmells); - expect(manager2['smells']).toEqual(newSmells); - expect(manager1).toBe(manager2); // Ensuring it's the same instance - }); + beforeEach(() => { + jest.clearAllMocks(); - it('should update smells correctly', () => { - const manager = new HoverManager(contextManagerMock, mockSmells); - const newSmells: Smell[] = [ - { - type: 'security', - symbol: 'SEC-003', - message: 'Unsafe API usage', - messageId: 'unsafe-api', - confidence: 'HIGH', - path: '/test/file3.py', - module: 'security_module', - occurences: [mockOccurrence], - additionalInfo: {}, - }, - ]; + mockSmellsCacheManager = { + getCachedSmells: jest.fn(), + } as unknown as jest.Mocked; - manager.updateSmells(newSmells); - expect(manager['smells']).toEqual(newSmells); - }); + mockContext = { + subscriptions: [], + } as unknown as vscode.ExtensionContext; - it('should generate valid hover content', () => { - const manager = new HoverManager(contextManagerMock, mockSmells); - const document = { + mockDocument = { + uri: vscode.Uri.file('/test/file.py'), fileName: '/test/file.py', - getText: jest.fn(), - } as any; + lineAt: jest.fn(), + } as unknown as vscode.TextDocument; - const position = { - line: 4, // 0-based line number (will become line 5 in 1-based) + mockPosition = { + line: 5, character: 0, - isBefore: jest.fn(), - isBeforeOrEqual: jest.fn(), - isAfter: jest.fn(), - isAfterOrEqual: jest.fn(), - translate: jest.fn(), - with: jest.fn(), - compareTo: jest.fn(), - isEqual: jest.fn(), - } as any; // Simplified type assertion since we don't need full Position type - - // Mock document text for line range - document.getText.mockReturnValue('mock code content'); - const content = manager.getHoverContent(document, position); - - expect(content?.value).toBeDefined(); // Check value exists - expect(content?.value).toContain('CRS-001'); - expect(content?.value).toContain('Cached repeated calls'); - expect(content?.isTrusted).toBe(true); - - // Verify basic structure for each smell - expect(content?.value).toContain('**CRS-001:** Cached repeated calls'); - expect(content?.value).toContain( - '[Refactor](command:extension.refactorThisSmell?', - ); - expect(content?.value).toContain( - '[Refactor all smells of this type...](command:extension.refactorAllSmellsOfType?', - ); - // Verify command parameters are properly encoded - const expectedSmellParam = encodeURIComponent(JSON.stringify(mockSmells[0])); - expect(content?.value).toContain( - `command:extension.refactorThisSmell?${expectedSmellParam}`, - ); - expect(content?.value).toContain( - `command:extension.refactorAllSmellsOfType?${expectedSmellParam}`, - ); - - // Verify formatting between elements - expect(content?.value).toContain('\t\t'); // Verify tab separation - expect(content?.value).toContain('\n\n'); // Verify line breaks between smells - - // // Verify empty case - // expect(manager.getHoverContent(document, invalidPosition)).toBeNull(); + } as unknown as vscode.Position; + + hoverManager = new HoverManager(mockSmellsCacheManager); }); - it('should register refactor commands', () => { - new HoverManager(contextManagerMock, mockSmells); + describe('register', () => { + it('should register hover provider for Python files', () => { + hoverManager.register(mockContext); - expect(vscode.commands.registerCommand).toHaveBeenCalledWith( - 'extension.refactorThisSmell', - expect.any(Function), - ); + expect(vscode.languages.registerHoverProvider).toHaveBeenCalledWith( + { language: 'python', scheme: 'file' }, + hoverManager, + ); + expect(mockContext.subscriptions).toHaveLength(1); + }); + }); - expect(vscode.commands.registerCommand).toHaveBeenCalledWith( - 'extension.refactorAllSmellsOfType', - expect.any(Function), - ); + describe('provideHover', () => { + it('should return undefined for non-Python files', () => { + const jsDocument = { + uri: vscode.Uri.file('/test/file.js'), + fileName: '/test/file.js', + } as vscode.TextDocument; + + const result = hoverManager.provideHover( + jsDocument, + mockPosition, + {} as vscode.CancellationToken, + ); + expect(result).toBeUndefined(); + }); + + it('should return undefined when no smells are cached', () => { + mockSmellsCacheManager.getCachedSmells.mockReturnValue(undefined); + const result = hoverManager.provideHover( + mockDocument, + mockPosition, + {} as vscode.CancellationToken, + ); + expect(result).toBeUndefined(); + }); + + it('should return undefined when no smells at line', () => { + mockSmellsCacheManager.getCachedSmells.mockReturnValue([ + createMockSmell('test-smell', 10), // Different line + ]); + const result = hoverManager.provideHover( + mockDocument, + mockPosition, + {} as vscode.CancellationToken, + ); + expect(result).toBeUndefined(); + }); + + it('should create hover for single smell at line', () => { + const mockSmell = createMockSmell('test-smell', 6); // line + 1 + mockSmellsCacheManager.getCachedSmells.mockReturnValue([mockSmell]); + + const result = hoverManager.provideHover( + mockDocument, + mockPosition, + {} as vscode.CancellationToken, + ); + + expect(vscode.MarkdownString).toHaveBeenCalled(); + expect(vscode.Hover).toHaveBeenCalled(); + + // Get the mock MarkdownString instance + const markdownInstance = (vscode.MarkdownString as jest.Mock).mock.results[0] + .value; + expect(markdownInstance.appendMarkdown).toHaveBeenCalledWith( + expect.stringContaining('Test smell message'), + ); + expect(markdownInstance.appendMarkdown).toHaveBeenCalledWith( + expect.stringContaining('command:ecooptimizer.refactorSmell'), + ); + }); + + it('should escape special characters in messages', () => { + const mockSmell = { + ...createMockSmell('test-smell', 6), + message: 'Message with *stars* and _underscores_', + messageId: 'id_with*stars*', + }; + mockSmellsCacheManager.getCachedSmells.mockReturnValue([mockSmell]); + + hoverManager.provideHover( + mockDocument, + mockPosition, + {} as vscode.CancellationToken, + ); + + const markdownInstance = (vscode.MarkdownString as jest.Mock).mock.results[0] + .value; + expect(markdownInstance.appendMarkdown).toHaveBeenCalledWith( + expect.stringContaining('Message with \\*stars\\* and \\_underscores\\_'), + ); + }); }); }); diff --git a/test/ui/lineSelection.test.ts b/test/ui/lineSelection.test.ts index cd851d2..37ba48f 100644 --- a/test/ui/lineSelection.test.ts +++ b/test/ui/lineSelection.test.ts @@ -1,149 +1,221 @@ -// test/line-selection-manager.test.ts +import * as vscode from 'vscode'; import { LineSelectionManager } from '../../src/ui/lineSelectionManager'; -import { ContextManager } from '../../src/context/contextManager'; -import vscode from 'vscode'; - -jest.mock('vscode'); - -jest.mock('../../src/utils/hashDocs', () => ({ - hashContent: jest.fn(() => 'mockHash'), -})); +import { SmellsCacheManager } from '../../src/context/SmellsCacheManager'; + +jest.mock('vscode', () => { + const actualVscode = jest.requireActual('vscode'); + return { + ...actualVscode, + window: { + ...actualVscode.window, + createTextEditorDecorationType: jest.fn(), + activeTextEditor: undefined, + }, + ThemeColor: jest.fn((colorName: string) => ({ id: colorName })), + }; +}); describe('LineSelectionManager', () => { - let contextManagerMock: ContextManager; + let manager: LineSelectionManager; + let mockSmellsCacheManager: jest.Mocked; let mockEditor: vscode.TextEditor; - let lineSelectionManager: LineSelectionManager; + let mockDocument: vscode.TextDocument; + let mockDecorationType: vscode.TextEditorDecorationType; + + // Helper function to create a mock smell + const createMockSmell = (symbol: string, line: number) => ({ + type: 'performance', + symbol, + message: 'Test smell', + messageId: 'test-smell', + confidence: 'HIGH', + path: '/test/file.js', + module: 'test', + occurences: [ + { + line, + column: 1, + endLine: line, + endColumn: 10, + }, + ], + additionalInfo: {}, + }); beforeEach(() => { jest.clearAllMocks(); - contextManagerMock = { - getWorkspaceData: jest.fn(() => ({ - '/test/file.py': { - hash: 'mockHash', - smells: [ - { symbol: 'PERF-001', occurences: [{ line: 5 }] }, - { symbol: 'SEC-002', occurences: [{ line: 5 }] }, - ], - }, - })), - } as unknown as ContextManager; + mockSmellsCacheManager = { + onSmellsUpdated: jest.fn(), + getCachedSmells: jest.fn(), + } as unknown as jest.Mocked; - mockEditor = { - document: { - fileName: '/test/file.py', - getText: jest.fn(() => 'mock content'), - lineAt: jest.fn(() => ({ text: 'mock line content' })), + mockDocument = { + fileName: '/test/file.js', + lineAt: jest.fn().mockReturnValue({ + text: 'const test = true;', + trimEnd: jest.fn().mockReturnValue('const test = true;'), + }), + uri: { + fsPath: '/test/file.js', }, + } as unknown as vscode.TextDocument; + + mockEditor = { + document: mockDocument, selection: { - start: { line: 4 }, // 0-based index, maps to line 5 isSingleLine: true, - } as any, + start: { line: 5 }, + }, setDecorations: jest.fn(), } as unknown as vscode.TextEditor; - lineSelectionManager = new LineSelectionManager(contextManagerMock); - }); + mockDecorationType = { + dispose: jest.fn(), + } as unknown as vscode.TextEditorDecorationType; - it('should remove last comment if decoration exists', () => { - const disposeMock = jest.fn(); - (lineSelectionManager as any).decoration = { dispose: disposeMock }; + (vscode.window.createTextEditorDecorationType as jest.Mock).mockReturnValue( + mockDecorationType, + ); - lineSelectionManager.removeLastComment(); - expect(disposeMock).toHaveBeenCalled(); - }); + (vscode.window.activeTextEditor as unknown) = mockEditor; - it('should not proceed if no editor is provided', () => { - expect(() => lineSelectionManager.commentLine(null as any)).not.toThrow(); + manager = new LineSelectionManager(mockSmellsCacheManager); }); - it('should not add comment if no smells detected for file', () => { - (contextManagerMock.getWorkspaceData as jest.Mock).mockReturnValue({}); - lineSelectionManager.commentLine(mockEditor); - expect(mockEditor.setDecorations).not.toHaveBeenCalled(); - }); + describe('constructor', () => { + it('should initialize with empty decoration and null lastDecoratedLine', () => { + expect((manager as any).decoration).toBeNull(); + expect((manager as any).lastDecoratedLine).toBeNull(); + }); - it('should not add comment if document hash does not match', () => { - (contextManagerMock.getWorkspaceData as jest.Mock).mockReturnValue({ - '/test/file.py': { hash: 'differentHash', smells: [] }, + it('should register smellsUpdated callback', () => { + expect(mockSmellsCacheManager.onSmellsUpdated).toHaveBeenCalled(); }); - lineSelectionManager.commentLine(mockEditor); - expect(mockEditor.setDecorations).not.toHaveBeenCalled(); }); - it('should not add comment for multi-line selections', () => { - // Set up multi-line selection - (mockEditor.selection as any).isSingleLine = false; + describe('removeLastComment', () => { + it('should dispose decoration if it exists', () => { + (manager as any).decoration = mockDecorationType; + (manager as any).lastDecoratedLine = 5; - lineSelectionManager.commentLine(mockEditor); + manager.removeLastComment(); - expect(mockEditor.setDecorations).not.toHaveBeenCalled(); + expect(mockDecorationType.dispose).toHaveBeenCalled(); + expect((manager as any).decoration).toBeNull(); + expect((manager as any).lastDecoratedLine).toBeNull(); + }); + + it('should do nothing if no decoration exists', () => { + manager.removeLastComment(); + expect(mockDecorationType.dispose).not.toHaveBeenCalled(); + }); }); - it('should not add comment when no smells exist at line', () => { - // Mock smells array with no matching line - (contextManagerMock.getWorkspaceData as jest.Mock).mockReturnValue({ - '/test/file.py': { - hash: 'mockHash', - smells: [ - { symbol: 'PERF-001', occurences: [{ line: 6 }] }, // Different line - { symbol: 'SEC-002', occurences: [{ line: 7 }] }, - ], - }, + describe('commentLine', () => { + it('should do nothing if no editor is provided', () => { + manager.commentLine(null as any); + expect(vscode.window.createTextEditorDecorationType).not.toHaveBeenCalled(); }); - lineSelectionManager.commentLine(mockEditor); + it('should do nothing if selection is multi-line', () => { + (mockEditor.selection as any).isSingleLine = false; + manager.commentLine(mockEditor); + expect(vscode.window.createTextEditorDecorationType).not.toHaveBeenCalled(); + }); - expect(mockEditor.setDecorations).not.toHaveBeenCalled(); - }); + it('should remove last comment if no smells are cached', () => { + mockSmellsCacheManager.getCachedSmells.mockReturnValue(undefined); + const removeSpy = jest.spyOn(manager, 'removeLastComment'); - it('should display single smell comment without count', () => { - // Mock single smell at line - (contextManagerMock.getWorkspaceData as jest.Mock).mockReturnValue({ - '/test/file.py': { - hash: 'mockHash', - smells: [{ symbol: 'PERF-001', occurences: [{ line: 5 }] }], - }, + manager.commentLine(mockEditor); + + expect(removeSpy).toHaveBeenCalled(); + expect(vscode.window.createTextEditorDecorationType).not.toHaveBeenCalled(); }); - lineSelectionManager.commentLine(mockEditor); + it('should do nothing if no smells exist at selected line', () => { + mockSmellsCacheManager.getCachedSmells.mockReturnValue([ + createMockSmell('LongMethod', 10), // Different line + ]); - expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledWith( - expect.objectContaining({ - after: expect.objectContaining({ - contentText: '🍂 Smell: PERF-001', - }), - }), - ); - }); + manager.commentLine(mockEditor); + + expect(vscode.window.createTextEditorDecorationType).not.toHaveBeenCalled(); + }); - it('should add a single-line comment if a smell is found', () => { - lineSelectionManager.commentLine(mockEditor); + it('should create decoration for single smell at line', () => { + mockSmellsCacheManager.getCachedSmells.mockReturnValue([ + createMockSmell('LongMethod', 6), // line + 1 + ]); - expect(mockEditor.setDecorations).toHaveBeenCalledWith( - expect.any(Object), - expect.any(Array), - ); + manager.commentLine(mockEditor); + + expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalled(); + expect(mockEditor.setDecorations).toHaveBeenCalledWith( + mockDecorationType, + expect.any(Array), + ); + expect((manager as any).lastDecoratedLine).toBe(5); + + const decorationConfig = ( + vscode.window.createTextEditorDecorationType as jest.Mock + ).mock.calls[0][0]; + expect(decorationConfig.after.contentText).toBe('🍂 Smell: LongMethod'); + }); + + it('should create decoration with count for multiple smells at line', () => { + mockSmellsCacheManager.getCachedSmells.mockReturnValue([ + createMockSmell('LongMethod', 6), + createMockSmell('ComplexCondition', 6), + ]); + + manager.commentLine(mockEditor); + + const decorationConfig = ( + vscode.window.createTextEditorDecorationType as jest.Mock + ).mock.calls[0][0]; + expect(decorationConfig.after.contentText).toContain( + '🍂 Smell: LongMethod | (+1)', + ); + }); + + it('should not create decoration if same line is already decorated', () => { + (manager as any).lastDecoratedLine = 5; + mockSmellsCacheManager.getCachedSmells.mockReturnValue([ + createMockSmell('LongMethod', 6), + ]); + + manager.commentLine(mockEditor); + + expect(vscode.window.createTextEditorDecorationType).not.toHaveBeenCalled(); + }); }); - it('should display a combined comment if multiple smells exist', () => { - lineSelectionManager.commentLine(mockEditor); - - // Verify the decoration type was created with correct options - expect(vscode.window.createTextEditorDecorationType).toHaveBeenCalledWith({ - isWholeLine: true, - after: { - contentText: expect.stringContaining('🍂 Smell: PERF-001 | (+1)'), - color: 'rgb(153, 211, 212)', - margin: '0 0 0 10px', - textDecoration: 'none', - }, + describe('smellsUpdated callback', () => { + let smellsUpdatedCallback: (targetFilePath: string) => void; + + beforeEach(() => { + smellsUpdatedCallback = (mockSmellsCacheManager.onSmellsUpdated as jest.Mock) + .mock.calls[0][0]; }); - // Verify decorations were applied to correct range - expect(mockEditor.setDecorations).toHaveBeenCalledWith( - expect.any(Object), // The decoration type instance - [new vscode.Range(4, 0, 4, 0)], // Expected range - ); + it('should remove comment when cache is cleared for all files', () => { + const removeSpy = jest.spyOn(manager, 'removeLastComment'); + smellsUpdatedCallback('all'); + expect(removeSpy).toHaveBeenCalled(); + }); + + it('should remove comment when cache is cleared for current file', () => { + const removeSpy = jest.spyOn(manager, 'removeLastComment'); + smellsUpdatedCallback('/test/file.js'); + expect(removeSpy).toHaveBeenCalled(); + }); + + it('should not remove comment when cache is cleared for different file', () => { + const removeSpy = jest.spyOn(manager, 'removeLastComment'); + smellsUpdatedCallback('/other/file.js'); + expect(removeSpy).not.toHaveBeenCalled(); + }); }); }); From b8dc03bf4d66796fec565382a73b4ecd9b590845 Mon Sep 17 00:00:00 2001 From: Tanveer Brar <92374772+tbrar06@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:46:53 -0400 Subject: [PATCH 101/121] update readme ssm-lab/capstone--source-code-optimizer#261 (#10) --- README.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fe74f71..39dfc36 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,81 @@ -VS Code Plugin for Source Code Optimizer - -1. clone this repo -2. open terminal and write: - `npm install` (only the first time) - `npm run compile` | `npm run watch` <- second command will auto compile on save -3. open another vs code window with the ecooptimzer repo -4. start venv in the ecooptimizer repo -5. run "python -m ecooptimizer.api.main" in terminal to start the developement server manually -6. come back to this repo, go to run and debug (or just click `F5` key) -7. run extension (should open a new vs code window so open the repo u want in this) -8. in the vscode search bar (`ctrl+shift+p`) type ">eco: detect smells" and run it +# EcoOptimizer VS Code Extension + +A Visual Studio Code extension that helps developers write more energy-efficient code by detecting code smells and suggesting refactoring opportunities. This extension integrates with the EcoOptimizer refactoring tool to provide real-time feedback and improvements for your Python codebase. + +## Features + +- **Code Smell Detection**: Automatically detects energy-inefficient patterns in your Python code +- **Refactoring Suggestions**: Provides actionable refactoring recommendations to improve code efficiency +- **Carbon Metrics**: Tracks and displays the potential energy savings from your refactoring efforts +- **Customizable Filters**: Configure which code smells to detect and analyze +- **Interactive UI**: Dedicated views for refactoring details, code smells, and metrics + +## Prerequisites + +- Visual Studio Code +- Python environment with EcoOptimizer package installed +- npm for extension development + +## Installation + +1. Clone this repository: + ``` + git clone https://github.com/your-repo/ecooptimizer-vs-code-plugin.git + cd ecooptimizer-vs-code-plugin + ``` + +2. Install dependencies: + ``` + npm install + ``` + +3. Compile the extension: + ``` + npm run compile + ``` + +## Usage + +1. Open your Python project in VS Code +2. Start the EcoOptimizer development server: + ```bash + python -m ecooptimizer.api.main + ``` + +3. Press `F5` to start the extension in a new VS Code window +4. Use the sidebar to use the features + +## Development + +### Project Structure + +- `src/`: Source code for the extension + - `api/`: API integration with EcoOptimizer + - `commands/`: VS Code command implementations + - `providers/`: TreeView providers for the UI + - `ui/`: UI components and views + - `utils/`: Utility functions + - `extension.ts`: Main extension entry point + +### Available Scripts + +- `npm run compile`: Compile the extension +- `npm run watch`: Watch for changes and recompile +- `npm test`: Run tests +- `npm run lint`: Run ESLint +- `npm run package`: Create production build + +### Testing + +The project uses Jest for testing. Run tests with: +```bash +npm test +``` + +## Contributors + +- Sevhena Walker +- Tanveer Brar +- Ayushi Amin +- Mya Hussain +- Nivetha Kuruparan From ea58250ca9dbb4027d81dad477c766d5dc39d054 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:09:45 -0400 Subject: [PATCH 102/121] push v0.1.1 - fixes ssm-lab/capstone--source-code-optimizer#503 --- .env | 2 +- .gitignore | 1 + .vscodeignore | 26 +-- LICENSE | 9 + README.md | 100 ++++------ package-lock.json | 129 ++++++++++++- package.json | 26 ++- src/createVenv.py | 4 + src/emitters/serverStatus.ts | 2 + src/extension.ts | 52 ++++- src/global.d.ts | 8 +- src/install.ts | 277 +++++++++++++++++++++++++++ src/lib/dependencyManager.ts | 54 ++++++ src/lib/processManager.ts | 95 +++++++++ src/providers/MetricsViewProvider.ts | 121 +++++++++--- src/utils/TreeStructureBuilder.ts | 5 +- src/utils/envConfig.ts | 16 +- vsc-extension-quickstart.md | 48 ----- webpack.config.js | 17 +- 19 files changed, 802 insertions(+), 190 deletions(-) create mode 100644 LICENSE create mode 100644 src/createVenv.py create mode 100644 src/install.ts create mode 100644 src/lib/dependencyManager.ts create mode 100644 src/lib/processManager.ts delete mode 100644 vsc-extension-quickstart.md diff --git a/.env b/.env index aee5d12..8cafc30 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -SERVER_URL='127.0.0.1:8000' +SERVER_URL='127.0.0.1' HASH_PATH_MAP_KEY='hashPathMap' SMELL_CACHE_KEY='smellCache' WORKSPACE_METRICS_DATA='metricsData' diff --git a/.gitignore b/.gitignore index 4a6b8bd..5516fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules .vscode-test/ *.vsix coverage/ +.venv/ \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index d255964..c80888d 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,14 +1,18 @@ -.vscode/** -.vscode-test/** -out/** -node_modules/** src/** -.gitignore -.yarnrc +.vscode +node_modules +package-lock.json +tsconfig.json webpack.config.js -vsc-extension-quickstart.md -**/tsconfig.json -**/eslint.config.mjs +eslint.config.mjs +.prettier* +.gitignore +run/** +.venv/** +test/** +.github/** +.husky/** +coverage/** **/*.map -**/*.ts -**/.vscode-test.* +.vscode-test.mjs +.env \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d87f17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2024-2025 Ayushi Amin, Mya Hussain, Nivetha Kuruparan, Sevhena Walker, Tanveer Brar + +Permission is hereby granted, on a case-by-case basis, to specific individuals or organizations ("Licensee") to use and access the software and associated documentation files (the "Software") strictly for evaluation or development purposes. This permission is non-transferable, non-exclusive, and does not grant the Licensee any rights to modify, merge, publish, distribute, sublicense, sell, or otherwise exploit the Software in any manner without explicit prior written consent from the copyright holder. + +Any unauthorized use, modification, distribution, or sale of the Software is strictly prohibited and may result in legal action. + +The Software is provided "AS IS," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software. + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. \ No newline at end of file diff --git a/README.md b/README.md index 39dfc36..c723736 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,55 @@ -# EcoOptimizer VS Code Extension +# EcoOptimizers - Sustainable Python Code Refactoring -A Visual Studio Code extension that helps developers write more energy-efficient code by detecting code smells and suggesting refactoring opportunities. This extension integrates with the EcoOptimizer refactoring tool to provide real-time feedback and improvements for your Python codebase. +EcoOptimizers is a VS Code extension that detects and refactors inefficient Python code, reducing unnecessary computations and lowering CO₂ emissions. By identifying common code smells and providing automated refactoring suggestions, EcoOptimizers helps you write cleaner, more efficient, and environmentally friendly code. ## Features -- **Code Smell Detection**: Automatically detects energy-inefficient patterns in your Python code -- **Refactoring Suggestions**: Provides actionable refactoring recommendations to improve code efficiency -- **Carbon Metrics**: Tracks and displays the potential energy savings from your refactoring efforts -- **Customizable Filters**: Configure which code smells to detect and analyze -- **Interactive UI**: Dedicated views for refactoring details, code smells, and metrics +- **Detect Code Smells** – Automatically analyze your Python code to find inefficiencies. +- **Refactor Code Smells** – Get instant suggestions and apply refactorings with ease. +- **Reduce CO₂ Emissions** – Improve computational efficiency and contribute to a greener future. +- **Seamless VS Code Integration** – Analyze and optimize your code directly within the editor. -## Prerequisites +## Supported Code Smells -- Visual Studio Code -- Python environment with EcoOptimizer package installed -- npm for extension development +EcoOptimizers detects and refactors the following common code smells: -## Installation - -1. Clone this repository: - ``` - git clone https://github.com/your-repo/ecooptimizer-vs-code-plugin.git - cd ecooptimizer-vs-code-plugin - ``` - -2. Install dependencies: - ``` - npm install - ``` +- **Cache Repeated Calls** – Identifies functions that repeatedly compute the same result and suggests caching techniques. +- **Long Lambda Functions** – Flags excessively long lambda expressions and converts them into named functions for readability and maintainability. +- **Use A Generator** – Suggests using generators instead of list comprehensions for memory efficiency. +- **Long Element Chain** – Detects deeply nested attribute accesses and recommends breaking them into intermediate variables for clarity. +- **Member Ignoring Method** – Identifies methods that do not use their class members and suggests converting them into static methods or external functions. +- **Long Message Chains** – Finds excessive method chaining and refactors them for better readability. +- **String Concatenation in Loop** – Detects inefficient string concatenation inside loops and suggests using lists or other optimized methods. +- **Long Parameter List** – Flags functions with too many parameters and suggests refactoring strategies such as grouping related parameters into objects. -3. Compile the extension: - ``` - npm run compile - ``` +## How It Works -## Usage +1. **Detect Smells** – Run the EcoOptimizers analysis tool to scan your code for inefficiencies. +2. **Refactor Suggestions** – View recommended changes and apply them with a click. +3. **Optimize Your Code** – Enjoy cleaner, more efficient Python code with reduced computational overhead. -1. Open your Python project in VS Code -2. Start the EcoOptimizer development server: - ```bash - python -m ecooptimizer.api.main - ``` +## Demo Videos -3. Press `F5` to start the extension in a new VS Code window -4. Use the sidebar to use the features +Watch EcoOptimizers in action: -## Development +- [Detecting Code Smells](https://drive.google.com/file/d/1Uyz0fpqjWVZVe_WXuJLB0bTtzOvjhefu/view?usp=sharing) 🔍 +- [Refactoring Code Smells](https://drive.google.com/file/d/1LQFdnKhuZ7nQGFEXZl3HQtF3TFgMJr6F/view?usp=sharing) 🔧 -### Project Structure - -- `src/`: Source code for the extension - - `api/`: API integration with EcoOptimizer - - `commands/`: VS Code command implementations - - `providers/`: TreeView providers for the UI - - `ui/`: UI components and views - - `utils/`: Utility functions - - `extension.ts`: Main extension entry point - -### Available Scripts +## Installation -- `npm run compile`: Compile the extension -- `npm run watch`: Watch for changes and recompile -- `npm test`: Run tests -- `npm run lint`: Run ESLint -- `npm run package`: Create production build +1. Open VS Code. +2. Go to the Extensions Marketplace. +3. Search for **EcoOptimizers**. +4. Click **Install**. +5. Intall the `ecooptimizer` python package. + - run: `pip install ecooptimizer` + - run: `eco-ext` +6. Start optimizing your Python code! -### Testing +## Contribute -The project uses Jest for testing. Run tests with: -```bash -npm test -``` +EcoOptimizers is open-source! Help improve the extension by contributing to our GitHub repository: [GitHub Repository](https://github.com/ssm-lab/capstone--source-code-optimizer) -## Contributors +--- -- Sevhena Walker -- Tanveer Brar -- Ayushi Amin -- Mya Hussain -- Nivetha Kuruparan +🚀 Start writing cleaner, more efficient Python code today with EcoOptimizers! \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1c722b8..9e36038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,26 @@ { "name": "ecooptimizer", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ecooptimizer", - "version": "0.0.1", + "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@types/dotenv": "^6.1.1", + "adm-zip": "^0.5.16", "bufferutil": "^4.0.9", "dotenv": "^16.4.7", "dotenv-webpack": "^8.1.0", + "follow-redirects": "^1.15.9", + "node-fetch": "^3.3.2", "utf-8-validate": "^6.0.5", "ws": "^8.18.0" }, "devDependencies": { + "@types/adm-zip": "^0.5.7", "@types/jest": "^29.5.14", "@types/node": "20.x", "@types/vscode": "^1.92.0", @@ -1522,6 +1527,15 @@ "optional": true, "peer": true }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2355,6 +2369,14 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -3315,6 +3337,14 @@ "node": ">=4" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -4025,6 +4055,28 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4120,6 +4172,25 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4137,6 +4208,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6361,6 +6443,41 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -8303,6 +8420,14 @@ "node": ">=10.13.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webpack": { "version": "5.97.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", diff --git a/package.json b/package.json index 3a29d98..35f2730 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecooptimizer", - "publisher": "ecooptimizers", + "publisher": "mac-ecooptimizers", "displayName": "EcoOptimizer VS Code Plugin", "contributors": [ "Sevhena Walker", @@ -14,7 +14,7 @@ "type": "git", "url": "https://github.com/ssm-lab/capstone--sco-vs-code-plugin" }, - "version": "0.0.1", + "version": "0.1.0", "engines": { "vscode": "^1.92.0" }, @@ -22,7 +22,8 @@ "Other" ], "activationEvents": [ - "onLanguage:python" + "onLanguage:python", + "onStartupFinished" ], "main": "./dist/extension.js", "directories": { @@ -32,6 +33,7 @@ "scripts": { "deploy": "vsce publish --yarn", "vscode:prepublish": "npm run package", + "postinstall": "node ./dist/install.js", "compile": "webpack", "test": "jest --verbose", "test:watch": "jest --watch --verbose", @@ -39,8 +41,7 @@ "package": "webpack --mode production --devtool hidden-source-map", "compile-tests": "tsc -p . --outDir out", "watch-tests": "tsc -p . -w --outDir out", - "lint": "eslint src", - "prepare": "husky" + "lint": "eslint src" }, "jest": { "preset": "ts-jest", @@ -91,6 +92,7 @@ ] }, "devDependencies": { + "@types/adm-zip": "^0.5.7", "@types/jest": "^29.5.14", "@types/node": "20.x", "@types/vscode": "^1.92.0", @@ -120,12 +122,16 @@ }, "dependencies": { "@types/dotenv": "^6.1.1", + "adm-zip": "^0.5.16", "bufferutil": "^4.0.9", "dotenv": "^16.4.7", "dotenv-webpack": "^8.1.0", + "follow-redirects": "^1.15.9", + "node-fetch": "^3.3.2", "utf-8-validate": "^6.0.5", "ws": "^8.18.0" }, + "icon": "./assets/green_leaf.png", "contributes": { "viewsContainers": { "activitybar": [ @@ -178,6 +184,16 @@ } ], "commands": [ + { + "command": "ecooptimizer.startServer", + "title": "Start EcoOptimizer Server", + "category": "Eco" + }, + { + "command": "ecooptimizer.stopServer", + "title": "Stop EcoOptimizer Server", + "category": "Eco" + }, { "command": "ecooptimizer.configureWorkspace", "title": "Configure Workspace", diff --git a/src/createVenv.py b/src/createVenv.py new file mode 100644 index 0000000..0013665 --- /dev/null +++ b/src/createVenv.py @@ -0,0 +1,4 @@ +import virtualenv +import os + +virtualenv.create_environment('.venv') \ No newline at end of file diff --git a/src/emitters/serverStatus.ts b/src/emitters/serverStatus.ts index 60bd515..dab00a4 100644 --- a/src/emitters/serverStatus.ts +++ b/src/emitters/serverStatus.ts @@ -61,6 +61,8 @@ class ServerStatus extends EventEmitter { // Notify subscribers this.emit('change', newStatus); + } else if (this.status === ServerStatusType.DOWN) { + vscode.commands.executeCommand('ecooptimizer.startServer'); } } } diff --git a/src/extension.ts b/src/extension.ts index aaf407d..a04e98a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,12 @@ import * as vscode from 'vscode'; import path from 'path'; +// let port: number; + +// export function getApiPort(): number { +// return port; +// } + // === Output Channel === export const ecoOutput = vscode.window.createOutputChannel('Eco-Optimizer', { log: true, @@ -13,6 +19,9 @@ export function isSmellLintingEnabled(): boolean { return smellLintingEnabled; } +// === In-Built === +import { existsSync, promises } from 'fs'; + // === Core Utilities === import { envConfig } from './utils/envConfig'; import { getNameByMessageId, loadSmells } from './utils/smellsData'; @@ -56,15 +65,34 @@ import { } from './utils/trackedDiffEditors'; import { initializeRefactorActionButtons } from './utils/refactorActionButtons'; import { LogManager } from './commands/showLogs'; -import { RefactorArtifacts } from './global'; -import { existsSync, promises } from 'fs'; + +// === Backend Server === +// import { ServerProcess } from './lib/processManager'; +// import { DependencyManager } from './lib/dependencyManager'; let backendLogManager: LogManager; +// let server: ServerProcess; -export function activate(context: vscode.ExtensionContext): void { +export async function activate(context: vscode.ExtensionContext): Promise { ecoOutput.info('Initializing Eco-Optimizer extension...'); console.log('Initializing Eco-Optimizer extension...'); + // === Install and Run Backend Server ==== + // if (!(await DependencyManager.ensureDependencies(context))) { + // vscode.window.showErrorMessage( + // 'Cannot run the extension without the ecooptimizer server. Deactivating extension.', + // ); + // } + + // server = new ServerProcess(context); + // try { + // port = await server.start(); + + // console.log(`Server started on port ${port}`); + // } catch (error) { + // vscode.window.showErrorMessage(`Failed to start server: ${error}`); + // } + backendLogManager = new LogManager(context); // === Load Core Data === @@ -127,6 +155,12 @@ export function activate(context: vscode.ExtensionContext): void { // === Register Commands === context.subscriptions.push( + // vscode.commands.registerCommand('ecooptimizer.startServer', async () => { + // port = await server.start(); + // }), + // vscode.commands.registerCommand('ecooptimizer.stopServer', async () => { + // server.dispose(); + // }), vscode.commands.registerCommand('ecooptimizer.configureWorkspace', async () => { await configureWorkspace(context); smellsViewProvider.refresh(); @@ -629,6 +663,16 @@ export function activate(context: vscode.ExtensionContext): void { cleanPastSessionArtifacts(); + // if (!port) { + // try { + // port = await server.start(); + + // console.log(`Server started on port ${port}`); + // } catch (error) { + // vscode.window.showErrorMessage(`Failed to start server: ${error}`); + // } + // } + ecoOutput.info('Eco-Optimizer extension activated successfully'); console.log('Eco-Optimizer extension activated successfully'); } @@ -637,5 +681,7 @@ export function deactivate(): void { ecoOutput.info('Extension deactivated'); console.log('Extension deactivated'); + // server.dispose(); backendLogManager.stopWatchingLogs(); + ecoOutput.dispose(); } diff --git a/src/global.d.ts b/src/global.d.ts index d25b7da..3371cf5 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -91,9 +91,9 @@ declare global { refactored: string; }[]; } -} -export interface RefactorArtifacts { - refactoredData: RefactoredData; - smell: Smell; + export interface RefactorArtifacts { + refactoredData: RefactoredData; + smell: Smell; + } } \ No newline at end of file diff --git a/src/install.ts b/src/install.ts new file mode 100644 index 0000000..ce482d4 --- /dev/null +++ b/src/install.ts @@ -0,0 +1,277 @@ +import * as path from 'path'; +import * as childProcess from 'child_process'; +import { access, unlink, writeFile } from 'fs/promises'; + +const PACKAGE_NAME = 'ecooptimizer'; +const PYPI_INDEX = 'https://pypi.org/simple'; + +interface InstallConfig { + pythonPath: string; + version: string; + targetDir: string; +} + +async function ensurePythonEnvironment(config: InstallConfig): Promise { + const venvPath = path.join(config.targetDir, '.venv'); + const isWindows = process.platform === 'win32'; + const pythonExecutable = path.join( + venvPath, + isWindows ? 'Scripts' : 'bin', + isWindows ? 'python.exe' : 'python', + ); + + try { + // 1. First verify Python is available + await new Promise((resolve, reject) => { + const pythonCheck = childProcess.spawn(config.pythonPath, ['--version']); + pythonCheck.stderr?.on('data', (chunk) => console.error(chunk)); + pythonCheck.stdout?.on('data', (chunk) => console.log(chunk)); + pythonCheck.on('close', (code) => { + code === 0 + ? resolve() + : reject(new Error(`Python check failed (code ${code})`)); + }); + pythonCheck.on('error', (err) => { + console.error(err); + reject(err); + }); + }); + + // 2. Check if venv already exists + let venvExists = false; + try { + await access(venvPath); + venvExists = true; + console.log('Virtual environment already exists'); + } catch { + console.log('Creating virtual environment...'); + } + + if (!venvExists) { + const tempFile = path.join(config.targetDir, 'create_venv_temp.py'); + try { + const scriptContent = ` +import sys +import venv +import os + +try: + venv.create("${venvPath.replace(/\\/g, '\\\\')}", + clear=True, + with_pip=True) + print("VENV_CREATION_SUCCESS") +except Exception as e: + print(f"VENV_CREATION_ERROR: {str(e)}", file=sys.stderr) + sys.exit(1) +`; + await writeFile(tempFile, scriptContent); + + const creationSuccess = await new Promise((resolve, reject) => { + const proc = childProcess.spawn(config.pythonPath, [tempFile], { + stdio: 'pipe', + }); + + let output = ''; + let errorOutput = ''; + + proc.stdout.on('data', (data) => (output += data.toString())); + proc.stderr.on('data', (data) => (errorOutput += data.toString())); + + proc.on('close', (code) => { + if (code === 0 && output.includes('VENV_CREATION_SUCCESS')) { + resolve(true); + } else { + const errorMatch = errorOutput.match(/VENV_CREATION_ERROR: (.+)/); + const errorMessage = + errorMatch?.[1] || `Process exited with code ${code}`; + console.error('Virtual environment creation failed:', errorMessage); + reject(new Error(errorMessage)); // Reject with the error message + } + }); + + proc.on('error', (err) => { + console.error('Process error:', err); + reject(err); // Reject with the process error + }); + }); + + if (!creationSuccess) { + try { + await access(pythonExecutable); + console.warn('Using partially created virtual environment'); + } catch (accessError) { + console.error( + 'Partial virtual environment creation failed:', + accessError, + ); + throw new Error('Virtual environment creation completely failed'); + } + } + } finally { + await unlink(tempFile).catch(() => {}); + } + } + + // 3. Verify the venv Python exists + await access(pythonExecutable); + return pythonExecutable; + } catch (error: any) { + console.error('Error in ensurePythonEnvironment:', error.message); + throw error; + } +} + +async function verifyPyPackage( + pythonPath: string, + config: InstallConfig, +): Promise { + console.log('Verifying python package version...'); + const installedVersion = childProcess + .execSync( + `"${pythonPath}" -c "import importlib.metadata; print(importlib.metadata.version('${PACKAGE_NAME}'))"`, + ) + .toString() + .trim(); + + if (installedVersion !== config.version) { + throw new Error( + `Version mismatch: Expected ${config.version}, got ${installedVersion}`, + ); + } + + console.log('Version match.'); + + return true; +} + +async function installFromPyPI(config: InstallConfig): Promise { + let pythonPath: string; + try { + pythonPath = await ensurePythonEnvironment(config); + console.log('Python environment is ready at:', pythonPath); + } catch (error: any) { + console.error('Failed to set up Python environment:', error.message); + return; + } + const pipPath = pythonPath.replace('python', 'pip'); + + if (await verifyPyPackage(pythonPath, config)) { + console.log('Package already installed.'); + return; + } + + console.log('Installing setup tools...'); + try { + childProcess.execSync(`"${pipPath}" install --upgrade "setuptools>=45.0.0"`, { + stdio: 'inherit', + }); + } catch (error) { + console.warn('Could not update setuptools:', error); + } + + console.log('Installing ecooptimizer...'); + try { + childProcess.execSync( + `"${pipPath}" install --index-url ${PYPI_INDEX} "${PACKAGE_NAME}==${config.version}"`, + { stdio: 'inherit' }, + ); + + verifyPyPackage(pythonPath, config); + console.log('✅ Installation completed successfully'); + } catch (error) { + console.error('❌ Installation failed:', error); + throw error; + } +} + +async function findPythonPath(): Promise { + // 1. Check explicitly set environment variable + if (process.env.PYTHON_PATH && (await validatePython(process.env.PYTHON_PATH))) { + return process.env.PYTHON_PATH; + } + + // 2. Common Python executable names (ordered by preference) + const candidates = ['python', 'python3.10', 'python3', 'py']; + + // 3. Platform-specific locations + if (process.platform === 'win32') { + candidates.push( + path.join( + process.env.LOCALAPPDATA || '', + 'Programs', + 'Python', + 'Python310', + 'python.exe', + ), + path.join(process.env.ProgramFiles || '', 'Python310', 'python.exe'), + ); + } + + if (process.platform === 'darwin') { + candidates.push('/usr/local/bin/python3'); // Homebrew default + } + + // Check common Python management tools + if (process.env.CONDA_PREFIX) { + candidates.push(path.join(process.env.CONDA_PREFIX, 'bin', 'python')); + } + + if (process.env.VIRTUAL_ENV) { + candidates.push(path.join(process.env.VIRTUAL_ENV, 'bin', 'python')); + } + + // 4. Test each candidate + for (const candidate of candidates) { + try { + if (await validatePython(candidate)) { + return candidate; + } + } catch { + continue; + } + } + + throw new Error('No valid Python installation found'); +} + +async function validatePython(pythonPath: string): Promise { + try { + // Check Python exists and has required version (3.9+) + const versionOutput = childProcess + .execSync(`"${pythonPath}" --version`) + .toString() + .trim(); + + // Parse version (e.g., "Python 3.10.6") + const versionMatch = versionOutput.match(/Python (\d+)\.(\d+)/); + if (!versionMatch) return false; + + const major = parseInt(versionMatch[1]); + const minor = parseInt(versionMatch[2]); + + console.log('Python version:', major, minor); + + return major === 3 && minor >= 9; // Require Python 3.9+ + } catch { + return false; + } +} + +// Updated main execution block +if (require.main === module) { + (async (): Promise => { + try { + const config: InstallConfig = { + pythonPath: await findPythonPath(), + version: require('../package.json').version, + targetDir: process.cwd(), + }; + + console.log(`Using Python at: ${config.pythonPath}`); + await installFromPyPI(config); + } catch (error) { + console.error('Fatal error:', error instanceof Error ? error.message : error); + process.exit(1); + } + })(); +} diff --git a/src/lib/dependencyManager.ts b/src/lib/dependencyManager.ts new file mode 100644 index 0000000..866ff16 --- /dev/null +++ b/src/lib/dependencyManager.ts @@ -0,0 +1,54 @@ +import { existsSync } from 'fs'; +import { join } from 'path'; +import * as vscode from 'vscode'; +import childProcess from 'child_process'; + +export class DependencyManager { + static async ensureDependencies( + context: vscode.ExtensionContext, + ): Promise { + const venvPath = join(context.extensionPath, '.venv'); + if (existsSync(venvPath)) return true; + + const choice = await vscode.window.showErrorMessage( + 'Python dependencies missing. Install now?', + 'Install', + 'Cancel', + ); + + if (choice === 'Install') { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Installing dependencies...', + }, + async () => { + try { + await this.runInstaller(context); + return true; + } catch (error) { + vscode.window.showErrorMessage(`Installation failed: ${error}`); + return false; + } + }, + ); + } + return false; + } + + private static async runInstaller( + context: vscode.ExtensionContext, + ): Promise { + return new Promise((resolve, reject) => { + const installer = childProcess.spawn('node', ['dist/install.js'], { + cwd: context.extensionPath, + stdio: 'inherit', + }); + installer.on('close', (code) => + code === 0 + ? resolve() + : reject(new Error(`Installer exited with code ${code}`)), + ); + }); + } +} diff --git a/src/lib/processManager.ts b/src/lib/processManager.ts new file mode 100644 index 0000000..7cc41cf --- /dev/null +++ b/src/lib/processManager.ts @@ -0,0 +1,95 @@ +import * as childProcess from 'child_process'; +import { existsSync } from 'fs'; +import * as net from 'net'; +import { join } from 'path'; +import * as vscode from 'vscode'; +import { ecoOutput } from '../extension'; + +export class ServerProcess { + private process?: childProcess.ChildProcess; + + constructor(private context: vscode.ExtensionContext) {} + + async start(): Promise { + const pythonPath = join( + this.context.extensionPath, + process.platform === 'win32' + ? '.venv\\Scripts\\python.exe' + : '.venv/bin/python', + ); + + if (!existsSync(pythonPath)) { + throw new Error('Python environment not found'); + } + + await this.killProcessTree(); // Cleanup any existing process + + const port = await this.findFreePort(); + this.process = childProcess.spawn( + pythonPath, + ['-m', 'ecooptimizer.api', '--port', port.toString(), '--dev'], + { + cwd: this.context.extensionPath, + env: { ...process.env, PYTHONUNBUFFERED: '1' }, + }, + ); + + this.process.stdout?.on('data', (data) => ecoOutput.info(`[Server] ${data}`)); + this.process.stderr?.on('data', (data) => ecoOutput.error(`[Server] ${data}`)); + this.process.on('close', () => { + ecoOutput.info('Server off.'); + console.log('Server off.'); + }); + + await this.verifyReady(port); + return port; + } + + private async findFreePort(): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + server.close(() => resolve(port)); + }); + }); + } + + private async killProcessTree(): Promise { + if (!this.process?.pid) return; + + try { + if (process.platform === 'win32') { + childProcess.execSync(`taskkill /PID ${this.process.pid} /T /F`); + } else { + process.kill(-this.process.pid, 'SIGKILL'); // Negative PID kills group + } + } catch (error) { + ecoOutput.error(`Cleanup failed: ${error}`); + } finally { + this.process = undefined; + } + } + + private async verifyReady(port: number, timeout = 10000): Promise { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + const socket = net.createConnection({ port }); + await new Promise((resolve, reject) => { + socket.on('connect', resolve); + socket.on('error', reject); + }); + socket.end(); + return; + } catch { + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } + throw new Error(`Server didn't start within ${timeout}ms`); + } + + dispose(): void { + this.process?.kill(); + } +} diff --git a/src/providers/MetricsViewProvider.ts b/src/providers/MetricsViewProvider.ts index 43f58c3..d1f0806 100644 --- a/src/providers/MetricsViewProvider.ts +++ b/src/providers/MetricsViewProvider.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; -import { basename } from 'path'; +import { basename, dirname } from 'path'; import { buildPythonTree } from '../utils/TreeStructureBuilder'; import { envConfig } from '../utils/envConfig'; import { getFilterSmells } from '../utils/smellsData'; @@ -56,11 +56,21 @@ export interface MetricsDataItem { }; } +interface FolderMetrics { + totalCarbonSaved: number; + smellDistribution: Map; // Map + children: { + files: Map; // Map + folders: Map; // Map + }; +} + export class MetricsViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData = new vscode.EventEmitter< MetricTreeItem | undefined >(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + private folderMetricsCache: Map = new Map(); constructor(private context: vscode.ExtensionContext) {} @@ -106,7 +116,10 @@ export class MetricsViewProvider implements vscode.TreeDataProvider this.createSmellItem(data)), + ...Array.from(folderMetrics.smellDistribution.entries()).map( + ([acronym, [name, carbonSaved]]) => + this.createSmellItem({ acronym, name, carbonSaved }), + ), ].sort(compareTreeItems); const contents = treeNodes.map((node) => { @@ -169,39 +182,65 @@ export class MetricsViewProvider implements vscode.TreeDataProvider { - let totalCarbonSaved = 0; - const smellDistribution = new Map(); + ): Promise { + // Check if we have cached metrics for this folder + const cachedMetrics = this.folderMetricsCache.get(folderPath); + if (cachedMetrics) { + return cachedMetrics; + } + + const folderMetrics: FolderMetrics = { + totalCarbonSaved: 0, + smellDistribution: new Map(), + children: { + files: new Map(), + folders: new Map(), + }, + }; const treeNodes = buildPythonTree(folderPath); - const fileNodes = treeNodes.filter((node) => node.isFile); - - for (const node of fileNodes) { - const fileMetrics = this.calculateFileMetrics(node.fullPath, metricsData); - totalCarbonSaved += fileMetrics.totalCarbonSaved; - - for (const smellData of fileMetrics.smellData) { - const current = smellDistribution.get(smellData.acronym)?.[1] || 0; - smellDistribution.set(smellData.acronym, [ - smellData.name, - current + smellData.carbonSaved, - ]); + + for (const node of treeNodes) { + if (node.isFile) { + const fileMetrics = this.calculateFileMetrics(node.fullPath, metricsData); + folderMetrics.children.files.set( + node.fullPath, + fileMetrics.totalCarbonSaved, + ); + folderMetrics.totalCarbonSaved += fileMetrics.totalCarbonSaved; + + for (const smellData of fileMetrics.smellData) { + const current = + folderMetrics.smellDistribution.get(smellData.acronym)?.[1] || 0; + folderMetrics.smellDistribution.set(smellData.acronym, [ + smellData.name, + current + smellData.carbonSaved, + ]); + } + } else { + const subFolderMetrics = await this.calculateFolderMetrics( + node.fullPath, + metricsData, + ); + folderMetrics.children.folders.set(node.fullPath, subFolderMetrics); + folderMetrics.totalCarbonSaved += subFolderMetrics.totalCarbonSaved; + + // Aggregate smell distribution from subfolder + subFolderMetrics.smellDistribution.forEach( + ([name, carbonSaved], acronym) => { + const current = folderMetrics.smellDistribution.get(acronym)?.[1] || 0; + folderMetrics.smellDistribution.set(acronym, [ + name, + current + carbonSaved, + ]); + }, + ); } } - return { - totalCarbonSaved, - smellData: Array.from(smellDistribution.entries()).map( - ([acronym, [name, carbonSaved]]) => ({ - acronym, - name, - carbonSaved, - }), - ), - }; + // Cache the calculated metrics + this.folderMetricsCache.set(folderPath, folderMetrics); + return folderMetrics; } private calculateFileMetrics( @@ -260,8 +299,30 @@ export class MetricsViewProvider implements vscode.TreeDataProvider( + envConfig.WORKSPACE_CONFIGURED_PATH!, + ); + + if (!configuredPath) { + return; + } + configuredPath = normalizePath(configuredPath); + + let currentPath = dirname(filePath); + console.log('file affected:', filePath); + + while (currentPath.includes(configuredPath)) { + this.folderMetricsCache.delete(currentPath); + currentPath = dirname(currentPath); + } + } } // Helper functions diff --git a/src/utils/TreeStructureBuilder.ts b/src/utils/TreeStructureBuilder.ts index b6a8404..8ad1a4a 100644 --- a/src/utils/TreeStructureBuilder.ts +++ b/src/utils/TreeStructureBuilder.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; +import { normalizePath } from './normalizePath'; /** * Options for configuring tree node appearance and behavior in the VS Code UI @@ -42,7 +43,7 @@ export function buildPythonTree(rootPath: string): TreeNode[] { const entries = fs.readdirSync(rootPath); for (const entry of entries) { - const fullPath = path.join(rootPath, entry); + const fullPath = normalizePath(path.join(rootPath, entry)); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { @@ -93,7 +94,7 @@ function containsPythonFiles(folderPath: string): boolean { const entries = fs.readdirSync(folderPath); for (const entry of entries) { - const fullPath = path.join(folderPath, entry); + const fullPath = normalizePath(path.join(folderPath, entry)); const stat = fs.statSync(fullPath); if (stat.isFile() && entry.endsWith('.py')) { diff --git a/src/utils/envConfig.ts b/src/utils/envConfig.ts index fc2bb37..45f3bd0 100644 --- a/src/utils/envConfig.ts +++ b/src/utils/envConfig.ts @@ -1,7 +1,3 @@ -import * as dotenv from 'dotenv'; - -dotenv.config(); - export interface EnvConfig { SERVER_URL?: string; SMELL_CACHE_KEY?: string; @@ -12,10 +8,10 @@ export interface EnvConfig { } export const envConfig: EnvConfig = { - SERVER_URL: process.env.SERVER_URL, - SMELL_CACHE_KEY: process.env.SMELL_CACHE_KEY, - HASH_PATH_MAP_KEY: process.env.FILE_HASH_CACHE_KEY, - WORKSPACE_METRICS_DATA: process.env.WORKSPACE_METRICS_DATA, - WORKSPACE_CONFIGURED_PATH: process.env.WORKSPACE_CONFIGURED_PATH, - UNFINISHED_REFACTORING: process.env.UNFINISHED_REFACTORING, + SERVER_URL: '127.0.0.1:8000', + SMELL_CACHE_KEY: 'smellCacheKey', + HASH_PATH_MAP_KEY: 'hashMapKey', + WORKSPACE_METRICS_DATA: 'workspaceMetrics', + WORKSPACE_CONFIGURED_PATH: 'workspacePath', + UNFINISHED_REFACTORING: 'pastRefactor', }; diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md deleted file mode 100644 index f518bb8..0000000 --- a/vsc-extension-quickstart.md +++ /dev/null @@ -1,48 +0,0 @@ -# Welcome to your VS Code Extension - -## What's in the folder - -* This folder contains all of the files necessary for your extension. -* `package.json` - this is the manifest file in which you declare your extension and command. - * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. -* `src/extension.ts` - this is the main file where you will provide the implementation of your command. - * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. - * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. - -## Setup - -* install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) - - -## Get up and running straight away - -* Press `F5` to open a new window with your extension loaded. -* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. -* Set breakpoints in your code inside `src/extension.ts` to debug your extension. -* Find output from your extension in the debug console. - -## Make changes - -* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. -* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. - - -## Explore the API - -* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. - -## Run tests - -* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) -* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. -* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` -* See the output of the test result in the Test Results view. -* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. - * The provided test runner will only consider files matching the name pattern `**.test.ts`. - * You can create folders inside the `test` folder to structure your tests any way you want. - -## Go further - -* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). -* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. -* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). diff --git a/webpack.config.js b/webpack.config.js index ea0eca6..320a4f0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,20 +1,20 @@ const path = require('path'); -const nodeExternals = require('webpack-node-externals'); -const Dotenv = require('dotenv-webpack'); module.exports = { target: 'node', - entry: './src/extension.ts', + entry: { + extension: './src/extension.ts', + install: './src/install.ts' // Separate entry point for install script + }, output: { path: path.resolve(__dirname, 'dist'), - filename: 'extension.js', - libraryTarget: 'commonjs2', + filename: '[name].js', + libraryTarget: 'commonjs2' }, resolve: { extensions: ['.ts', '.js'], }, externals: [ - nodeExternals(), { vscode: 'commonjs vscode' }, ], module: { @@ -31,9 +31,4 @@ module.exports = { infrastructureLogging: { level: 'log' // enables logging required for problem matchers }, - plugins: [ - new Dotenv({ - path: './.env', - }), - ], }; \ No newline at end of file From e26804fe6fb92da6716c708c518da7f5869ffee0 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:12:36 -0400 Subject: [PATCH 103/121] 0.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e36038..191e82a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ecooptimizer", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ecooptimizer", - "version": "0.1.0", + "version": "0.2.0", "hasInstallScript": true, "dependencies": { "@types/dotenv": "^6.1.1", diff --git a/package.json b/package.json index 35f2730..bbd27c3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "type": "git", "url": "https://github.com/ssm-lab/capstone--sco-vs-code-plugin" }, - "version": "0.1.0", + "version": "0.2.0", "engines": { "vscode": "^1.92.0" }, From fd2b31c6b16036338f648da1fca5447df1594b6b Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:26:39 -0400 Subject: [PATCH 104/121] Add real logo --- assets/eco_logo.png | Bin 0 -> 12250 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/eco_logo.png diff --git a/assets/eco_logo.png b/assets/eco_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a718c531ad3777a9df9e68b95b01ae98bb605331 GIT binary patch literal 12250 zcmbt)cUaR|)95e25>yOe1BD<+0v4(QVyM9a5y4fIjx?hnC6t697J^Yx(V&qg6_j z=AxMhg3KnluiJ_sD7oTSO%)*R!oCymXGXlce-eUV9w>fMzpcRNBFHc5QQQ2eex92M z5tLZ-u$`1}vUy5uJYXY;gL6uJSVT0LY7kD|9Tn$jGF0DWVi2{{(Ztur)6z43EjcpE zJvEW+o$9qMA~iZ<%}x_%r8Vk#n7Asep0ft%9-Z*MlidkHe5m1S+hFK($Z{~b%dpv zwUw2PS-52w*=#3ywUyP%HEZmwEyMmH^mEC7lGzXrRBTq-thBXRV`Xi%dX0_UzjmIU z{7?LC6k0^2;wVll|DpRY*fg$#g+jV9Q9p$;J@{KH|E%~avY&|mrk4M{s&+>Fqo#OT zV$8G&?2ND=$B<*maa4tpto~Ng&IkfEiW)=ymx_on)c;fw*d8G!EN-`>NeT#VR~RjZ zYU1P?Ln9l6#qBikC&$E4=mwsYo#ezggSGI-z?-s*N)JmU8*HNNrWn8hqISm_m>GaX z{*ROYy_ti>zwz<^aqjqkvwnqrPCKE(GGPJ^z3T5n!-znnQ6njdPTRublF8&m1J}f` zLaJwEL3kqQz|mC!aSO5b^xbVhCCA3c zfUh|IziPP%eC>Uv*DhWD8vp+M)Q|kJgKW>u z9MR-;-1jFR&*!})B1m%swn!QN^je7#WF3QuMhukH2?(MU%10r`5GDq3QPH6y$dYD` zGNOr6#Uczf%l{^7$u(M81vl@yzJ1*A7gpf@Np@Z1t4PZ`aU~rn#PH5z0+Q<$puLY2 zVEd&jP_W{LzQ^hb8!PuW7ekS`$B7x|E6sB}2(AJ{_SbWjL4rp14)V)i5Pd7LcvFD( zaqcQy-V%upDKjP$G0;~h9J-Jy{jR)7W6Y{`4WenLiY@k#h65J9!A(#GApR_MLckee zivwwhEhmyp7JWcmhAhRoXDiKj7%l5e`Gaor1jTq2&L1w|(h`cen>_-eL`p~~-^wE@ z_!3U?nf)pp#wi#kYmhB$vBw7+NGzobkmgxd;@m9(+C!)1_)`{$GLrkNI^p_n!WIRW5#rxZ=#uxY^UA2>!k$#hG()!8=7LM2%U|yU(CF)6u3eQvFi_es7LDp_3O;eW4s! zL8$4(p^QxYU>p`TI)_wPWVCC=!wQSlXCBnQ+ zh9HOAIBl^@qi)hv5CaEQ$E{JpAB`kRQn0kuI`Wj@NGyVgF-(2n`0H>O zxiqT~!A79Rm@TVbkmZg4vxbE*qIwN(&iM9SSIMbHS3T9is~qMf*5of+q(`0l%|=u1 zynuZ}Gl*F(3RCUw<0(&%?kB*$NT7PE-m4g3_;w4YZNOcMAPnFT0}dqDW6FX8v?-eT z&S=J~2>zt~a|NBOS?%BG3|h0wK6e3uG?h&)nf@*7V>ziIT~5b*-0EF=Uyi6Lowj6aRE42s4oBSIbO6s7_r z?pj3BW~tsUELw|%vMt3d&)|`&%}1}^{S+%+`ze-a>Qtomls-F`KPG@dl4q#!g{uxYJU^rIOoebi6WhriKu@j$zv(XfD zvtjO}cFx#k8iJ%e;MjxV>I-KZo>aYmhNi55Ow51NGuTvxL6A}%YE!tLye|-qFnrZ{ zA@zZ1mH14>x)#nvRWreW(d75{loe*&5wy}jfA#L+6}*~WH|D@q;q2U}8%Ul0ZjXJu zeKym_Qc0JK#Uo#;KZSbx+%=lKeTh1j#E4=awa4@Tww>M@=> zAib|ishgPJqebw|P-d(N77bypPJ^A;HO&@_n zwpXyW`_em^Z;YF`VdDMDhZXAtb!HAPRxL{7#$4sjezw0M?6VtiR{iM$Vyw;tMCPkD zBl$YPttTPY9o*qImBnoNr{|@k9^>_`zS8=NWrf$J=19_g5K#~3^D23q*~QdN;CNry z!bpct(YQ%}!Jw!AWYt9nqyi)aNY2q%#s_}`iJ5nn=XI8BiZlF2-*U+=QYX0Tw$60q zau#uPMfKU&cCMpu#5dcFNtc6f43opso@T!oQok^5yxmXXFjHli=1WDnsYPBRABWf_ z+0^ykKJ-PlsD^tWhqE9DLPNUhq3+FsG4_+qBOi@PeP^g-Z=bAOAJiGC$nD6Il^sAu z?@F5_o)mIU9?#z036YLV)6`s{iamSCNd7I)RC4rYnDb+T>ap2O7U{0PU^1bcdy(xh z5ysw)4s1ipLiyN@(k0cp74GV(i}Q>nLR?3Q*GLRSs?%(*D!S42B>tR^+{u2e-~z70 zX(v`mp`!;JBinip-jatEi%s@ccM()S9V3n|Yt7P4yszmlxXF$KX17T-n{^dj5Zk8A z&dA^3I?T&3D2Fh<)iNDwjHC#*@fg4*lDtpV1TgrR=Vho~|RA@WUXsuZZa0-7tTL$Vz5b<$+F#V_^@KVP>b8$ zJN97tD^;A@abAdW@deqv4+Nt5SB$cs6)Q7J zfS*AH(=5K$qXJyVD@byX??#tZ6Z=B3jNgkZF$k}jGf|m2?f-`#aIR_tjpPbUJylOLgs&M7Kyc2ya)wvdqdc zkx$PGQQ{+$Y=@rv1SPd))jjcFTf(J`1z*Mnzn)?rwE5GykNAcjwxjW-QFWnmfL)NF zN~qpRJ6uO*4L2u{z3Or(4wbKkw`I zSUkIhbS%7=4kB|`=Q(>6L%^%0xhO*CdVv|c5l;fsUM4a9@>y5_F%@xr&9zJWS`9*R z6Jz&0`m6PN(UY9YEG{Krl={);Zz}49_eIMJt33S=xC*2=QH8S>v#?XZDuVg8J( z!Jhu27uhcc*o}Kcf+M&NyNzy@I@GkUHJHekMncr-3+3aNzF+FK)t(3dL0XD`-06l` zqY;P>1=6$d!EqujznGzT)J2$jkUXP3qe%JFrRzni>>rULtzTpE=b&uE_*jxOeDV5Z zEIr|{)A3g;B+I17Ty&`GGKr(_;-w5kR)cB&Y(u7HaRof1)72#e^d>!3(K(|cT8z8tqk6S)%;_Q6fu|w@GcgAs2bBOzh1Ix3N2+J;XDncPY&ul3pamsu*q zh(cM4k3l|P7oeT7w6SK^R2Y`AaPkRcwFd-5?a0N7Z)E}))E6mRUWF8R`1ISC1>m={ z<-MPA2W%fCQLckj%bH7389G#F9V+$KvcfNr;kNJZbO~9y><^B|h0;&Hz4fkhm@Msn zGdfzp^DFvx@SwSsSih(YqZXC?GG;`wK30FB?OSMZ=sXzUlDeZ!;s8AwuEtb9Az^}wBI+qmrK@5dJO4rpu{Ukp8= zsi}{meSCfLf!5*7n;YE^@oyWT&0hTeD~IEr5TJGDUMgdO=2NADKXQE5`<+;^;)rWz z_0B;d6)$vsd4)COnD^3!OR`%Mm!4R^aZTmZa705*&Gg3U!zq!2i#wj>tTf!KG3wOR zFf@5^*Q){Nk-YTtD@J9F_3EijdEoeG_Fce*#A4Hz?QJEvY!&3Z5P^^NB|L1n@tSuG zKS&WhJwx$BhdvTQhRxc4Z1Iszw z$GiwvC=^3mb*RnCciKhnLE34?5_O=#>mnelTBlJ>OiMENvdz!QZKXLg#=XwIoeIaM zFN~O`KX+bd%L;t3S(Go#P28a&yN*mVz8NUpa39a+niU5ce_Te=_IC^Q(UISrIM3C! z|FJ6NsfO(_;*ejEc0FmlfZI?Uc+xbEeTkkvaf+B3vb*9!Fb6Sof zwe{rW2CR@&2(@x;RI#5BrVJLFY$SNzuixBgnD!Dk=Kar<)2ufp!U)oYN9t8>r^khe zMK2ah@n+R5#$4xW401b4eSBVv;$kLaf7O}#xGc)~+3yDfpX0{f2({u5WmO;`9y}oY z##P3fovu8^Ew&jq9@eDK* zlft0a-LJ9-X+Iz+d0B}WXdPZSV6ZT9lStO zA>5szI2cA-NFwwSl=6ORFn)mP$xiEl@c`StA8zIq+igfc$Wk@xi4dX+7*m9;c`W<; zHUO{`qR%8-dat;Eb8aOl%`JQKtz#-a`&yRW54!oD_7j0K)MheS=WEMIBYowp<_CWH z{Q8udyK{zP5T*Ng`lilPY)load7VGNKS6+Nw(%0@vgTiUjD9$N_|-3}*t-LVSvSX( zBAom#O{+im1m2Uq7;?96tbcJ?oVYm?@_HwfQjgkz1*l8nV-zl8BfIkEh=Z}@cQ|-C zhG^liOrl+Eq!-TD((@8`ou+K;K5NyMR8)nDkD2vz*ObQ37je!l1f_=i9A(Eg&YLuc zt54m0%xhrvk*}e8Ojf_8`1A&g%)M!6-)i_}aB4sufTz(DK+2C~?>|mcOSPyZ=0{_Z z+E$K6XD7ZPYbZD3glqlV#%naBe-YEkp<^SVf;7@1H2kzN(nNj^{IFy=I{JRadG44{ z>l4O=%K4xpA2@F$>8&-Fosx#9Np(`*L`ji0yDLU^>f`Zd*;MZ!ZI3s+E+Ij+p{ASA zTviO1Q#iif3Q1$F`5dXHyxUudV#UTzbFp@jX!D(=C7EQQp}3fDUS|P;Gbn;T9MtxJ zmOu~EZrX`G^eT*>ei>l>OQzIK(W$?we-qDt2LKPtwn93i${zogUoOGxP2NE0fl`z`^AKTd4hc4tLN zX7n3g|E%rGFLGX_9vQ8+Q^gXY0^VN6)qTzDKcaVkem8j(3yM6aL*3M-i!Z*-Vw`U) zR@Uoz`I829P`k4wQL2_3%1_si;9JdY!gjVm5tv)M*dJ}z$}w81PS~~lxb$^av61B# z$j)|O13;~Ku-yc&Anl3ayKw|ee z5!LVA1N1vvf~1;Ma|L|z0_bigxX2DONjO-%Y@ZN?O@V8o0_gi{ZfDC3p_bSy&_}~> zH8s>3Hl44EHP2hz*-{Pg=V^XuJ6nrK0%oxh0(D%iwXa~W0)7Cf*g+VLPbX$X&)D36 zj=e^6L1Un@2?7uI*+|$VNML)hsYkgR1%J-`TyZA2zL|jJ)jcC{lxJNvot!UK3$3=T zzrM~;NyJlF!_Tw?*hozM)M&Onl*>Xt_gm=H^IWHCpwVGor#T5{; zS!~QX>g^5r+VALPQyr6@Yzx5Nw(a5S6NBN8s(r`4X9oO)JN51zJqL8grt#&S1QiD5^dO>v4 ztWeW=9kV&XHJ@YraB&>_y9+68k7#AsO&W5k{w0C4_6C&0tL}1iBiY|gNhX2xP5d(n z!H;WL4DU{0W9g}K>v9%yJ(IXvfE$}bT02H~sYh}PaBIGK8XXM1AjFC-wcN;0jUyRX=uker%* z?fd$9J3{&U3P>X~wo|C+X;}mKkFrh}+&e5$?FFXT?s}tsLrCoxa8pOQ9f5S}$2=h_ ze$RPY^I_8uhHK1#9p0I2y$IiwfhNg(E?M8+o>m1itcH0ojk5l=nQzbMvw}M)8yLX4*%^^h>8yW8m zB&}ZWpZYsZ?bCks)U>=tz-b8y_xLqcIQj~N>E8LS8ccfb20n^mN48KcEe7g+TQ_h;(Ub`iyY?WWp&3EjwbdjJQhjm)d*vo+@&z7NgviuyWHw+=6wHMWl3ehS0H^)1B#OcSXY`&}X$f;RR1w4a=0BtB1h7ua1ToUo|V-DA+2+80>^*SR)zR zufuc#lf&NlRI(D)0DV0?+gtUBd0NKw|gz2?<3g zo_vQu?wD&}^l801g=NEaycJ zE^oI;=^L~wJFY@PT@N?sjOwBvzY5N8NM zM>Uu%PL<+1*MJX>1|o;2LH$+aDxO(kjmM+nKqLgj8pU>~qX_iJW;xULxF4F5nJ&ke%Y2SIdLYEB=)OVk&=A=x!%cn-JFgZj7u80_8so_@W<(= zULTg-*sVy<&=#45=&MHxIW5GK)btJc8%1#BPtXA~@e-@KW?!@S76-msmZuzrggs zS?=ZlI${WIc3}J!Fdj`h7f46006?A(#+0Dp7;|o8>Urqn5PmAp2h3|j_B9hyYCR2Y zsq#~bDoY;;&XEG(y+j>s`9{C0W63Pp)r=2INS`6eTmko!hk&!@Mqm+ofvIDV;EE92 zeEL(u0_R`ouG&oflW+u&fN;^kc{Hw=E&ulI#&2>i*QC|CrsvvSZP-JHDu4Nsom|cm z-^{>?@RGhZ=N0$^^=5^%>)X|oU`tJlI!YXt@lw}Wjb2W3%lhsv=p*qq2+U&r9m%~+ z2FU#NVOAp{_>xr zSmpqT`2n6Tf4WeJI-27#?hfUXe@1An!UpZcYnwJntA-`t=SeZj&e`&bSpVQytP;>1 zuTuoFa4f@rELe&tcX|*C%nDx!4H;lGCx~u+>aqOx>LMrTs5xl=&iIJML@6<|ofH8&WL@6?2c!u$xYFj88 zHS7F&E&>!3V)Y?&Hdow-_rh`Th#zYn5ki$%d)k*jSP`zK$&{7u1~YxLj6S8e3(GG{ z>MkGmovsI{X^Q)Y33ckueW+miDD8g*0h|C`s>ufsvh+`UcF(?s4p>cvW-}7X1Nf2T z5NN%(-v#R!U(`gwEVdH=2-rRCf0O0;G?$E{-(J|M^f$pn)6~?U$iE0j#vUC$=bjzr zcG1&PX#>hgt^DCHCypnpFW(W$P_Z~>V;mIOor_s-9d8)a6+oPI>_k`ktX8!X9HJHe z(HwGL8?w?R)K8D&x9jLawak=+fR&c`8ejgI{5iDcK^{J?_MdU~c&XX=g4eC|axzYpj4tmvLG zIVnS8Dvf$k$4y}Y0Tw_8RV?Uxfn}UZzHM)C=CcUZ>}n|of!SK&1grXZI9D?jC+3~9pS40=H(+P>Ni+|z=$^&Im!ghh<b?q>1i+R#&#IySzmT^Rq0hZ|k57MzKzbAAIHPp@UO65JmDjy3Ldv;7is>ATDA%gfvC$1Jx~?EK!wMd~nF z@hMw_CWhbZ^;O%!n zm5$tt;HNKyr|b9zdt|^$+EA5q(iWqc}_J)S8;I8o}U$o8B*(?+6UaU`=*h()YV z5wqSvigC@f6-W1?T%N%*ws{iTz*-fXn*cAz>V&P`tysRL(_{32rI@(QzcD}%(u-A7 zI1(9*&|1|oC$kr&80Qh)?9@{Yw&#SvBYwTI;Ss`>&h>SVMALhGI44661GAXJ_c?8! zlVHQ}X;#tR%$TPmXWTMP+a~x$hB;hb z1)6~Ok#PQ-@ypy7GPT26k{o+@%+Qf@Zkd+i5A=!BEtkw5PZObBW~!%#ZqLbIA&H}- zUAtak`5ZGiA3*n;)GIR<-sc$MR2^%Vw-2$Y*XxxBU+7|z6IWZG9NMRLyr~QOD|u|; zYoWbu2)C^R4+X*r;t(qWUrcr5{I;ZFCz!a_JmxE{?Ku|b@9(yMYV!6I-iJOj3E^FP?grPSN=mp@gVoJ~hgD=}XYfjbjPJs@tVBz`!o zI0Wlf`F9g`qfF+q!cy!q3vRkTWVk%Gxu?Dq9Xh`tmS6X78V%IHoa9DIeFD&`KR`Xe zA$*MjJ9`QS*!uLEP-AeERntJ+%|{~{IIOY6m_BQlf~6XTkXfs@`!^Om8ZpA>y^p)Q z>A0=96r?gwhgx*w(a0G^^Nwy-yqVI@}`j=7d{$!hQ}I8 zlIgQz3i9|1sqo4a!2~|8JsMeSGkW1mPp@Mx!E5>~U6EiK+sR{L?8YS$3|-^8aW|*w zH3r!l$}f8eOUhvh4r?Oup=+@B+1u z&}f|nhvpLa@D&mJX89fVHquszds_rgFH9KZcDNIs`yTb2H(0#2^XDlyt^e(vg*z`i z@usuBnV?tNqmf;i&z-~{2oji=Y`9Yx$^5~#N8F+RqCh0^k`o)ER?7En><$*7O+x6G zzgGm~2W`iAQu!Wxi|!j|6I|Wgp~>`Ur}&&_$=)w*S7UJvlilO7O*=-rmw3`rp6-}^ ze+JgjN*r0dyuH&s;Ehboh}?Vv9_Y4K=HEeCTXeSzYZZn-MRd#aKv0+XpixZd+T?h& zHM}_jamMVILM;FG_Gy#EHrM|(8@;bicaEQATHL6|yEW)8N`=#2LST%2e1mak1j@jK z(3bg=BGe2^h2_Bin`@Qlg#IM%lu@TVr{4+`yf08W#jRl=bTcWd&i;arHok9E6Y4W$ z=zTjR!+VnqrSFTGn10bEF~*&fj7Dz4w^+hwx)=;w?oW49>Hwi`1J;M$T0_zX))xS> z8Dyg{wpTnIcaLLas)}vC2mo8Qc>*0L&j#5-WBzu@XZ+wG%l+8yIt2y;cXPE~kf{Vm zcMAtL8QZFm^x zRj!8nU!INTD{(b92x2-6DhwFCNEai|B~uYygVw@~+MWF+U9k zonKEUqBUO{(YCyL13jtf!8;ie8UXWWg5SeD$XD}418vKXXIRvxSxk(ElrQ;f zzgs0!YgX*nGQwM$CWdw?Ol&a)tMCGPYB|r>kNMJ7`X6wVBkOnLXZLC`bm4@9EU3(& zqUwheMOILruiu*oJK)7VeSaAjN!|5e1C;?4zv0<02z&w*_&8b8N< z1LF>?QY8u;YC+x(?RWk%HI65ctazJB%9Iuay~m)skAic|x|~hj6P)eQYv>XPa=Ikk zNN^MQ)3%f(v>~y;czlYNppJboK>OV+nT~-HkhgX;l(exAwj9@s1e*_C6qiFy4$eNu zTg&(z=-+fOlytH|B71A>Rgb}Q2%o#1eb7iE;4Phc&qDDIgA4pxhzmB9umSQBASVEF zQ~x(=CRZ0H`c?a#*?WBiZ07OM4$`PQ&qI*Mjw{X_*-M$jpo4%!UkbQWk+O%;n>VKJ zACb1<9k*(~!?xNY7mpFq`{C*obZ_d1l)dA8P^njAV^LieE_l24yTTc16vVsw2z-Ty znCdA$wL}!3Uc!?wf~ZV?Z#n(_<^LSmJcV>EW!My+p4tmrV90)?U7nuEa`;~jeic6g R@3V?Or1f6wN?bz^{V()ZkB$HU literal 0 HcmV?d00001 diff --git a/package.json b/package.json index bbd27c3..55fcb27 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "utf-8-validate": "^6.0.5", "ws": "^8.18.0" }, - "icon": "./assets/green_leaf.png", + "icon": "./assets/eco_log.png", "contributes": { "viewsContainers": { "activitybar": [ From 4ecc60549988dad518e276ee49665fb5dbbf4628 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:28:44 -0400 Subject: [PATCH 105/121] 0.2.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 191e82a..4cb9351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ecooptimizer", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ecooptimizer", - "version": "0.2.0", + "version": "0.2.1", "hasInstallScript": true, "dependencies": { "@types/dotenv": "^6.1.1", diff --git a/package.json b/package.json index 55fcb27..52b18d7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "type": "git", "url": "https://github.com/ssm-lab/capstone--sco-vs-code-plugin" }, - "version": "0.2.0", + "version": "0.2.1", "engines": { "vscode": "^1.92.0" }, From f55d7def67d4b542e529f6eb15348b375a969a50 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:30:04 -0400 Subject: [PATCH 106/121] fix icon path --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52b18d7..3f8ef2c 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "utf-8-validate": "^6.0.5", "ws": "^8.18.0" }, - "icon": "./assets/eco_log.png", + "icon": "./assets/eco_logo.png", "contributes": { "viewsContainers": { "activitybar": [ From 6b618f393d8747b3038c6e3151969c59421cf214 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:31:25 -0400 Subject: [PATCH 107/121] 0.2.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4cb9351..7f966e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ecooptimizer", - "version": "0.2.1", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ecooptimizer", - "version": "0.2.1", + "version": "0.2.2", "hasInstallScript": true, "dependencies": { "@types/dotenv": "^6.1.1", diff --git a/package.json b/package.json index 3f8ef2c..02d9127 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "type": "git", "url": "https://github.com/ssm-lab/capstone--sco-vs-code-plugin" }, - "version": "0.2.1", + "version": "0.2.2", "engines": { "vscode": "^1.92.0" }, From e78a1c2a11088c855a371fe235f6b3ea155c61ff Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:29:07 -0400 Subject: [PATCH 108/121] fixed package.json scripts --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 02d9127..ce726b9 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,11 @@ "scripts": { "deploy": "vsce publish --yarn", "vscode:prepublish": "npm run package", - "postinstall": "node ./dist/install.js", "compile": "webpack", "test": "jest --verbose", "test:watch": "jest --watch --verbose", "watch": "webpack --watch", "package": "webpack --mode production --devtool hidden-source-map", - "compile-tests": "tsc -p . --outDir out", - "watch-tests": "tsc -p . -w --outDir out", "lint": "eslint src" }, "jest": { From 0ff0b6f60494d3bca62651edf3d59de4e8310670 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:03:08 -0400 Subject: [PATCH 109/121] Add better comments --- src/commands/configureWorkspace.ts | 2 +- src/commands/detection/detectSmells.ts | 18 +++++ src/commands/views/exportMetricsData.ts | 14 ++++ src/createVenv.py | 4 -- src/emitters/serverStatus.ts | 2 - src/install.ts | 63 +++++++++++++----- src/lib/README.md | 3 + src/lib/dependencyManager.ts | 25 ++++++- src/lib/processManager.ts | 51 ++++++++++++-- src/providers/FilterViewProvider.ts | 52 +++++++++++++-- src/providers/MetricsViewProvider.ts | 85 ++++++++++++++++++++++-- src/providers/SmellsViewProvider.ts | 82 +++++++++++++++++++---- src/ui/hoverManager.ts | 29 ++++++-- src/utils/initializeStatusesFromCache.ts | 19 ++++-- 14 files changed, 386 insertions(+), 63 deletions(-) delete mode 100644 src/createVenv.py create mode 100644 src/lib/README.md diff --git a/src/commands/configureWorkspace.ts b/src/commands/configureWorkspace.ts index f9a931c..5e1901e 100644 --- a/src/commands/configureWorkspace.ts +++ b/src/commands/configureWorkspace.ts @@ -18,7 +18,7 @@ export async function configureWorkspace( /** * Recursively identifies Python project folders by scanning for: * - Python files (*.py) - * - __init__.py package markers + * - \_\_init\_\_.py package markers * Maintains a hierarchical understanding of Python projects in the workspace. * * @param folderPath - Absolute filesystem path to scan diff --git a/src/commands/detection/detectSmells.ts b/src/commands/detection/detectSmells.ts index e530d2b..6dbb5a8 100644 --- a/src/commands/detection/detectSmells.ts +++ b/src/commands/detection/detectSmells.ts @@ -12,6 +12,10 @@ import { ecoOutput } from '../../extension'; /** * Performs code smell analysis on a single Python file with comprehensive state management. * Only shows user notifications for critical events requiring attention. + * + * @param filePath - Absolute path to the Python file to analyze + * @param smellsViewProvider - Provider for updating the UI with results + * @param smellsCacheManager - Manager for cached smell results */ export async function detectSmellsFile( filePath: string, @@ -26,6 +30,7 @@ export async function detectSmellsFile( if (!shouldProceed) return; + // Transform enabled smells into backend-compatible format const enabledSmells = getEnabledSmells(); const enabledSmellsForBackend = Object.fromEntries( Object.entries(enabledSmells).map(([key, value]) => [key, value.options]), @@ -35,6 +40,7 @@ export async function detectSmellsFile( ecoOutput.info(`[detection.ts] Analyzing: ${path.basename(filePath)}`); const { smells, status } = await fetchSmells(filePath, enabledSmellsForBackend); + // Handle backend response if (status === 200) { if (smells.length > 0) { ecoOutput.info(`[detection.ts] Detected ${smells.length} smells`); @@ -65,12 +71,15 @@ export async function detectSmellsFile( * - Using cached results (info) * - Server is down (warning) * - No smells configured (warning) + * + * @returns boolean indicating whether analysis should proceed */ async function precheckAndMarkQueued( filePath: string, smellsViewProvider: SmellsViewProvider, smellsCacheManager: SmellsCacheManager, ): Promise { + // Validate file scheme and extension const fileUri = vscode.Uri.file(filePath); if (fileUri.scheme !== 'file') { return false; @@ -80,6 +89,7 @@ async function precheckAndMarkQueued( return false; } + // Check for cached results if (smellsCacheManager.hasCachedSmells(filePath)) { const cached = smellsCacheManager.getCachedSmells(filePath); ecoOutput.info( @@ -95,6 +105,7 @@ async function precheckAndMarkQueued( return false; } + // Check server availability if (serverStatus.getStatus() === ServerStatusType.DOWN) { const msg = 'Backend server unavailable - using cached results where available'; ecoOutput.warn(`[detection.ts] ${msg}`); @@ -103,6 +114,7 @@ async function precheckAndMarkQueued( return false; } + // Verify at least one smell detector is enabled const enabledSmells = getEnabledSmells(); if (Object.keys(enabledSmells).length === 0) { const msg = 'No smell detectors enabled in settings'; @@ -118,6 +130,10 @@ async function precheckAndMarkQueued( /** * Recursively analyzes Python files in a directory with progress indication. * Shows a progress notification for the folder scan operation. + * + * @param folderPath - Absolute path to the folder to analyze + * @param smellsViewProvider - Provider for updating the UI with results + * @param smellsCacheManager - Manager for cached smell results */ export async function detectSmellsFolder( folderPath: string, @@ -133,6 +149,7 @@ export async function detectSmellsFolder( async () => { const pythonFiles: string[] = []; + // Recursive directory walker for Python files function walk(dir: string): void { try { const entries = fs.readdirSync(dir); @@ -167,6 +184,7 @@ export async function detectSmellsFolder( `Analyzing ${pythonFiles.length} Python files...`, ); + // Process each found Python file for (const file of pythonFiles) { await detectSmellsFile(file, smellsViewProvider, smellsCacheManager); } diff --git a/src/commands/views/exportMetricsData.ts b/src/commands/views/exportMetricsData.ts index 2e095e2..8fbb512 100644 --- a/src/commands/views/exportMetricsData.ts +++ b/src/commands/views/exportMetricsData.ts @@ -5,20 +5,30 @@ import { writeFileSync } from 'fs'; import { MetricsDataItem } from '../../providers/MetricsViewProvider'; import { envConfig } from '../../utils/envConfig'; +/** + * Exports collected metrics data to a JSON file in the workspace. + * Handles both file and directory workspace paths, saving the output + * as 'metrics-data.json' in the appropriate location. + * + * @param context - Extension context containing metrics data and workspace state + */ export async function exportMetricsData( context: vscode.ExtensionContext, ): Promise { + // Retrieve stored metrics data from extension context const metricsData = context.workspaceState.get<{ [path: string]: MetricsDataItem; }>(envConfig.WORKSPACE_METRICS_DATA!, {}); console.log('metrics data:', metricsData); + // Early return if no data available if (Object.keys(metricsData).length === 0) { vscode.window.showInformationMessage('No metrics data available to export.'); return; } + // Get configured workspace path from extension context const configuredWorkspacePath = context.workspaceState.get( envConfig.WORKSPACE_CONFIGURED_PATH!, ); @@ -30,6 +40,7 @@ export async function exportMetricsData( return; } + // Determine output file location based on workspace type const workspaceUri = vscode.Uri.file(configuredWorkspacePath); let fileUri: vscode.Uri; @@ -37,8 +48,10 @@ export async function exportMetricsData( const stat = await vscode.workspace.fs.stat(workspaceUri); if (stat.type === vscode.FileType.Directory) { + // For directories, save directly in the workspace root fileUri = vscode.Uri.joinPath(workspaceUri, 'metrics-data.json'); } else if (stat.type === vscode.FileType.File) { + // For single files, save in the parent directory const parentDir = vscode.Uri.file(dirname(configuredWorkspacePath)); fileUri = vscode.Uri.joinPath(parentDir, 'metrics-data.json'); } else { @@ -50,6 +63,7 @@ export async function exportMetricsData( return; } + // Write the metrics data to JSON file try { const jsonData = JSON.stringify(metricsData, null, 2); writeFileSync(fileUri.fsPath, jsonData, 'utf-8'); diff --git a/src/createVenv.py b/src/createVenv.py deleted file mode 100644 index 0013665..0000000 --- a/src/createVenv.py +++ /dev/null @@ -1,4 +0,0 @@ -import virtualenv -import os - -virtualenv.create_environment('.venv') \ No newline at end of file diff --git a/src/emitters/serverStatus.ts b/src/emitters/serverStatus.ts index dab00a4..60bd515 100644 --- a/src/emitters/serverStatus.ts +++ b/src/emitters/serverStatus.ts @@ -61,8 +61,6 @@ class ServerStatus extends EventEmitter { // Notify subscribers this.emit('change', newStatus); - } else if (this.status === ServerStatusType.DOWN) { - vscode.commands.executeCommand('ecooptimizer.startServer'); } } } diff --git a/src/install.ts b/src/install.ts index ce482d4..f018d07 100644 --- a/src/install.ts +++ b/src/install.ts @@ -2,15 +2,25 @@ import * as path from 'path'; import * as childProcess from 'child_process'; import { access, unlink, writeFile } from 'fs/promises'; +// Constants for package management const PACKAGE_NAME = 'ecooptimizer'; const PYPI_INDEX = 'https://pypi.org/simple'; +/** + * Configuration interface for Python environment setup + */ interface InstallConfig { pythonPath: string; version: string; targetDir: string; } +/** + * Ensures a valid Python virtual environment exists + * @param config Installation configuration + * @returns Path to the Python executable in the virtual environment + * @throws Error if environment setup fails + */ async function ensurePythonEnvironment(config: InstallConfig): Promise { const venvPath = path.join(config.targetDir, '.venv'); const isWindows = process.platform === 'win32'; @@ -21,7 +31,7 @@ async function ensurePythonEnvironment(config: InstallConfig): Promise { ); try { - // 1. First verify Python is available + // 1. Verify Python is available and executable await new Promise((resolve, reject) => { const pythonCheck = childProcess.spawn(config.pythonPath, ['--version']); pythonCheck.stderr?.on('data', (chunk) => console.error(chunk)); @@ -37,7 +47,7 @@ async function ensurePythonEnvironment(config: InstallConfig): Promise { }); }); - // 2. Check if venv already exists + // 2. Check for existing virtual environment let venvExists = false; try { await access(venvPath); @@ -50,6 +60,7 @@ async function ensurePythonEnvironment(config: InstallConfig): Promise { if (!venvExists) { const tempFile = path.join(config.targetDir, 'create_venv_temp.py'); try { + // Python script to create virtual environment const scriptContent = ` import sys import venv @@ -85,16 +96,17 @@ except Exception as e: const errorMessage = errorMatch?.[1] || `Process exited with code ${code}`; console.error('Virtual environment creation failed:', errorMessage); - reject(new Error(errorMessage)); // Reject with the error message + reject(new Error(errorMessage)); } }); proc.on('error', (err) => { console.error('Process error:', err); - reject(err); // Reject with the process error + reject(err); }); }); + // Fallback check if venv was partially created if (!creationSuccess) { try { await access(pythonExecutable); @@ -108,11 +120,12 @@ except Exception as e: } } } finally { + // Clean up temporary file await unlink(tempFile).catch(() => {}); } } - // 3. Verify the venv Python exists + // 3. Final verification of virtual environment Python await access(pythonExecutable); return pythonExecutable; } catch (error: any) { @@ -121,6 +134,13 @@ except Exception as e: } } +/** + * Verifies installed package version matches expected version + * @param pythonPath Path to Python executable + * @param config Installation configuration + * @returns true if version matches + * @throws Error if version mismatch or package not found + */ async function verifyPyPackage( pythonPath: string, config: InstallConfig, @@ -140,10 +160,13 @@ async function verifyPyPackage( } console.log('Version match.'); - return true; } +/** + * Installs package from PyPI into virtual environment + * @param config Installation configuration + */ async function installFromPyPI(config: InstallConfig): Promise { let pythonPath: string; try { @@ -155,11 +178,13 @@ async function installFromPyPI(config: InstallConfig): Promise { } const pipPath = pythonPath.replace('python', 'pip'); + // Skip if already installed if (await verifyPyPackage(pythonPath, config)) { console.log('Package already installed.'); return; } + // Update setuptools first console.log('Installing setup tools...'); try { childProcess.execSync(`"${pipPath}" install --upgrade "setuptools>=45.0.0"`, { @@ -169,6 +194,7 @@ async function installFromPyPI(config: InstallConfig): Promise { console.warn('Could not update setuptools:', error); } + // Main package installation console.log('Installing ecooptimizer...'); try { childProcess.execSync( @@ -184,16 +210,21 @@ async function installFromPyPI(config: InstallConfig): Promise { } } +/** + * Finds a valid Python executable path + * @returns Path to Python executable + * @throws Error if no valid Python found + */ async function findPythonPath(): Promise { - // 1. Check explicitly set environment variable + // Check explicit environment variable first if (process.env.PYTHON_PATH && (await validatePython(process.env.PYTHON_PATH))) { return process.env.PYTHON_PATH; } - // 2. Common Python executable names (ordered by preference) + // Common Python executable names (ordered by preference) const candidates = ['python', 'python3.10', 'python3', 'py']; - // 3. Platform-specific locations + // Platform-specific locations if (process.platform === 'win32') { candidates.push( path.join( @@ -211,7 +242,7 @@ async function findPythonPath(): Promise { candidates.push('/usr/local/bin/python3'); // Homebrew default } - // Check common Python management tools + // Check environment-specific paths if (process.env.CONDA_PREFIX) { candidates.push(path.join(process.env.CONDA_PREFIX, 'bin', 'python')); } @@ -220,7 +251,7 @@ async function findPythonPath(): Promise { candidates.push(path.join(process.env.VIRTUAL_ENV, 'bin', 'python')); } - // 4. Test each candidate + // Test each candidate for (const candidate of candidates) { try { if (await validatePython(candidate)) { @@ -234,15 +265,18 @@ async function findPythonPath(): Promise { throw new Error('No valid Python installation found'); } +/** + * Validates Python executable meets requirements + * @param pythonPath Path to Python executable + * @returns true if valid Python 3.9+ installation + */ async function validatePython(pythonPath: string): Promise { try { - // Check Python exists and has required version (3.9+) const versionOutput = childProcess .execSync(`"${pythonPath}" --version`) .toString() .trim(); - // Parse version (e.g., "Python 3.10.6") const versionMatch = versionOutput.match(/Python (\d+)\.(\d+)/); if (!versionMatch) return false; @@ -250,14 +284,13 @@ async function validatePython(pythonPath: string): Promise { const minor = parseInt(versionMatch[2]); console.log('Python version:', major, minor); - return major === 3 && minor >= 9; // Require Python 3.9+ } catch { return false; } } -// Updated main execution block +// Main execution block when run directly if (require.main === module) { (async (): Promise => { try { diff --git a/src/lib/README.md b/src/lib/README.md new file mode 100644 index 0000000..94ac452 --- /dev/null +++ b/src/lib/README.md @@ -0,0 +1,3 @@ +The files in this folder do not currently do anything in the extension. + +They are left here for the future when the server backend can be properly integrated into the extension. \ No newline at end of file diff --git a/src/lib/dependencyManager.ts b/src/lib/dependencyManager.ts index 866ff16..a388d6e 100644 --- a/src/lib/dependencyManager.ts +++ b/src/lib/dependencyManager.ts @@ -3,13 +3,27 @@ import { join } from 'path'; import * as vscode from 'vscode'; import childProcess from 'child_process'; +/** + * Handles Python dependency management for the extension. + * Creates and manages a virtual environment (.venv) in the extension directory + * and provides installation capabilities when dependencies are missing. + */ export class DependencyManager { + /** + * Ensures required dependencies are installed. Checks for existing virtual environment + * and prompts user to install if missing. + * + * @param context - Extension context containing installation path + * @returns Promise resolving to true if dependencies are available, false otherwise + */ static async ensureDependencies( context: vscode.ExtensionContext, ): Promise { + // Check for existing virtual environment const venvPath = join(context.extensionPath, '.venv'); if (existsSync(venvPath)) return true; + // Prompt user to install dependencies if venv doesn't exist const choice = await vscode.window.showErrorMessage( 'Python dependencies missing. Install now?', 'Install', @@ -36,14 +50,23 @@ export class DependencyManager { return false; } + /** + * Executes the dependency installation process in a child process. + * Shows progress to user and handles installation errors. + * + * @param context - Extension context containing installation path + * @throws Error when installation process fails + */ private static async runInstaller( context: vscode.ExtensionContext, ): Promise { return new Promise((resolve, reject) => { + // Spawn installer process with inherited stdio for live output const installer = childProcess.spawn('node', ['dist/install.js'], { cwd: context.extensionPath, - stdio: 'inherit', + stdio: 'inherit', // Show installation progress in parent console }); + installer.on('close', (code) => code === 0 ? resolve() diff --git a/src/lib/processManager.ts b/src/lib/processManager.ts index 7cc41cf..37f66eb 100644 --- a/src/lib/processManager.ts +++ b/src/lib/processManager.ts @@ -5,12 +5,25 @@ import { join } from 'path'; import * as vscode from 'vscode'; import { ecoOutput } from '../extension'; +/** + * Manages the lifecycle of the backend server process, including: + * - Starting the Python server with proper environment + * - Port allocation and verification + * - Process cleanup on exit + * - Logging and error handling + */ export class ServerProcess { private process?: childProcess.ChildProcess; constructor(private context: vscode.ExtensionContext) {} + /** + * Starts the backend server process and verifies it's ready. + * @returns Promise resolving to the port number the server is running on + * @throws Error if server fails to start or Python environment is missing + */ async start(): Promise { + // Determine Python executable path based on platform const pythonPath = join( this.context.extensionPath, process.platform === 'win32' @@ -22,29 +35,39 @@ export class ServerProcess { throw new Error('Python environment not found'); } - await this.killProcessTree(); // Cleanup any existing process + // Clean up any existing server process + await this.killProcessTree(); + // Find and bind to an available port const port = await this.findFreePort(); + + // Start the Python server process this.process = childProcess.spawn( pythonPath, ['-m', 'ecooptimizer.api', '--port', port.toString(), '--dev'], { cwd: this.context.extensionPath, - env: { ...process.env, PYTHONUNBUFFERED: '1' }, + env: { ...process.env, PYTHONUNBUFFERED: '1' }, // Ensure unbuffered output }, ); + // Set up process event handlers this.process.stdout?.on('data', (data) => ecoOutput.info(`[Server] ${data}`)); this.process.stderr?.on('data', (data) => ecoOutput.error(`[Server] ${data}`)); this.process.on('close', () => { - ecoOutput.info('Server off.'); - console.log('Server off.'); + ecoOutput.info('Server stopped'); + console.log('Server stopped'); }); + // Verify server is actually listening before returning await this.verifyReady(port); return port; } + /** + * Finds an available network port + * @returns Promise resolving to an available port number + */ private async findFreePort(): Promise { return new Promise((resolve) => { const server = net.createServer(); @@ -55,22 +78,34 @@ export class ServerProcess { }); } + /** + * Kills the server process and its entire process tree + * Handles platform-specific process termination + */ private async killProcessTree(): Promise { if (!this.process?.pid) return; try { if (process.platform === 'win32') { + // Windows requires taskkill for process tree termination childProcess.execSync(`taskkill /PID ${this.process.pid} /T /F`); } else { - process.kill(-this.process.pid, 'SIGKILL'); // Negative PID kills group + // Unix systems can kill process groups with negative PID + process.kill(-this.process.pid, 'SIGKILL'); } } catch (error) { - ecoOutput.error(`Cleanup failed: ${error}`); + ecoOutput.error(`Process cleanup failed: ${error}`); } finally { this.process = undefined; } } + /** + * Verifies the server is actually listening on the specified port + * @param port Port number to check + * @param timeout Maximum wait time in milliseconds + * @throws Error if server doesn't become ready within timeout + */ private async verifyReady(port: number, timeout = 10000): Promise { const start = Date.now(); while (Date.now() - start < timeout) { @@ -83,12 +118,16 @@ export class ServerProcess { socket.end(); return; } catch { + // Retry after short delay if connection fails await new Promise((resolve) => setTimeout(resolve, 200)); } } throw new Error(`Server didn't start within ${timeout}ms`); } + /** + * Clean up resources when disposing of the manager + */ dispose(): void { this.process?.kill(); } diff --git a/src/providers/FilterViewProvider.ts b/src/providers/FilterViewProvider.ts index 55dc968..cab9a17 100644 --- a/src/providers/FilterViewProvider.ts +++ b/src/providers/FilterViewProvider.ts @@ -10,9 +10,11 @@ import { SmellsCacheManager } from '../context/SmellsCacheManager'; import { SmellsViewProvider } from './SmellsViewProvider'; /** - * Provides a tree view for filtering code smells within the VS Code extension. + * Provides a tree view for managing and filtering code smells in the VS Code extension. + * Handles smell configuration, option editing, and maintains consistency with cached results. */ export class FilterViewProvider implements vscode.TreeDataProvider { + // Event emitter for tree view updates private _onDidChangeTreeData: vscode.EventEmitter< vscode.TreeItem | undefined | void > = new vscode.EventEmitter(); @@ -31,6 +33,10 @@ export class FilterViewProvider implements vscode.TreeDataProvider): void { this.treeView = treeView; @@ -41,6 +47,7 @@ export class FilterViewProvider implements vscode.TreeDataProvider { if (!element) { + // Root level items - all available smells return Promise.resolve( Object.keys(this.smells) .sort((a, b) => this.smells[a].name.localeCompare(this.smells[b].name)) @@ -72,6 +85,7 @@ export class FilterViewProvider implements vscode.TreeDataProvider { if (this.smells[smellKey]) { this.smells[smellKey].enabled = !this.smells[smellKey].enabled; @@ -98,6 +116,12 @@ export class FilterViewProvider implements vscode.TreeDataProvider { const confirmed = await this.confirmFilterChange(); if (!confirmed) return; @@ -134,20 +162,24 @@ export class FilterViewProvider implements vscode.TreeDataProvider { const confirmed = await this.confirmFilterChange(); if (!confirmed) return; loadSmells('default'); - - const defaultSmells = getFilterSmells(); - this.smells = defaultSmells; + this.smells = getFilterSmells(); saveSmells(this.smells); await this.invalidateCachedSmellsForAffectedFiles(); this._onDidChangeTreeData.fire(); } + /** + * Invalidates cached smells for all files when filters change + */ async invalidateCachedSmellsForAffectedFiles(): Promise { const cachedFilePaths = this.smellsCacheManager.getAllFilePaths(); @@ -160,6 +192,10 @@ export class FilterViewProvider implements vscode.TreeDataProvider { const suppressWarning = this.context.workspaceState.get( 'ecooptimizer.suppressFilterWarning', @@ -167,7 +203,7 @@ export class FilterViewProvider implements vscode.TreeDataProvider; // Map @@ -65,15 +77,24 @@ interface FolderMetrics { }; } +/** + * Provides a tree view of carbon savings metrics across the workspace + * Aggregates data by folder structure and smell types with caching for performance + */ export class MetricsViewProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData = new vscode.EventEmitter< MetricTreeItem | undefined >(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + // Cache for folder metrics to avoid repeated calculations private folderMetricsCache: Map = new Map(); constructor(private context: vscode.ExtensionContext) {} + /** + * Triggers a refresh of the tree view + */ refresh(): void { this._onDidChangeTreeData.fire(undefined); } @@ -82,17 +103,24 @@ export class MetricsViewProvider implements vscode.TreeDataProvider { const metricsData = this.context.workspaceState.get<{ [path: string]: MetricsDataItem; }>(envConfig.WORKSPACE_METRICS_DATA!, {}); + // Root level items if (!element) { const configuredPath = this.context.workspaceState.get( envConfig.WORKSPACE_CONFIGURED_PATH!, ); if (!configuredPath) return []; + // Show either single file or folder contents at root const isDirectory = fs.existsSync(configuredPath) && fs.statSync(configuredPath).isDirectory(); if (isDirectory) { @@ -102,6 +130,7 @@ export class MetricsViewProvider implements vscode.TreeDataProvider { return node.isFile ? this.createFileItem(node.fullPath, metricsData) @@ -131,6 +162,7 @@ export class MetricsViewProvider implements vscode.TreeDataProvider { - // Check if we have cached metrics for this folder + // Return cached metrics if available const cachedMetrics = this.folderMetricsCache.get(folderPath); if (cachedMetrics) { return cachedMetrics; @@ -198,10 +243,12 @@ export class MetricsViewProvider implements vscode.TreeDataProvider { if (smellConfigData[symbol]) { @@ -276,6 +329,9 @@ export class MetricsViewProvider implements vscode.TreeDataProvider( envConfig.WORKSPACE_CONFIGURED_PATH!, @@ -315,9 +377,8 @@ export class MetricsViewProvider implements vscode.TreeDataProvider { + // Event emitter for tree view updates private _onDidChangeTreeData: vscode.EventEmitter< TreeItem | SmellTreeItem | undefined | void > = new vscode.EventEmitter(); @@ -16,30 +22,51 @@ export class SmellsViewProvider TreeItem | SmellTreeItem | undefined | void > = this._onDidChangeTreeData.event; + // Tracks analysis status and smells for each file private fileStatuses: Map = new Map(); private fileSmells: Map = new Map(); constructor(private context: vscode.ExtensionContext) {} + /** + * Triggers a refresh of the tree view + */ refresh(): void { this._onDidChangeTreeData.fire(); } + /** + * Updates the analysis status for a file + * @param filePath Path to the file + * @param status New status ('queued', 'passed', 'failed', etc.) + */ setStatus(filePath: string, status: string): void { - this.fileStatuses.set(normalizePath(filePath), status); + const normalizedPath = normalizePath(filePath); + this.fileStatuses.set(normalizedPath, status); + // Clear smells if status is outdated if (status === 'outdated') { - this.fileSmells.delete(filePath); + this.fileSmells.delete(normalizedPath); } this._onDidChangeTreeData.fire(); } + /** + * Sets the detected smells for a file + * @param filePath Path to the file + * @param smells Array of detected smells + */ setSmells(filePath: string, smells: Smell[]): void { this.fileSmells.set(filePath, smells); this._onDidChangeTreeData.fire(); } + /** + * Removes a file from the tree view + * @param filePath Path to the file to remove + * @returns Whether the file was found and removed + */ public removeFile(filePath: string): boolean { const normalizedPath = normalizePath(filePath); const exists = this.fileStatuses.has(normalizedPath); @@ -50,6 +77,9 @@ export class SmellsViewProvider return exists; } + /** + * Clears all file statuses and smells from the view + */ public clearAllStatuses(): void { this.fileStatuses.clear(); this.fileSmells.clear(); @@ -60,6 +90,11 @@ export class SmellsViewProvider return element; } + /** + * Builds the tree view hierarchy + * @param element The parent element or undefined for root items + * @returns Promise resolving to child tree items + */ async getChildren( element?: TreeItem | SmellTreeItem, ): Promise<(TreeItem | SmellTreeItem)[]> { @@ -70,12 +105,12 @@ export class SmellsViewProvider return []; } - // Smell nodes never have children + // Smell nodes are leaf nodes - no children if (element instanceof SmellTreeItem) { return []; } - // If file node, show smells + // If this is a file node, show its smells if ( element?.contextValue === 'file' || element?.contextValue === 'file_with_smells' @@ -84,27 +119,32 @@ export class SmellsViewProvider return smells.map((smell) => new SmellTreeItem(smell)); } - // If root element (first load) + // Root element - show either single file or folder contents if (!element) { const stat = fs.statSync(rootPath); if (stat.isFile()) { return [this.createTreeItem(rootPath, true)]; } else if (stat.isDirectory()) { - return [this.createTreeItem(rootPath, false)]; // 👈 Show the root folder as the top node + return [this.createTreeItem(rootPath, false)]; // Show root folder as top node } } - // Folder node – get its children + // Folder node - build its contents const currentPath = element?.resourceUri?.fsPath; if (!currentPath) return []; const childNodes = buildPythonTree(currentPath); - return childNodes.map(({ fullPath, isFile }) => this.createTreeItem(fullPath, isFile), ); } + /** + * Creates a tree item for a file or folder + * @param filePath Path to the file/folder + * @param isFile Whether this is a file (false for folders) + * @returns Configured TreeItem instance + */ private createTreeItem(filePath: string, isFile: boolean): TreeItem { const label = path.basename(filePath); const status = @@ -112,6 +152,9 @@ export class SmellsViewProvider const icon = isFile ? getStatusIcon(status) : new vscode.ThemeIcon('folder'); const tooltip = isFile ? getStatusMessage(status) : undefined; + // Determine collapsible state: + // - Folders are always collapsible + // - Files are collapsible only if they have smells const collapsibleState = isFile ? this.fileSmells.has(filePath) && this.fileSmells.get(filePath)!.length > 0 ? vscode.TreeItemCollapsibleState.Collapsed @@ -123,7 +166,7 @@ export class SmellsViewProvider item.iconPath = icon; item.tooltip = tooltip; - // Override contextValue if file has smells + // Mark files with smells with special context if ( isFile && this.fileSmells.has(filePath) && @@ -132,6 +175,7 @@ export class SmellsViewProvider item.contextValue = 'file_with_smells'; } + // Show outdated status in description if (status === 'outdated') { item.description = 'outdated'; } @@ -140,6 +184,9 @@ export class SmellsViewProvider } } +/** + * Tree item representing a file or folder in the smells view + */ export class TreeItem extends vscode.TreeItem { constructor( label: string, @@ -151,6 +198,7 @@ export class TreeItem extends vscode.TreeItem { this.resourceUri = vscode.Uri.file(fullPath); this.contextValue = contextValue; + // Make files clickable to open them if (contextValue === 'file' || contextValue === 'file_with_smells') { this.command = { title: 'Open File', @@ -161,8 +209,12 @@ export class TreeItem extends vscode.TreeItem { } } +/** + * Tree item representing a detected code smell + */ export class SmellTreeItem extends vscode.TreeItem { constructor(public readonly smell: Smell) { + // Format label with acronym and line numbers const acronym = getAcronymByMessageId(smell.messageId) ?? smell.messageId; const lines = smell.occurences ?.map((occ) => occ.line) @@ -177,6 +229,7 @@ export class SmellTreeItem extends vscode.TreeItem { this.contextValue = 'smell'; this.iconPath = new vscode.ThemeIcon('snake'); + // Set up command to jump to the first occurrence const firstLine = smell.occurences?.[0]?.line; if (smell.path && typeof firstLine === 'number') { this.command = { @@ -188,6 +241,11 @@ export class SmellTreeItem extends vscode.TreeItem { } } +/** + * Gets the appropriate icon for a file's analysis status + * @param status Analysis status string + * @returns ThemeIcon with appropriate icon and color + */ export function getStatusIcon(status: string): vscode.ThemeIcon { switch (status) { case 'queued': @@ -218,9 +276,9 @@ export function getStatusIcon(status: string): vscode.ThemeIcon { } /** - * Retrieves the status message corresponding to the smell analysis state. - * @param status - The analysis status. - * @returns A descriptive status message. + * Gets a human-readable message for an analysis status + * @param status Analysis status string + * @returns Descriptive status message */ export function getStatusMessage(status: string): string { switch (status) { diff --git a/src/ui/hoverManager.ts b/src/ui/hoverManager.ts index 271739e..7e8e0af 100644 --- a/src/ui/hoverManager.ts +++ b/src/ui/hoverManager.ts @@ -2,22 +2,30 @@ import * as vscode from 'vscode'; import { SmellsCacheManager } from '../context/SmellsCacheManager'; /** - * Displays smell information on hover when hovering over lines in Python files. + * Provides hover information for detected code smells in Python files. + * Shows smell details and quick actions when hovering over affected lines. */ export class HoverManager implements vscode.HoverProvider { constructor(private smellsCacheManager: SmellsCacheManager) {} /** - * Registers the hover provider for Python files. + * Registers the hover provider with VS Code + * @param context The extension context for managing disposables */ public register(context: vscode.ExtensionContext): void { - const selector: vscode.DocumentSelector = { language: 'python', scheme: 'file' }; + const selector: vscode.DocumentSelector = { + language: 'python', + scheme: 'file', // Only show for local files, not untitled documents + }; const disposable = vscode.languages.registerHoverProvider(selector, this); context.subscriptions.push(disposable); } /** - * Provides hover content with stacked smell info. + * Generates hover content when hovering over lines with detected smells + * @param document The active text document + * @param position The cursor position where hover was triggered + * @returns Hover content or undefined if no smells found */ public provideHover( document: vscode.TextDocument, @@ -31,26 +39,34 @@ export class HoverManager implements vscode.HoverProvider { const smells = this.smellsCacheManager.getCachedSmells(filePath); if (!smells || smells.length === 0) return; + // Convert VS Code position to 1-based line number const lineNumber = position.line + 1; + // Find smells that occur on this line const smellsAtLine = smells.filter((smell) => smell.occurences.some((occ) => occ.line === lineNumber), ); if (smellsAtLine.length === 0) return; + // Helper to escape markdown special characters const escape = (text: string): string => { return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); }; + // Build markdown content with smell info and actions const markdown = new vscode.MarkdownString(); - markdown.isTrusted = true; + markdown.isTrusted = true; // Allow command URIs markdown.supportHtml = true; markdown.supportThemeIcons = true; + // Add each smell's info and actions smellsAtLine.forEach((smell) => { + // Basic smell info const messageLine = `${escape(smell.message)} (**${escape(smell.messageId)}**)`; - const divider = '\n\n---\n\n'; + const divider = '\n\n---\n\n'; // Visual separator + + // Command URIs for quick actions const refactorSmellCmd = `command:ecooptimizer.refactorSmell?${encodeURIComponent(JSON.stringify(smell))} "Fix this specific smell"`; const refactorTypeCmd = `command:ecooptimizer.refactorAllSmellsOfType?${encodeURIComponent( JSON.stringify({ @@ -59,6 +75,7 @@ export class HoverManager implements vscode.HoverProvider { }), )} "Fix all similar smells"`; + // Build the hover content markdown.appendMarkdown(messageLine); markdown.appendMarkdown(divider); markdown.appendMarkdown(`[$(tools) Refactor Smell](${refactorSmellCmd}) | `); diff --git a/src/utils/initializeStatusesFromCache.ts b/src/utils/initializeStatusesFromCache.ts index a786fb6..27beaeb 100644 --- a/src/utils/initializeStatusesFromCache.ts +++ b/src/utils/initializeStatusesFromCache.ts @@ -8,7 +8,10 @@ import { envConfig } from './envConfig'; /** * Initializes file statuses and smells in the SmellsViewProvider from the smell cache. - * Also validates that cached files are part of the current workspace. + + * @param context The extension context containing workspace configuration + * @param smellsCacheManager The cache manager instance + * @param smellsViewProvider The view provider to update with cached data */ export async function initializeStatusesFromCache( context: vscode.ExtensionContext, @@ -16,6 +19,8 @@ export async function initializeStatusesFromCache( smellsViewProvider: SmellsViewProvider, ): Promise { ecoOutput.info('workspace key: ', envConfig.WORKSPACE_CONFIGURED_PATH); + + // Get configured workspace path from extension state let configuredPath = context.workspaceState.get( envConfig.WORKSPACE_CONFIGURED_PATH!, ); @@ -28,23 +33,25 @@ export async function initializeStatusesFromCache( } configuredPath = normalizePath(configuredPath); - ecoOutput.info( `[CacheInit] Starting cache initialization for workspace: ${configuredPath}`, ); + // Get all cached file paths and initialize counters const pathMap = smellsCacheManager.getAllFilePaths(); ecoOutput.trace(`[CacheInit] Found ${pathMap.length} files in cache`); ecoOutput.trace(`[CacheInit] Found ${pathMap} files in cache`); + let validFiles = 0; let removedFiles = 0; let filesWithSmells = 0; let cleanFiles = 0; + // Process each cached file for (const filePath of pathMap) { ecoOutput.trace(`[CacheInit] Processing cache entry: ${filePath}`); - // Ignore files outside the configured workspace + // Skip files outside the current workspace if (!filePath.startsWith(configuredPath)) { ecoOutput.trace( `[CacheInit] File outside workspace - removing from cache: ${filePath}`, @@ -54,7 +61,7 @@ export async function initializeStatusesFromCache( continue; } - // Verify file still exists + // Verify file exists on disk try { await fs.access(filePath); ecoOutput.trace(`[CacheInit] File verified: ${filePath}`); @@ -67,10 +74,12 @@ export async function initializeStatusesFromCache( continue; } + // Get cached smells for valid files const smells = smellsCacheManager.getCachedSmells(filePath); if (smells !== undefined) { validFiles++; + // Update view provider based on smell data if (smells.length > 0) { ecoOutput.trace( `[CacheInit] Found ${smells.length} smells for file: ${filePath}`, @@ -90,7 +99,7 @@ export async function initializeStatusesFromCache( } } - // Summary statistics + // Log summary statistics ecoOutput.info( `[CacheInit] Cache initialization complete. ` + `Results: ${validFiles} valid files (${filesWithSmells} with smells, ${cleanFiles} clean), ` + From 27f74c5bb4aabffd04138d40900fcbf94147f9db Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:28:10 -0400 Subject: [PATCH 110/121] updated configs and dependencies --- package-lock.json | 115 ---------------------------------------------- package.json | 7 ++- tsconfig.json | 2 +- 3 files changed, 4 insertions(+), 120 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f966e3..8dfc29b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,15 +7,11 @@ "": { "name": "ecooptimizer", "version": "0.2.2", - "hasInstallScript": true, "dependencies": { "@types/dotenv": "^6.1.1", - "adm-zip": "^0.5.16", "bufferutil": "^4.0.9", "dotenv": "^16.4.7", "dotenv-webpack": "^8.1.0", - "follow-redirects": "^1.15.9", - "node-fetch": "^3.3.2", "utf-8-validate": "^6.0.5", "ws": "^8.18.0" }, @@ -2369,14 +2365,6 @@ "node": ">=0.4.0" } }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "engines": { - "node": ">=12.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -3337,14 +3325,6 @@ "node": ">=4" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -4055,28 +4035,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4172,25 +4130,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4208,17 +4147,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6443,41 +6371,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -8420,14 +8313,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webpack": { "version": "5.97.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", diff --git a/package.json b/package.json index ce726b9..1c7e04c 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,9 @@ "!src/providers/*", "!src/commands/showLogs.ts", "!src/emitters/serverStatus.ts", - "!src/utils/envConfig.ts" + "!src/utils/envConfig.ts", + "!src/lib/*", + "!src/install.ts" ] }, "lint-staged": { @@ -119,12 +121,9 @@ }, "dependencies": { "@types/dotenv": "^6.1.1", - "adm-zip": "^0.5.16", "bufferutil": "^4.0.9", "dotenv": "^16.4.7", "dotenv-webpack": "^8.1.0", - "follow-redirects": "^1.15.9", - "node-fetch": "^3.3.2", "utf-8-validate": "^6.0.5", "ws": "^8.18.0" }, diff --git a/tsconfig.json b/tsconfig.json index 5250b08..da4d5ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "forceConsistentCasingInFileNames": true, "allowSyntheticDefaultImports": true }, - "include": ["./src/global.d.ts", "./src/types.d.ts", "src/**/*.ts", "test/**/*.ts"], + "include": ["./src/global.d.ts", "src/**/*.ts", "test/**/*.ts"], "exclude": ["node_modules", "dist"] } \ No newline at end of file From 90db46e47bf25f86ec57ec5aa4fdf6ff805f9183 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:47:54 -0400 Subject: [PATCH 111/121] remove virtual folders property from tsconfig.json --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index da4d5ef..56c7ffd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "DOM.Iterable" ], "sourceMap": true, - "rootDirs": ["./src", "./test"], "outDir": "dist", "strict": true, "typeRoots": ["./node_modules/@types", "./types"], From e2751ad34acbd7b44189641a55844326040b1dfb Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:00:54 -0400 Subject: [PATCH 112/121] Changed workflow testing step to do clean install --- .github/workflows/jest-tests.yaml | 6 ++++-- tsconfig.json | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/jest-tests.yaml b/.github/workflows/jest-tests.yaml index 60c8bc3..9e7a61d 100644 --- a/.github/workflows/jest-tests.yaml +++ b/.github/workflows/jest-tests.yaml @@ -18,8 +18,10 @@ jobs: with: node-version: 20 - - name: Install dependencies - run: npm install + - name: Clean install + run: | + rm -rf node_modules + npm ci - name: Run Jest tests run: npm test -- --coverage diff --git a/tsconfig.json b/tsconfig.json index 56c7ffd..da4d5ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "DOM.Iterable" ], "sourceMap": true, + "rootDirs": ["./src", "./test"], "outDir": "dist", "strict": true, "typeRoots": ["./node_modules/@types", "./types"], From ed9a6f48c9c9f7b37ef8c56ce940f98c484c0db4 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:28:23 -0400 Subject: [PATCH 113/121] updated test workflow --- .github/workflows/jest-tests.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/jest-tests.yaml b/.github/workflows/jest-tests.yaml index 9e7a61d..ea5f047 100644 --- a/.github/workflows/jest-tests.yaml +++ b/.github/workflows/jest-tests.yaml @@ -17,6 +17,8 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 + cache: npm + cache-dependency-path: package-lock.json - name: Clean install run: | From 5dc3955fcbb6f613a5e01db9f5a2bc793d8623ad Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:31:54 -0400 Subject: [PATCH 114/121] added temp purge cache step to test workflow --- .github/workflows/jest-tests.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/jest-tests.yaml b/.github/workflows/jest-tests.yaml index ea5f047..af9fbba 100644 --- a/.github/workflows/jest-tests.yaml +++ b/.github/workflows/jest-tests.yaml @@ -20,6 +20,12 @@ jobs: cache: npm cache-dependency-path: package-lock.json + - name: Purge all caches + run: | + npm cache clean --force + rm -rf node_modules package-lock.json + rm -rf ~/.npm ~/.yarn + - name: Clean install run: | rm -rf node_modules From c8d1d6aaf1e7e90a54c0f7e057ed314b134d1bc3 Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:33:17 -0400 Subject: [PATCH 115/121] updated test workflow --- .github/workflows/jest-tests.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/jest-tests.yaml b/.github/workflows/jest-tests.yaml index af9fbba..757ecf0 100644 --- a/.github/workflows/jest-tests.yaml +++ b/.github/workflows/jest-tests.yaml @@ -23,12 +23,11 @@ jobs: - name: Purge all caches run: | npm cache clean --force - rm -rf node_modules package-lock.json + rm -rf node_modules rm -rf ~/.npm ~/.yarn - name: Clean install run: | - rm -rf node_modules npm ci - name: Run Jest tests From 3ef6e42dda9515efdc9bc7b3c1a97ae60c3cd92f Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:07:58 -0400 Subject: [PATCH 116/121] modified jest config --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c7e04c..fa5061d 100644 --- a/package.json +++ b/package.json @@ -47,10 +47,12 @@ "./test/setup.ts" ], "moduleNameMapper": { - "^vscode$": "/test/mocks/vscode-mock.ts" + "^vscode$": "/test/mocks/vscode-mock.ts", + "^@/(.*)$": "/src/$1" }, "moduleDirectories": [ "node_modules", + "src", "tests/__mocks__" ], "roots": [ From 55eeb78e3f29bd0ade73a79a0ea07c6aa8453cff Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:34:51 -0400 Subject: [PATCH 117/121] fixed module import issue --- .github/workflows/jest-tests.yaml | 8 -------- package.json | 2 +- src/ui/{FileHighlighter.ts => fileHighlighter.ts} | 0 3 files changed, 1 insertion(+), 9 deletions(-) rename src/ui/{FileHighlighter.ts => fileHighlighter.ts} (100%) diff --git a/.github/workflows/jest-tests.yaml b/.github/workflows/jest-tests.yaml index 757ecf0..c71a146 100644 --- a/.github/workflows/jest-tests.yaml +++ b/.github/workflows/jest-tests.yaml @@ -17,14 +17,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - cache: npm - cache-dependency-path: package-lock.json - - - name: Purge all caches - run: | - npm cache clean --force - rm -rf node_modules - rm -rf ~/.npm ~/.yarn - name: Clean install run: | diff --git a/package.json b/package.json index fa5061d..e490eae 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "moduleDirectories": [ "node_modules", "src", - "tests/__mocks__" + "test/__mocks__" ], "roots": [ "/src", diff --git a/src/ui/FileHighlighter.ts b/src/ui/fileHighlighter.ts similarity index 100% rename from src/ui/FileHighlighter.ts rename to src/ui/fileHighlighter.ts From 0ad077d366ef6f5d0994a06b31d211cb7b079abf Mon Sep 17 00:00:00 2001 From: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:25:09 -0400 Subject: [PATCH 118/121] Updated publishing workflow + updated version commit 9f61596224988c21432fa12123edcd2857a429e9 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 19:23:25 2025 -0400 found it commit 27525517553312d69deb0cea9b94682a8047e0ef Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 19:21:46 2025 -0400 added logging commit d9518e6908df6f6b2390f47c9e06e7a6095e7e93 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 19:18:34 2025 -0400 try another fix commit f4a59caf08d6e11cc542b690694be3dee33ac6c4 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 19:16:55 2025 -0400 try a fix commit 7802389e76d7b19c8c1e7681d8e7eebe2b4038d7 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 19:10:17 2025 -0400 fix typo commit 6addc6a26612b03d4d9665551d11cc3a91248cf1 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 19:08:57 2025 -0400 fix workflow logic commit d5f0b0be398b9a8a8f975dfebe933ef9655bd202 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 18:58:30 2025 -0400 Created final publish workflow + updated version commit 296cf33482cb40fc67ac0b186ff3592a711a530c Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 18:50:23 2025 -0400 Updated release step in publishing workflow commit 8272d07fe15d30f15deb1b3aca42af1a49f9d842 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 18:40:57 2025 -0400 updated workflow logic commit f8e3a857c3249893d5fa0e55bb8f1b43c0f020a9 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 18:11:13 2025 -0400 trying one more thing commit 92178684f9e9deb9dd469d5c41132cdc33f424cb Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 18:09:46 2025 -0400 fix typo commit 7edd7a9cc69d03412d5d59cb6c0aabc3c6c9d713 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 18:09:16 2025 -0400 trying something commit ca82e6728207021d1fa21a6bbb1f0ae7355cba33 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 17:53:51 2025 -0400 updated vsce verification step commit 660887382155cef720cdfa641be695b3b4e09e4c Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 17:48:32 2025 -0400 updated vsce install + trigger event commit 68a928a995019137a0b8e698a3e2816ed825df1b Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 17:38:12 2025 -0400 found silly bug commit 188608838e1d72f2aea4f93a1a1c1b4149f7e774 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 17:36:24 2025 -0400 update again commit f271594f345032b9c40beaa2ca04181f298145c7 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 17:33:44 2025 -0400 update publish.yml commit 40f4149c3925ebc2d25c8700aeb0745c300ec2d8 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 17:29:22 2025 -0400 updated publishing workflow commit 57a705f533d9f73ef1f3a5485047230d2367b1d8 Merge: 55eeb78 2e6f904 Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Mon Mar 31 17:25:13 2025 -0400 Merge branch 'main' of https://github.com/ssm-lab/capstone--sco-vs-code-plugin into test-publish-workflow commit 2e6f904904db1b5d46696be483c187edf29c869b Author: Sevhena Walker <83547364+Sevhena@users.noreply.github.com> Date: Fri Mar 28 09:21:36 2025 -0400 Create publish.yml --- .github/workflows/publish.yml | 58 +++++++++++++++++++++++++++ .github/workflows/version-check.yaml | 59 ++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/version-check.yaml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..611807c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,58 @@ +name: Publish Extension + +on: + push: + branches: [main] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write # Needed for tag/release creation + id-token: write # For OIDC auth + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history needed for tagging + + - name: Get version + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "tag_name=v$VERSION" >> $GITHUB_OUTPUT + + - name: Create and push tag + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git tag ${{ steps.version.outputs.tag_name }} + git push origin ${{ steps.version.outputs.tag_name }} + + - name: Install dependencies + run: | + npm install + npm install -g @vscode/vsce + + - name: Package Extension + run: | + mkdir -p dist + vsce package --out ./dist/extension-${{ steps.version.outputs.tag_name }}.vsix + + - name: Create Draft Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.version.outputs.tag_name }} + name: ${{ steps.version.outputs.tag_name }} + body: 'Release notes' + files: | + dist/extension-${{ steps.version.outputs.tag_name }}.vsix + draft: true + prerelease: false + + - name: Publish to Marketplace + run: | + vsce publish -p $VSCE_PAT + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml new file mode 100644 index 0000000..a92a9a4 --- /dev/null +++ b/.github/workflows/version-check.yaml @@ -0,0 +1,59 @@ +name: PR Version Check + +on: + pull_request: + branches: [main] + +jobs: + validate_version: + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 # Required for branch comparison + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: npm install compare-versions + + - name: Get PR version + id: pr_version + run: | + PR_VERSION=$(node -p "require('./package.json').version") + echo "pr_version=$PR_VERSION" >> $GITHUB_OUTPUT + + - name: Fetch main branch + run: git fetch origin main + + - name: Get main's version + id: main_version + run: | + MAIN_VERSION=$(git show origin/main:package.json | node -p "JSON.parse(require('fs').readFileSync(0)).version") + echo "main_version=$MAIN_VERSION" >> $GITHUB_OUTPUT + + - name: Validate version bump + run: | + # Write a temporary Node.js script for version comparison + cat > compare-versions.mjs << 'EOF' + import { compareVersions } from 'compare-versions'; + + const mainVersion = process.argv[2]; + const prVersion = process.argv[3]; + + console.log("Main version:", mainVersion) + console.log("PR version:", prVersion) + + if (compareVersions(prVersion, mainVersion) < 1) { + console.error(`::error::Version ${prVersion} must be greater than ${mainVersion}`); + process.exit(1); + } + EOF + + node compare-versions.mjs "${{ steps.main_version.outputs.main_version }}" "${{ steps.pr_version.outputs.pr_version }}" + + echo "✓ Version validated" diff --git a/package.json b/package.json index e490eae..773269b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "type": "git", "url": "https://github.com/ssm-lab/capstone--sco-vs-code-plugin" }, - "version": "0.2.2", + "version": "0.2.3", "engines": { "vscode": "^1.92.0" }, From d462b36f7679efab651b096a8ab1ac35ea7dfd5e Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Thu, 3 Apr 2025 04:57:41 -0400 Subject: [PATCH 119/121] Added configureWorkspace.test.ts --- test/commands/configureWorkspace.test.ts | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/commands/configureWorkspace.test.ts diff --git a/test/commands/configureWorkspace.test.ts b/test/commands/configureWorkspace.test.ts new file mode 100644 index 0000000..8693124 --- /dev/null +++ b/test/commands/configureWorkspace.test.ts @@ -0,0 +1,84 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { configureWorkspace } from '../../src/commands/configureWorkspace'; +import { envConfig } from '../../src/utils/envConfig'; + +jest.mock('fs'); +jest.mock('vscode', () => { + const original = jest.requireActual('vscode'); + return { + ...original, + workspace: { workspaceFolders: [] }, + window: { + showQuickPick: jest.fn(), + showInformationMessage: jest.fn(), + showErrorMessage: jest.fn(), + }, + commands: { + executeCommand: jest.fn(), + }, + }; +}); + +describe('configureWorkspace (Jest)', () => { + const mockContext = { + workspaceState: { + update: jest.fn(), + }, + } as unknown as vscode.ExtensionContext; + + beforeEach(() => { + jest.resetAllMocks(); + + // Mock a workspace folder + (vscode.workspace.workspaceFolders as any) = [ + { + uri: { fsPath: '/project' }, + }, + ]; + + // Mock fs behavior + (fs.readdirSync as jest.Mock).mockImplementation((dirPath: string) => { + if (dirPath === '/project') { + return ['main.py', 'subdir']; + } else if (dirPath === '/project/subdir') { + return ['__init__.py']; + } + return []; + }); + + (fs.statSync as jest.Mock).mockImplementation((filePath: string) => ({ + isDirectory: () => !filePath.endsWith('.py'), + })); + + // Mock quick pick + (vscode.window.showQuickPick as jest.Mock).mockResolvedValue({ + label: 'project', + description: '/project', + detail: 'Python content: main.py', + folderPath: '/project', + }); + + envConfig.WORKSPACE_CONFIGURED_PATH = 'myWorkspaceKey'; + }); + + it('should detect Python folders and configure the workspace', async () => { + await configureWorkspace(mockContext); + + expect(mockContext.workspaceState.update).toHaveBeenCalledWith( + 'myWorkspaceKey', + '/project', + ); + + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'workspaceState.workspaceConfigured', + true, + ); + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + 'Configured workspace: project', + ); + }); +}); From 732406148dc2f18249a0bf15d1fb7af2eeb190c9 Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Thu, 3 Apr 2025 04:59:20 -0400 Subject: [PATCH 120/121] Added resetConfiguration.test.ts --- test/commands/resetConfiguration.test.ts | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/commands/resetConfiguration.test.ts diff --git a/test/commands/resetConfiguration.test.ts b/test/commands/resetConfiguration.test.ts new file mode 100644 index 0000000..e7d08ad --- /dev/null +++ b/test/commands/resetConfiguration.test.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode'; +import { resetConfiguration } from '../../src/commands/resetConfiguration'; +import { envConfig } from '../../src/utils/envConfig'; + +jest.mock('vscode', () => { + const original = jest.requireActual('vscode'); + return { + ...original, + window: { + showWarningMessage: jest.fn(), + }, + commands: { + executeCommand: jest.fn(), + }, + }; +}); + +describe('resetConfiguration (Jest)', () => { + const mockContext = { + workspaceState: { + update: jest.fn(), + }, + } as unknown as vscode.ExtensionContext; + + beforeEach(() => { + jest.resetAllMocks(); + envConfig.WORKSPACE_CONFIGURED_PATH = 'myWorkspaceKey'; + }); + + it('should reset workspace configuration when confirmed', async () => { + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue('Reset'); + + const result = await resetConfiguration(mockContext); + + expect(mockContext.workspaceState.update).toHaveBeenCalledWith( + 'myWorkspaceKey', + undefined, + ); + + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'workspaceState.workspaceConfigured', + false, + ); + + expect(result).toBe(true); + }); + + it('should not reset workspace configuration if user cancels', async () => { + (vscode.window.showWarningMessage as jest.Mock).mockResolvedValue(undefined); + + const result = await resetConfiguration(mockContext); + + expect(mockContext.workspaceState.update).not.toHaveBeenCalled(); + expect(vscode.commands.executeCommand).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); +}); From d7104078013555c3b95cf774dfc24c6f8b9a5a8b Mon Sep 17 00:00:00 2001 From: Nivetha Kuruparan Date: Thu, 3 Apr 2025 05:13:25 -0400 Subject: [PATCH 121/121] Added refactorActionButtons.test.ts --- package.json | 3 + test/utils/refactorActionButtons.test.ts | 81 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 test/utils/refactorActionButtons.test.ts diff --git a/package.json b/package.json index 773269b..124eddb 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,9 @@ "!src/commands/showLogs.ts", "!src/emitters/serverStatus.ts", "!src/utils/envConfig.ts", + "!src/utils/TreeStructureBuilder.ts", + "!src/commands/views/jumpToSmell.ts", + "!src/commands/views/openFile.ts", "!src/lib/*", "!src/install.ts" ] diff --git a/test/utils/refactorActionButtons.test.ts b/test/utils/refactorActionButtons.test.ts new file mode 100644 index 0000000..1b64119 --- /dev/null +++ b/test/utils/refactorActionButtons.test.ts @@ -0,0 +1,81 @@ +import * as vscode from 'vscode'; +import { + initializeRefactorActionButtons, + showRefactorActionButtons, + hideRefactorActionButtons, +} from '../../src/utils/refactorActionButtons'; + +jest.mock('vscode', () => { + const original = jest.requireActual('vscode'); + return { + ...original, + StatusBarAlignment: { + Right: 2, // You can use the actual enum value or a string + }, + window: { + createStatusBarItem: jest.fn(), + }, + commands: { + executeCommand: jest.fn(), + }, + ThemeColor: jest.fn().mockImplementation((color) => color), + }; +}); + +jest.mock('../../src/extension', () => ({ + ecoOutput: { + trace: jest.fn(), + replace: jest.fn(), + }, +})); + +describe('Refactor Action Buttons', () => { + const acceptMock = { + show: jest.fn(), + hide: jest.fn(), + }; + const rejectMock = { + show: jest.fn(), + hide: jest.fn(), + }; + + const pushSpy = jest.fn(); + const mockContext = { + subscriptions: { push: pushSpy }, + } as unknown as vscode.ExtensionContext; + + beforeEach(() => { + jest.resetAllMocks(); + pushSpy.mockClear(); + + (vscode.window.createStatusBarItem as jest.Mock) + .mockImplementationOnce(() => acceptMock) + .mockImplementationOnce(() => rejectMock); + }); + + it('should show the buttons and set context when shown', () => { + initializeRefactorActionButtons(mockContext); + showRefactorActionButtons(); + + expect(acceptMock.show).toHaveBeenCalled(); + expect(rejectMock.show).toHaveBeenCalled(); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'refactoringInProgress', + true, + ); + }); + + it('should hide the buttons and clear context when hidden', () => { + initializeRefactorActionButtons(mockContext); + hideRefactorActionButtons(); + + expect(acceptMock.hide).toHaveBeenCalled(); + expect(rejectMock.hide).toHaveBeenCalled(); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'refactoringInProgress', + false, + ); + }); +});