From 527ad0ea41a379660bb00ca9bd794c272a53a416 Mon Sep 17 00:00:00 2001 From: Valentin Date: Fri, 7 Apr 2023 19:03:39 +0200 Subject: [PATCH 1/5] Prettify project --- .eslintrc.json | 38 +++++------ .vscode/extensions.json | 8 +-- .vscode/launch.json | 56 ++++++++--------- .vscode/settings.json | 19 +++--- .vscode/tasks.json | 32 +++++----- LICENSE.md | 2 +- README.md | 12 ++-- package.json | 38 ++++++----- src/extension.ts | 115 +++++++++++++++++++--------------- src/fileDecorationProvider.ts | 24 +++---- src/utils.ts | 9 +-- tsconfig.json | 28 ++++----- 12 files changed, 195 insertions(+), 186 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index f9b22b7..5dfecab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,24 +1,18 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/naming-convention": "warn", - "@typescript-eslint/semi": "warn", - "curly": "warn", - "eqeqeq": "warn", - "no-throw-literal": "warn", - "semi": "off" - }, - "ignorePatterns": [ - "out", - "dist", - "**/*.d.ts" - ] + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/naming-convention": "warn", + "@typescript-eslint/semi": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-throw-literal": "warn", + "semi": "off" + }, + "ignorePatterns": ["out", "dist", "**/*.d.ts"] } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3ac9aeb..c0a2258 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,5 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "dbaeumer.vscode-eslint" - ] + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["dbaeumer.vscode-eslint"] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 6e84338..53aeb05 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,34 +3,30 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--disable-extensions", - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "args": [ - "--disable-extensions", - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf8c2..e375ad4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,12 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" -} \ No newline at end of file + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + "editor.formatOnSave": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3b17e53..078ff7e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,20 +1,20 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - } - } - ] + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] } diff --git a/LICENSE.md b/LICENSE.md index 418db4a..16dd154 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 33bdbe6..aa56b8e 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ The extension allows you to mark/unmark files by: -* Providing a `scope.txt` file in the workspace root folder. The file contains a list of absolute file paths or relative file paths to the workspace root folder. The file paths must be separated by newlines. -* Right clicking on a file in the file explorer and selecting `Mark/Unmark File` from the context menu. -* Right clicking on an editor tab and selecting `Mark/Unmark File` from the context menu. +- Providing a `scope.txt` file in the workspace root folder. The file contains a list of absolute file paths or relative file paths to the workspace root folder. The file paths must be separated by newlines. +- Right clicking on a file in the file explorer and selecting `Mark/Unmark File` from the context menu. +- Right clicking on an editor tab and selecting `Mark/Unmark File` from the context menu. Note that the marked files are stored in a `scope.txt` file in the workspace root folder. If the file does not exist, it will be created. @@ -20,13 +20,12 @@ Note that the marked files are stored in a `scope.txt` file in the workspace roo ![Mark files from scope file](images/scope.gif) - ## Extension Settings This extension contributes the following settings: -* `markfiles.colorMarkedFile`: Choose whether to mark files by adding an icon, changing the color of the file name or both. -* `markfiles.markedFileIcon`: Choose the symbol to use for marked files. Note that the symbol must be a single unicode character. +- `markfiles.colorMarkedFile`: Choose whether to mark files by adding an icon, changing the color of the file name or both. +- `markfiles.markedFileIcon`: Choose the symbol to use for marked files. Note that the symbol must be a single unicode character. The color used to mark files can be changed by modifying the following key in your `settings.json` file: @@ -36,7 +35,6 @@ The color used to mark files can be changed by modifying the following key in yo } ``` - ## Release Notes Check [CHANGELOG.md](CHANGELOG.md) diff --git a/package.json b/package.json index f3307b2..e9804bb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "Other" ], "icon": "icon.png", - "activationEvents": ["onStartupFinished"], + "activationEvents": [ + "onStartupFinished" + ], "main": "./out/extension.js", "contributes": { "commands": [ @@ -30,17 +32,21 @@ "command": "markfiles.reloadFromScopeFile", "title": "Reload marked files from scope file" } - ], + ], "menus": { - "explorer/context": [{ - "command": "markfiles.markUnmarkSelectedFile", - "group": "Mark/Unmark file" - }], - "editor/title/context": [{ - "command": "markfiles.markUnmarkActiveFile", - "group": "Mark/Unmark file" - }] - }, + "explorer/context": [ + { + "command": "markfiles.markUnmarkSelectedFile", + "group": "Mark/Unmark file" + } + ], + "editor/title/context": [ + { + "command": "markfiles.markUnmarkActiveFile", + "group": "Mark/Unmark file" + } + ] + }, "colors": [ { "id": "markfiles.markedFileColor", @@ -58,7 +64,11 @@ "type": "string", "markdownDescription": "Choose whether to mark files by adding an icon, changing the color of the file name or both. Customize the color of the filename in the `#workbench.colorCustomizations#` section using the `markfiles.markedFileColor` key", "default": "icon", - "enum": ["icon", "color", "both"], + "enum": [ + "icon", + "color", + "both" + ], "enumDescriptions": [ "Mark files by adding an icon", "Mark files by changing the color of the file name", @@ -70,8 +80,8 @@ "description": "Choose the symbol to use for marked files (only unicode character)", "default": "📌", "maxLength": 2, - "minLength":1, - "pattern": "[^\\x00-\\x7F]+" + "minLength": 1, + "pattern": "[^\\x00-\\x7F]+" } } } diff --git a/src/extension.ts b/src/extension.ts index 486f59e..00e0e3b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,63 +1,76 @@ -import * as vscode from 'vscode'; -import { DecorationProvider } from './fileDecorationProvider'; +import * as vscode from "vscode"; +import { DecorationProvider } from "./fileDecorationProvider"; var provider: DecorationProvider; // This method is called when the extension is activated export function activate(context: vscode.ExtensionContext) { + //register the decoration provider + provider = new DecorationProvider(); + let disposable = vscode.window.registerFileDecorationProvider(provider); + context.subscriptions.push(disposable); - //register the decoration provider - provider = new DecorationProvider(); - let disposable = vscode.window.registerFileDecorationProvider(provider); - context.subscriptions.push(disposable); + context.subscriptions.push( + vscode.commands.registerCommand( + "markfiles.markUnmarkActiveFile", + async (contextUri: vscode.Uri) => { + const uri = vscode.window.activeTextEditor?.document.uri; + if (uri) { + const stat = await vscode.workspace.fs.stat(uri); + if (stat.type !== vscode.FileType.File) { + return; + } //can't mark directory + if (provider.markedFiles.has(uri.fsPath)) { + provider.update([], [uri.fsPath]); + } else { + provider.update([uri.fsPath], []); + } + } + } + ) + ); - context.subscriptions.push( - vscode.commands.registerCommand('markfiles.markUnmarkActiveFile', async (contextUri: vscode.Uri) => { - const uri = vscode.window.activeTextEditor?.document.uri; - if (uri) { - const stat = await vscode.workspace.fs.stat(uri); - if (stat.type !== vscode.FileType.File) {return;} //can't mark directory - if (provider.markedFiles.has(uri.fsPath)) { - provider.update([], [uri.fsPath]); - } else { - provider.update([uri.fsPath], []); - } - } - }) - ); + context.subscriptions.push( + vscode.commands.registerCommand( + "markfiles.markUnmarkSelectedFile", + async (clickedFile: vscode.Uri, selectedFiles: vscode.Uri[]) => { + for (const uri of selectedFiles) { + if (uri) { + const stat = await vscode.workspace.fs.stat(uri); + if (stat.type !== vscode.FileType.File) { + continue; + } //can't mark directory + if (provider.markedFiles.has(uri.fsPath)) { + provider.update([], [uri.fsPath]); + } else { + provider.update([uri.fsPath], []); + } + } + } + } + ) + ); - context.subscriptions.push( - vscode.commands.registerCommand('markfiles.markUnmarkSelectedFile', async (clickedFile: vscode.Uri, selectedFiles: vscode.Uri[]) => { - for (const uri of selectedFiles) { - if (uri) { - const stat = await vscode.workspace.fs.stat(uri); - if (stat.type !== vscode.FileType.File) {continue;} //can't mark directory - if (provider.markedFiles.has(uri.fsPath)) { - provider.update([], [uri.fsPath]); - } else { - provider.update([uri.fsPath], []); - } - } - } - }) - ); + context.subscriptions.push( + vscode.commands.registerCommand( + "markfiles.reloadFromScopeFile", + async () => { + provider.loadFromScopeFile(true); + vscode.window.showInformationMessage( + "Loading marked files from scope file(s)" + ); + } + ) + ); - context.subscriptions.push( - vscode.commands.registerCommand('markfiles.reloadFromScopeFile', async () => { - provider.loadFromScopeFile(true); - vscode.window.showInformationMessage('Loading marked files from scope file(s)'); - }) - ); - - //listen to configuration changes - vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('markfiles')) { - provider.configChanged(); - }}); - context.subscriptions.push(disposable); - - + //listen to configuration changes + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("markfiles")) { + provider.configChanged(); + } + }); + context.subscriptions.push(disposable); } // This method is called when the extension is deactivated -export function deactivate() {} \ No newline at end of file +export function deactivate() {} diff --git a/src/fileDecorationProvider.ts b/src/fileDecorationProvider.ts index ab63d20..7985d7c 100644 --- a/src/fileDecorationProvider.ts +++ b/src/fileDecorationProvider.ts @@ -1,4 +1,4 @@ -import path = require('path'); +import path = require("path"); import { Uri, CancellationToken, @@ -9,9 +9,9 @@ import { workspace, ThemeColor, FileType, -} from 'vscode'; -import { asyncReadFile } from './utils'; -import { existsSync, writeFile } from 'fs'; +} from "vscode"; +import { asyncReadFile } from "./utils"; +import { existsSync, writeFile } from "fs"; export class DecorationProvider implements FileDecorationProvider { private readonly _onDidChangeFileDecorations: EventEmitter = @@ -29,17 +29,17 @@ export class DecorationProvider implements FileDecorationProvider { if (token.isCancellationRequested) { return {}; } - const config = workspace.getConfiguration('markfiles'); + const config = workspace.getConfiguration("markfiles"); const colorize = - config.colorMarkedFile === 'color' || config.colorMarkedFile === 'both'; + config.colorMarkedFile === "color" || config.colorMarkedFile === "both"; const icon = - config.colorMarkedFile === 'icon' || config.colorMarkedFile === 'both'; + config.colorMarkedFile === "icon" || config.colorMarkedFile === "both"; if (this.markedFiles.has(uri.fsPath)) { return { propagate: true, badge: icon && config.markedFileIcon, - tooltip: 'This file is marked', - color: colorize && new ThemeColor('markfiles.markedFileColor'), + tooltip: "This file is marked", + color: colorize && new ThemeColor("markfiles.markedFileColor"), }; } return {}; @@ -81,7 +81,7 @@ export class DecorationProvider implements FileDecorationProvider { return; } for (const rf of rootFolders) { - const scopeFilePath = path.join(rf, 'scope.txt'); + const scopeFilePath = path.join(rf, "scope.txt"); this.scopeFilesByProjetRootsURIs[rf] = scopeFilePath; } @@ -129,9 +129,9 @@ export class DecorationProvider implements FileDecorationProvider { }, {}); for (const workspaceUri in markedFilesByWS) { - const markedFiles = markedFilesByWS[workspaceUri].join('\n'); + const markedFiles = markedFilesByWS[workspaceUri].join("\n"); const path = this.scopeFilesByProjetRootsURIs[workspaceUri]; - writeFile(path, markedFiles, { flag: 'w' }, (err) => { + writeFile(path, markedFiles, { flag: "w" }, (err) => { if (err) { console.error( `markfiles: Failed to write scope file with path ${path}. Err: ${err}` diff --git a/src/utils.ts b/src/utils.ts index cd499a4..692cb90 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,14 +1,15 @@ -import {promises as fsPromises} from 'fs'; +import { promises as fsPromises } from "fs"; export async function asyncReadFile(path: string) { try { - const contents = await fsPromises.readFile(path, 'utf-8'); + const contents = await fsPromises.readFile(path, "utf-8"); const arr = contents.split(/\r?\n/); return arr; } catch (err) { - console.error(`markfiles: Failed to read file with path ${path} from disk. err: ${err}`); + console.error( + `markfiles: Failed to read file with path ${path} from disk. err: ${err}` + ); } } - diff --git a/tsconfig.json b/tsconfig.json index 315af7e..c5bff67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,15 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "outDir": "out", - "lib": [ - "ES2020" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */ + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + } } From e655035327fd2cd37dd7f556d0c69b74b3679ad0 Mon Sep 17 00:00:00 2001 From: Valentin Date: Sat, 8 Apr 2023 15:19:41 +0200 Subject: [PATCH 2/5] add test and tsc artifacts to gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 6704566..43caf2e 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,9 @@ dist # TernJS port file .tern-port + +#test +.vscode-test + +#typescript compiled artifacts +out/ From b2337160ac3f70b9e5e6dcf5ea9c86ec38e52d09 Mon Sep 17 00:00:00 2001 From: Valentin Date: Sat, 8 Apr 2023 15:25:36 +0200 Subject: [PATCH 3/5] * add support for .gitignore pattern * switch from file-based marked file storage to workspace memento * add an option to export marked files to disk * various fixes * remove compilation artifacts * add testing infrastructure --- out/extension.js | 46 -------- out/extension.js.map | 1 - out/fileDecorationProvider.js | 114 ------------------- out/fileDecorationProvider.js.map | 1 - out/utils.js | 16 --- out/utils.js.map | 1 - package.json | 19 +++- src/extension.ts | 92 +++++++++------ src/fileDecorationProvider.ts | 181 ++++++++++++++++++++++-------- src/test/runTest.ts | 23 ++++ src/test/suite/extension.test.ts | 15 +++ src/test/suite/index.ts | 37 ++++++ yarn.lock | 2 +- 13 files changed, 286 insertions(+), 262 deletions(-) delete mode 100644 out/extension.js delete mode 100644 out/extension.js.map delete mode 100644 out/fileDecorationProvider.js delete mode 100644 out/fileDecorationProvider.js.map delete mode 100644 out/utils.js delete mode 100644 out/utils.js.map create mode 100644 src/test/runTest.ts create mode 100644 src/test/suite/extension.test.ts create mode 100644 src/test/suite/index.ts diff --git a/out/extension.js b/out/extension.js deleted file mode 100644 index 30eea00..0000000 --- a/out/extension.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.deactivate = exports.activate = void 0; -const vscode = require("vscode"); -const fileDecorationProvider_1 = require("./fileDecorationProvider"); -var provider; -// This method is called when the extension is activated -function activate(context) { - //register the decoration provider - provider = new fileDecorationProvider_1.DecorationProvider(); - let disposable = vscode.window.registerFileDecorationProvider(provider); - context.subscriptions.push(disposable); - disposable = vscode.commands.registerCommand('markfiles.markUnmarkFile', async (contextUri) => { - const uri = contextUri || vscode.window.activeTextEditor?.document.uri; - if (uri) { - const stat = await vscode.workspace.fs.stat(uri); - if (stat.type !== vscode.FileType.File) { - return; - } //can't mark directory - if (provider.markedFiles.has(uri.fsPath)) { - provider.update([], [uri.fsPath]); - } - else { - provider.update([uri.fsPath], []); - } - } - }); - context.subscriptions.push(disposable); - disposable = vscode.commands.registerCommand('markfiles.reloadFromScopeFile', async () => { - provider.loadFromScopeFile(true); - vscode.window.showInformationMessage('Loading marked files from scope file(s)'); - }); - context.subscriptions.push(disposable); - //listen to configuration changes - vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('markfiles')) { - provider.configChanged(); - } - }); - context.subscriptions.push(disposable); -} -exports.activate = activate; -// This method is called when the extension is deactivated -function deactivate() { } -exports.deactivate = deactivate; -//# sourceMappingURL=extension.js.map \ No newline at end of file diff --git a/out/extension.js.map b/out/extension.js.map deleted file mode 100644 index 048c829..0000000 --- a/out/extension.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":";;;AAAA,iCAAiC;AACjC,qEAA8D;AAE9D,IAAI,QAA4B,CAAC;AAEjC,wDAAwD;AACxD,SAAgB,QAAQ,CAAC,OAAgC;IAExD,kCAAkC;IAClC,QAAQ,GAAG,IAAI,2CAAkB,EAAE,CAAC;IACpC,IAAI,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,8BAA8B,CAAC,QAAQ,CAAC,CAAC;IACxE,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAEvC,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,0BAA0B,EAAE,KAAK,EAAE,UAAsB,EAAE,EAAE;QACzG,MAAM,GAAG,GAAG,UAAU,IAAI,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC,GAAG,CAAC;QACvE,IAAI,GAAG,EAAE;YACR,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjD,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;gBAAC,OAAO;aAAC,CAAC,sBAAsB;YACxE,IAAI,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;gBACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;aAClC;iBAAM;gBACN,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;aAClC;SACD;IACF,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAEvC,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QACxF,QAAQ,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,yCAAyC,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAEvC,iCAAiC;IACjC,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE,EAAE;QAC/C,IAAI,CAAC,CAAC,oBAAoB,CAAC,WAAW,CAAC,EAAE;YACxC,QAAQ,CAAC,aAAa,EAAE,CAAC;SACzB;IAAA,CAAC,CAAC,CAAC;IACL,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAGxC,CAAC;AAnCD,4BAmCC;AAED,0DAA0D;AAC1D,SAAgB,UAAU,KAAI,CAAC;AAA/B,gCAA+B"} \ No newline at end of file diff --git a/out/fileDecorationProvider.js b/out/fileDecorationProvider.js deleted file mode 100644 index a6c35d5..0000000 --- a/out/fileDecorationProvider.js +++ /dev/null @@ -1,114 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DecorationProvider = void 0; -const path = require("path"); -const vscode_1 = require("vscode"); -const utils_1 = require("./utils"); -const fs_1 = require("fs"); -class DecorationProvider { - constructor() { - this._onDidChangeFileDecorations = new vscode_1.EventEmitter(); - this.onDidChangeFileDecorations = this._onDidChangeFileDecorations.event; - this.markedFiles = new Set(); - this.scopeFilesByProjetRootsURIs = {}; //project root URI --> scope file URIs - this.loadFromScopeFile(); - } - provideFileDecoration(uri, token) { - if (token.isCancellationRequested) { - return {}; - } - const config = vscode_1.workspace.getConfiguration('markfiles'); - const colorize = config.colorMarkedFile === 'color' || config.colorMarkedFile === 'both'; - const icon = config.colorMarkedFile === 'icon' || config.colorMarkedFile === 'both'; - if (this.markedFiles.has(uri.fsPath)) { - return { - propagate: true, - badge: icon && config.markedFileIcon, - tooltip: 'This file is marked', - color: colorize && new vscode_1.ThemeColor('markfiles.markedFileColor'), - }; - } - return {}; - } - //mark or unmark files - async update(markedFiles, unmarkedFiles) { - if (!markedFiles.length && !unmarkedFiles.length) { - return; - } - markedFiles.forEach((markedFile) => { - if (!this.markedFiles.has(markedFile)) { - this.markedFiles.add(markedFile); - this._onDidChangeFileDecorations.fire(vscode_1.Uri.file(markedFile)); - } - }); - unmarkedFiles.forEach((unmarkedFile) => { - if (this.markedFiles.delete(unmarkedFile)) { - this._onDidChangeFileDecorations.fire(vscode_1.Uri.file(unmarkedFile)); - } - }); - this.writeMarkedFilesToFile(); - } - async loadFromScopeFile(reload = false) { - if (reload) { - this.markedFiles.forEach((markedFile) => { - this.markedFiles.delete(markedFile); - this._onDidChangeFileDecorations.fire(vscode_1.Uri.file(markedFile)); - }); - } - const rootFolders = vscode_1.workspace.workspaceFolders?.map((folder) => folder.uri.path); - if (!rootFolders) { - return; - } - for (const rf of rootFolders) { - const scopeFilePath = path.join(rf, 'scope.txt'); - this.scopeFilesByProjetRootsURIs[rf] = scopeFilePath; - } - for (const wsURI in this.scopeFilesByProjetRootsURIs) { - const scopeFileURI = this.scopeFilesByProjetRootsURIs[wsURI]; - if ((0, fs_1.existsSync)(scopeFileURI)) { - this.loadMarkedFiles(scopeFileURI, wsURI); //load marked files from `scope` file in workspace root - } - } - } - async configChanged() { - this.markedFiles.forEach((markedFile) => this._onDidChangeFileDecorations.fire(vscode_1.Uri.file(markedFile))); - } - async loadMarkedFiles(scopeUri, projectRootUri) { - let markedRelPath = (await (0, utils_1.asyncReadFile)(scopeUri)) || []; - let markedAbsPath = markedRelPath - .filter((p) => p.length > 0) - .map((relPath) => path.resolve(projectRootUri, relPath)); - //.filter(async (p) => await (await workspace.fs.stat(Uri.parse(p))).type === FileType.File) - this.update(markedAbsPath, []); - } - //write marked files to `scope` file in workspace root - async writeMarkedFilesToFile() { - if (!Object.keys(this.scopeFilesByProjetRootsURIs).length) { - return; - } - // sort by workspace folders - const markedFilesByWS = Array.from(this.markedFiles).reduce((acc, uri) => { - const workspaceFolder = vscode_1.workspace.getWorkspaceFolder(vscode_1.Uri.parse(uri)); - if (workspaceFolder) { - if (!acc[workspaceFolder.uri.fsPath]) { - acc[workspaceFolder.uri.fsPath] = [uri]; - } - else { - acc[workspaceFolder.uri.fsPath].push(uri); - } - } - return acc; - }, {}); - for (const workspaceUri in markedFilesByWS) { - const markedFiles = markedFilesByWS[workspaceUri].join('\n'); - const path = this.scopeFilesByProjetRootsURIs[workspaceUri]; - (0, fs_1.writeFile)(path, markedFiles, { flag: 'w' }, (err) => { - if (err) { - console.error(`markfiles: Failed to write scope file with path ${path}. Err: ${err}`); - } - }); - } - } -} -exports.DecorationProvider = DecorationProvider; -//# sourceMappingURL=fileDecorationProvider.js.map \ No newline at end of file diff --git a/out/fileDecorationProvider.js.map b/out/fileDecorationProvider.js.map deleted file mode 100644 index 2f8ac1f..0000000 --- a/out/fileDecorationProvider.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"fileDecorationProvider.js","sourceRoot":"","sources":["../src/fileDecorationProvider.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/out/utils.js b/out/utils.js deleted file mode 100644 index 9ce67bf..0000000 --- a/out/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.asyncReadFile = void 0; -const fs_1 = require("fs"); -async function asyncReadFile(path) { - try { - const contents = await fs_1.promises.readFile(path, 'utf-8'); - const arr = contents.split(/\r?\n/); - return arr; - } - catch (err) { - console.error(`markfiles: Failed to read file with path ${path} from disk. err: ${err}`); - } -} -exports.asyncReadFile = asyncReadFile; -//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/out/utils.js.map b/out/utils.js.map deleted file mode 100644 index 3377aa0..0000000 --- a/out/utils.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;AAAA,2BAA0C;AAEnC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,IAAI;QACF,MAAM,QAAQ,GAAG,MAAM,aAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAE1D,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAEpC,OAAO,GAAG,CAAC;KACZ;IAAC,OAAO,GAAG,EAAE;QACZ,OAAO,CAAC,KAAK,CAAC,4CAA4C,IAAI,oBAAoB,GAAG,EAAE,CAAC,CAAC;KAC1F;AACH,CAAC;AAVD,sCAUC"} \ No newline at end of file diff --git a/package.json b/package.json index e9804bb..8f56667 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,10 @@ { "command": "markfiles.reloadFromScopeFile", "title": "Reload marked files from scope file" + }, + { + "command": "markfiles.writeMarkedFilesToDisk", + "title": "Export marked files to a file" } ], "menus": { @@ -82,6 +86,10 @@ "maxLength": 2, "minLength": 1, "pattern": "[^\\x00-\\x7F]+" + }, + "markfiles.autoloadFromScope": { + "type": "boolean", + "description": "Specifies whether to automatically try loading marked files from the scope file in case no files are marked in the current workspace" } } } @@ -95,21 +103,24 @@ "test": "node ./out/test/runTest.js" }, "devDependencies": { - "@types/vscode": "^1.76.0", "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "16.x", + "@types/vscode": "^1.76.0", "@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/parser": "^5.53.0", + "@vscode/test-electron": "^2.2.3", "eslint": "^8.34.0", "glob": "^8.1.0", "mocha": "^10.2.0", - "typescript": "^4.9.5", - "@vscode/test-electron": "^2.2.3" + "typescript": "^4.9.5" }, "repository": { "type": "git", "url": "https://github.com/vquelque/vscode_mark_files.git" }, - "license": "MIT" + "license": "MIT", + "dependencies": { + "ignore": "^5.2.4" + } } diff --git a/src/extension.ts b/src/extension.ts index 00e0e3b..d5e09f6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,14 +1,28 @@ import * as vscode from "vscode"; import { DecorationProvider } from "./fileDecorationProvider"; -var provider: DecorationProvider; +let provider: DecorationProvider; +let outputChannel: vscode.OutputChannel; +let storage: vscode.Memento; // This method is called when the extension is activated export function activate(context: vscode.ExtensionContext) { + //create WS storage + + storage = context.workspaceState; + if (!storage) { + vscode.window.showInformationMessage( + "Please create a workspace to save the marked files" + ); + } + //initialize output channel + outputChannel = vscode.window.createOutputChannel("Mark Files"); + //register the decoration provider provider = new DecorationProvider(); - let disposable = vscode.window.registerFileDecorationProvider(provider); - context.subscriptions.push(disposable); + context.subscriptions.push( + vscode.window.registerFileDecorationProvider(provider) + ); context.subscriptions.push( vscode.commands.registerCommand( @@ -16,37 +30,26 @@ export function activate(context: vscode.ExtensionContext) { async (contextUri: vscode.Uri) => { const uri = vscode.window.activeTextEditor?.document.uri; if (uri) { - const stat = await vscode.workspace.fs.stat(uri); - if (stat.type !== vscode.FileType.File) { - return; - } //can't mark directory - if (provider.markedFiles.has(uri.fsPath)) { - provider.update([], [uri.fsPath]); - } else { - provider.update([uri.fsPath], []); - } + provider.markOrUnmarkFiles([uri]); } } ) ); + context.subscriptions.push( + vscode.commands.registerCommand( + "markfiles.writeMarkedFilesToDisk", + async () => { + await provider.exportMarkedFilesToFile(); + } + ) + ); + context.subscriptions.push( vscode.commands.registerCommand( "markfiles.markUnmarkSelectedFile", async (clickedFile: vscode.Uri, selectedFiles: vscode.Uri[]) => { - for (const uri of selectedFiles) { - if (uri) { - const stat = await vscode.workspace.fs.stat(uri); - if (stat.type !== vscode.FileType.File) { - continue; - } //can't mark directory - if (provider.markedFiles.has(uri.fsPath)) { - provider.update([], [uri.fsPath]); - } else { - provider.update([uri.fsPath], []); - } - } - } + provider.markOrUnmarkFiles(selectedFiles); } ) ); @@ -63,14 +66,39 @@ export function activate(context: vscode.ExtensionContext) { ) ); - //listen to configuration changes - vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration("markfiles")) { - provider.configChanged(); - } - }); - context.subscriptions.push(disposable); + context.subscriptions.push( + //listen to configuration changes + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("markfiles")) { + provider.configChanged(); + } + }) + ); + + context.subscriptions.push( + vscode.workspace.onDidRenameFiles(async (rename) => { + if (rename.files.length === 0) { + return; + } + + for (const file of rename.files) { + provider.handleFileRename(file.oldUri, file.newUri); + } + }) + ); } +//print to the output channel +export const printChannelOutput = (content: string, reveal = false): void => { + outputChannel.appendLine(content); + if (reveal) { + outputChannel.show(true); + } +}; + +export const getStorage = () => { + return storage; +}; + // This method is called when the extension is deactivated export function deactivate() {} diff --git a/src/fileDecorationProvider.ts b/src/fileDecorationProvider.ts index 7985d7c..5a42670 100644 --- a/src/fileDecorationProvider.ts +++ b/src/fileDecorationProvider.ts @@ -9,9 +9,12 @@ import { workspace, ThemeColor, FileType, + window, } from "vscode"; import { asyncReadFile } from "./utils"; import { existsSync, writeFile } from "fs"; +import ignore from "ignore"; +import { getStorage, printChannelOutput } from "./extension"; export class DecorationProvider implements FileDecorationProvider { private readonly _onDidChangeFileDecorations: EventEmitter = @@ -19,10 +22,14 @@ export class DecorationProvider implements FileDecorationProvider { readonly onDidChangeFileDecorations: Event = this._onDidChangeFileDecorations.event; public markedFiles: Set = new Set(); - private scopeFilesByProjetRootsURIs: { [scopeUri: string]: string } = {}; //project root URI --> scope file URIs constructor() { - this.loadFromScopeFile(); + //try loading the existing marks + if (!this.loadMarkedFilesFromWSStorage()) { + //fallback to scope file if user config allows + const config = workspace.getConfiguration("markfiles"); + config.autoloadFromScope && this.loadFromScopeFile(); + } } provideFileDecoration(uri: Uri, token: CancellationToken): FileDecoration { @@ -46,47 +53,42 @@ export class DecorationProvider implements FileDecorationProvider { } //mark or unmark files - public async update( - markedFiles: Array, - unmarkedFiles: Array - ) { - if (!markedFiles.length && !unmarkedFiles.length) { - return; - } - markedFiles.forEach((markedFile) => { - if (!this.markedFiles.has(markedFile)) { - this.markedFiles.add(markedFile); - this._onDidChangeFileDecorations.fire(Uri.file(markedFile)); + public async markOrUnmarkFiles(uris: Uri[]) { + uris.forEach(async (uri) => { + const stat = await workspace.fs.stat(uri); + if (stat.type !== FileType.File) { + return; //can't mark directory } - }); - unmarkedFiles.forEach((unmarkedFile) => { - if (this.markedFiles.delete(unmarkedFile)) { - this._onDidChangeFileDecorations.fire(Uri.file(unmarkedFile)); + const fPath = uri.fsPath; + if (this.markedFiles.has(uri.fsPath)) { + this.markedFiles.delete(fPath); + getStorage().update(fPath, undefined); //remove from WS storage + } else { + this.markedFiles.add(fPath); + getStorage().update(fPath, true); // add to WS storage } + this._onDidChangeFileDecorations.fire(uri); }); - this.writeMarkedFilesToFile(); } + //reload marked files by reading the scope file + //this clears exising marks public async loadFromScopeFile(reload: boolean = false) { + const scopeFileUrisByFolder = this.getFileURIsByFolderForWS("scope", "txt"); if (reload) { - this.markedFiles.forEach((markedFile) => { - this.markedFiles.delete(markedFile); - this._onDidChangeFileDecorations.fire(Uri.file(markedFile)); - }); - } - const rootFolders = workspace.workspaceFolders?.map( - (folder) => folder.uri.path - ); - if (!rootFolders) { - return; - } - for (const rf of rootFolders) { - const scopeFilePath = path.join(rf, "scope.txt"); - this.scopeFilesByProjetRootsURIs[rf] = scopeFilePath; + const confirm = await window.showInformationMessage( + "This operation will clear all marked files. Do you want to continue?", + "Yes", + "No" + ); + if (confirm === "No") { + //abort + return; + } + this.clearMarkedFilesForWS(); } - - for (const wsURI in this.scopeFilesByProjetRootsURIs) { - const scopeFileURI = this.scopeFilesByProjetRootsURIs[wsURI]; + for (const wsURI in scopeFileUrisByFolder) { + const scopeFileURI = scopeFileUrisByFolder[wsURI]; if (existsSync(scopeFileURI)) { this.loadMarkedFiles(scopeFileURI, wsURI); //load marked files from `scope` file in workspace root } @@ -100,17 +102,35 @@ export class DecorationProvider implements FileDecorationProvider { } async loadMarkedFiles(scopeUri: string, projectRootUri: string) { - let markedRelPath = (await asyncReadFile(scopeUri)) || []; - let markedAbsPath = markedRelPath - .filter((p) => p.length > 0) - .map((relPath) => path.resolve(projectRootUri, relPath)); - //.filter(async (p) => await (await workspace.fs.stat(Uri.parse(p))).type === FileType.File) - this.update(markedAbsPath, []); + let patterns = (await asyncReadFile(scopeUri)) || []; + const ig = ignore().add(patterns); + // find files in all workspace folders, and filter them according to the specified gitignore patterns + // https://git-scm.com/docs/gitignore + let markedAbsPath = (await workspace.findFiles("**/*")) + .map((uri) => path.relative(projectRootUri, uri.fsPath)) + .filter((relPath) => ig.ignores(relPath)) + .map((relPath) => Uri.file(path.resolve(projectRootUri, relPath))); + printChannelOutput(`Loaded patterns from ${scopeUri}`); + this.markOrUnmarkFiles(markedAbsPath); } //write marked files to `scope` file in workspace root - private async writeMarkedFilesToFile() { - if (!Object.keys(this.scopeFilesByProjetRootsURIs).length) { + public async exportMarkedFilesToFile() { + printChannelOutput("Exporting marked files to scope file"); + const fileName = await window.showInputBox({ + placeHolder: "File name", + prompt: "Please enter a name for the exported file", + value: "scope", + }); + if (!fileName) { + return; + } + const scopeFileUrisByFolder = this.getFileURIsByFolderForWS( + fileName, + "txt" + ); + if (!Object.keys(scopeFileUrisByFolder).length) { + printChannelOutput("No opened project - aborting"); return; } // sort by workspace folders @@ -119,10 +139,11 @@ export class DecorationProvider implements FileDecorationProvider { }>((acc: { [key: string]: string[] }, uri: string) => { const workspaceFolder = workspace.getWorkspaceFolder(Uri.parse(uri)); if (workspaceFolder) { + const relativePath = path.relative(workspaceFolder.uri.fsPath, uri); //use relative URIs in the scope file if (!acc[workspaceFolder.uri.fsPath]) { - acc[workspaceFolder.uri.fsPath] = [uri]; + acc[workspaceFolder.uri.fsPath] = [relativePath]; } else { - acc[workspaceFolder.uri.fsPath].push(uri); + acc[workspaceFolder.uri.fsPath].push(relativePath); } } return acc; @@ -130,7 +151,23 @@ export class DecorationProvider implements FileDecorationProvider { for (const workspaceUri in markedFilesByWS) { const markedFiles = markedFilesByWS[workspaceUri].join("\n"); - const path = this.scopeFilesByProjetRootsURIs[workspaceUri]; + const path = scopeFileUrisByFolder[workspaceUri]; + + try { + await await workspace.fs.stat(Uri.file(path)); + const confirm = await window.showInformationMessage( + "This operation will overwrite the existing scope file. Do you want to continue?", + "Yes", + "No" + ); + if (confirm === "No") { + //abort + return; + } + } catch { + //no existing file => continue + } + printChannelOutput(`Exporting marked files to file ${path}`); writeFile(path, markedFiles, { flag: "w" }, (err) => { if (err) { console.error( @@ -140,4 +177,56 @@ export class DecorationProvider implements FileDecorationProvider { }); } } + + public handleFileRename(oldUri: Uri, newUri: Uri) { + if (!this.markedFiles.has(oldUri.fsPath)) { + return; + } + this.markedFiles.delete(oldUri.fsPath); + getStorage().update(oldUri.fsPath, undefined); + this.markedFiles.add(newUri.fsPath); + getStorage().update(newUri.fsPath, true); + this._onDidChangeFileDecorations.fire(newUri); + } + + private loadMarkedFilesFromWSStorage(): boolean { + if (!getStorage()) { + return false; + } + const markedURIs = getStorage().keys(); + if (!markedURIs.length) { + return false; + } + printChannelOutput(`Loaded files from workspace storage`); + this.markOrUnmarkFiles(markedURIs.map((uri) => Uri.file(uri))); + return true; + } + + private clearMarkedFilesForWS() { + this.markedFiles.forEach((markedFile) => { + this.markedFiles.delete(markedFile); + getStorage().update(markedFile, undefined); + this._onDidChangeFileDecorations.fire(Uri.file(markedFile)); + }); + } + + /** + * @param fileName: name of the file + * @returns an array of URIs for files with name `fileName` for each folder in the workspace + */ + private getFileURIsByFolderForWS(fileName: string, extension: string) { + const scopeFilesByProjectRootsURIs: { [scopeUri: string]: string } = {}; + //iterate over all opened workspace folders + const rootFolders = workspace.workspaceFolders?.map( + (folder) => folder.uri.path + ); + if (!rootFolders) { + return {}; + } + for (const rf of rootFolders) { + const scopeFilePath = path.join(rf, fileName.concat(".", extension)); + scopeFilesByProjectRootsURIs[rf] = scopeFilePath; + } + return scopeFilesByProjectRootsURIs; + } } diff --git a/src/test/runTest.ts b/src/test/runTest.ts new file mode 100644 index 0000000..41dc0fe --- /dev/null +++ b/src/test/runTest.ts @@ -0,0 +1,23 @@ +import * as path from "path"; + +import { runTests } from "@vscode/test-electron"; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, "../../"); + + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, "./suite/index"); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error("Failed to run tests"); + process.exit(1); + } +} + +main(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts new file mode 100644 index 0000000..5060953 --- /dev/null +++ b/src/test/suite/extension.test.ts @@ -0,0 +1,15 @@ +import * as assert from "assert"; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from "vscode"; +// import * as myExtension from '../../extension'; + +suite("Extension Test Suite", () => { + vscode.window.showInformationMessage("Start all tests."); + + test("Sample test", () => { + assert.strictEqual([1, 2, 3].indexOf(5), -1); + assert.strictEqual([1, 2, 3].indexOf(0), -1); + }); +}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts new file mode 100644 index 0000000..3e791cf --- /dev/null +++ b/src/test/suite/index.ts @@ -0,0 +1,37 @@ +import * as path from "path"; +import * as Mocha from "mocha"; +import * as glob from "glob"; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: "tdd", + }); + + const testsRoot = path.resolve(__dirname, ".."); + + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); +} diff --git a/yarn.lock b/yarn.lock index e577591..bb30424 100644 --- a/yarn.lock +++ b/yarn.lock @@ -732,7 +732,7 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== From 8720781e3ddf8716afdd53ad93e4b5c37e79865c Mon Sep 17 00:00:00 2001 From: Valentin Date: Tue, 11 Apr 2023 10:03:01 +0200 Subject: [PATCH 4/5] Add test boilerplate --- package.json | 3 ++- src/extension.ts | 2 +- src/fileDecorationProvider.ts | 1 + src/test/example/file1.txt | 1 + src/test/example/file2.txt | 1 + src/test/runTest.ts | 6 ++++- src/test/suite/extension.test.ts | 6 ----- src/test/suite/index.ts | 1 + src/test/suite/provider.test.ts | 39 ++++++++++++++++++++++++++++++++ src/test/utils.ts | 33 +++++++++++++++++++++++++++ 10 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 src/test/example/file1.txt create mode 100644 src/test/example/file2.txt create mode 100644 src/test/suite/provider.test.ts create mode 100644 src/test/utils.ts diff --git a/package.json b/package.json index 8f56667..af4de98 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,8 @@ "vscode:prepublish": "yarn run compile", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", - "pretest": "yarn run compile && yarn run lint", + "copy-files": "cp -r ./src/test/example ./out/test", + "pretest": "yarn run compile && yarn run lint && yarn run copy-files", "lint": "eslint src --ext ts", "test": "node ./out/test/runTest.js" }, diff --git a/src/extension.ts b/src/extension.ts index d5e09f6..e43de1b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,6 @@ let storage: vscode.Memento; // This method is called when the extension is activated export function activate(context: vscode.ExtensionContext) { //create WS storage - storage = context.workspaceState; if (!storage) { vscode.window.showInformationMessage( @@ -86,6 +85,7 @@ export function activate(context: vscode.ExtensionContext) { } }) ); + return context; } //print to the output channel diff --git a/src/fileDecorationProvider.ts b/src/fileDecorationProvider.ts index 5a42670..17cdf17 100644 --- a/src/fileDecorationProvider.ts +++ b/src/fileDecorationProvider.ts @@ -57,6 +57,7 @@ export class DecorationProvider implements FileDecorationProvider { uris.forEach(async (uri) => { const stat = await workspace.fs.stat(uri); if (stat.type !== FileType.File) { + console.log("can't mark directory"); return; //can't mark directory } const fPath = uri.fsPath; diff --git a/src/test/example/file1.txt b/src/test/example/file1.txt new file mode 100644 index 0000000..50bd449 --- /dev/null +++ b/src/test/example/file1.txt @@ -0,0 +1 @@ +hello world 1! \ No newline at end of file diff --git a/src/test/example/file2.txt b/src/test/example/file2.txt new file mode 100644 index 0000000..e7dee3b --- /dev/null +++ b/src/test/example/file2.txt @@ -0,0 +1 @@ +hello world 2! \ No newline at end of file diff --git a/src/test/runTest.ts b/src/test/runTest.ts index 41dc0fe..3cd9f17 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -13,7 +13,11 @@ async function main() { const extensionTestsPath = path.resolve(__dirname, "./suite/index"); // Download VS Code, unzip it and run the integration test - await runTests({ extensionDevelopmentPath, extensionTestsPath }); + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [path.resolve(__dirname, "./example/")], + }); } catch (err) { console.error("Failed to run tests"); process.exit(1); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 5060953..07e8af2 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -3,13 +3,7 @@ import * as assert from "assert"; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from "vscode"; -// import * as myExtension from '../../extension'; suite("Extension Test Suite", () => { vscode.window.showInformationMessage("Start all tests."); - - test("Sample test", () => { - assert.strictEqual([1, 2, 3].indexOf(5), -1); - assert.strictEqual([1, 2, 3].indexOf(0), -1); - }); }); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 3e791cf..d50b5ef 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -6,6 +6,7 @@ export function run(): Promise { // Create the mocha test const mocha = new Mocha({ ui: "tdd", + timeout: 1000000, }); const testsRoot = path.resolve(__dirname, ".."); diff --git a/src/test/suite/provider.test.ts b/src/test/suite/provider.test.ts new file mode 100644 index 0000000..51ceaf0 --- /dev/null +++ b/src/test/suite/provider.test.ts @@ -0,0 +1,39 @@ +import * as markFilesProvider from "../../fileDecorationProvider"; +import * as vscode from "vscode"; +import path = require("path"); +import { extension, createTestFile, removeTestFile } from "../utils"; + +suite("Provider Test Suite", () => { + let provider: markFilesProvider.DecorationProvider; + let exampleFilesUris: vscode.Uri[]; + let extensionContext: vscode.ExtensionContext; + + // suiteSetup(async () => { + // provider = new markFilesProvider.DecorationProvider(); + // const exampleFiles = ["file1.txt", "file2.txt"].map((fname) => + // path.resolve(__dirname, "../example", fname) + // ); + // exampleFilesUris = exampleFiles.map((p) => vscode.Uri.file(p)); + // }); + + suiteSetup(async () => { + await createTestFile("file3.html"); + extensionContext = await extension.activate(); + }); + + test("Test mark file", async () => { + await vscode.commands.executeCommand('markfiles.markUnmarkSelectedFile'); + // await vscode.commands.executeCommand('markfiles.writeMarkedFilesToDisk'); + console.log("keys : \n"); + console.log(extensionContext); + }); + + + suiteTeardown(async () => {await removeTestFile();}); +}); + +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/src/test/utils.ts b/src/test/utils.ts new file mode 100644 index 0000000..045ce39 --- /dev/null +++ b/src/test/utils.ts @@ -0,0 +1,33 @@ +import * as vscode from "vscode"; +import * as assert from "assert"; +import * as path from "path"; +import * as fs from "fs"; + +const packageJSON = JSON.parse( + fs.readFileSync(path.join(__dirname, "../../package.json"), "utf-8") +) as { name: string; publisher: string }; + +export const extension = vscode.extensions.getExtension( + `${packageJSON.publisher}.${packageJSON.name}` +)! as vscode.Extension; + +assert.ok(extension); + +export async function createTestFile( + fileName: string, + content: string = "" +): Promise { + const filePath = path.join( + vscode.workspace.workspaceFolders![0].uri.fsPath, + fileName + ); + fs.writeFileSync(filePath, content); + const uri = vscode.Uri.file(filePath); + await vscode.window.showTextDocument(uri); +} + +export async function removeTestFile(): Promise { + const uri = vscode.window.activeTextEditor?.document.uri as vscode.Uri; + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await vscode.workspace.fs.delete(uri); +} From c7cf2b78a33eb708c34e998a516a8611c49864b6 Mon Sep 17 00:00:00 2001 From: Valentin Date: Tue, 11 Apr 2023 10:22:40 +0200 Subject: [PATCH 5/5] Bump version number, update README, prettify all --- CHANGELOG.md | 13 +++++++++++++ README.md | 6 +++--- package.json | 2 +- src/test/suite/provider.test.ts | 9 +++++---- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 866cea4..2470d18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,3 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [1.0.0] - 2023-03-12 + +## [1.1.0] - 2023-04-11 + +### Added + +- Mark/Unmark multiple files from the file explorer (Thanks @UsmannK) +- Switch to '.gitignore format' for the `scope.txt` file (Thanks @DavidBDiligence) +- Switch from disk-based storage to workspace memory (for future web compatibility) +- Add a feature to export marked files to a custom file on disk + +### Changed + +- Formatted files using Prettier. diff --git a/README.md b/README.md index aa56b8e..ad8af5b 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ The extension allows you to mark/unmark files by: -- Providing a `scope.txt` file in the workspace root folder. The file contains a list of absolute file paths or relative file paths to the workspace root folder. The file paths must be separated by newlines. -- Right clicking on a file in the file explorer and selecting `Mark/Unmark File` from the context menu. +- Providing a `scope.txt` file in the workspace root folder. The file contains a list of patterns for files to include in the scope. The patterns are written using the [.gitignore format](https://git-scm.com/docs/gitignore#_pattern_format), e.g., `**.sol` to include all Solidity files in all directories. +- Selecting a single/multiple files in the explorer and clicking on `Mark/Unmark File` from the context menu. - Right clicking on an editor tab and selecting `Mark/Unmark File` from the context menu. -Note that the marked files are stored in a `scope.txt` file in the workspace root folder. If the file does not exist, it will be created. +Note that the marked files are stored in the workspace local storage. In case of mulit-root workspaces, if you don't save the workspace, the marks will be discarded if the workspace is closed. You can also export the marked files to a file on disk by invoking the `Export maked files to a file` command. For multi-root workspaces, marked files will be exported to individual files located at the root of each opened foler. Marked files exported to disk can be re-imported by naming the export file as `scope.txt`, and using the import feature. ### Mark/Unmark files from contextual menus diff --git a/package.json b/package.json index af4de98..73b0ea6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "markfiles", "displayName": "Mark Files", "description": "An extension that allows you to mark files in the file explorer", - "version": "1.0.0", + "version": "1.1.0", "publisher": "vquelque", "engines": { "vscode": "^1.76.0" diff --git a/src/test/suite/provider.test.ts b/src/test/suite/provider.test.ts index 51ceaf0..51a5845 100644 --- a/src/test/suite/provider.test.ts +++ b/src/test/suite/provider.test.ts @@ -22,14 +22,15 @@ suite("Provider Test Suite", () => { }); test("Test mark file", async () => { - await vscode.commands.executeCommand('markfiles.markUnmarkSelectedFile'); - // await vscode.commands.executeCommand('markfiles.writeMarkedFilesToDisk'); + await vscode.commands.executeCommand("markfiles.markUnmarkSelectedFile"); + // await vscode.commands.executeCommand('markfiles.writeMarkedFilesToDisk'); console.log("keys : \n"); console.log(extensionContext); }); - - suiteTeardown(async () => {await removeTestFile();}); + suiteTeardown(async () => { + await removeTestFile(); + }); }); function sleep(ms: number): Promise {