├── .gitignore ├── media ├── icon.png ├── bulk-problem-diagnostics.png ├── icon2.svg └── icon.svg ├── .vscodeignore ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── src ├── test │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ └── runTest.ts ├── extraExcludedfiles.ts ├── fileutil.ts ├── extra │ └── moodle.ts └── extension.ts ├── tsconfig.json ├── .eslintrc.json ├── CHANGELOG.md ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/vscode-bulk-problem-diagnostics/master/media/icon.png -------------------------------------------------------------------------------- /media/bulk-problem-diagnostics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/vscode-bulk-problem-diagnostics/master/media/bulk-problem-diagnostics.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/extraExcludedfiles.ts: -------------------------------------------------------------------------------- 1 | import { Uri, workspace } from 'vscode'; 2 | import { moodleExcludedFiles } from './extra/moodle'; 3 | 4 | export async function extraExcludedFiles(rootUri:Uri): Promise> { 5 | const autoExclude:boolean = 6 | workspace.getConfiguration().get('bulkProblemDiagnostics.autoExcludeFrameworkSuggestions') ?? true; 7 | if (!autoExclude) { 8 | return []; 9 | } 10 | 11 | // More framework detections can be added here: 12 | return [ 13 | ...await moodleExcludedFiles(rootUri), 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "Bulk Problem Diagnostics" extension will be documented in this file. 4 | 5 | ## [1.0.7] 6 | - Due to the recent changes in some extensions (including ESLint 3.0.8), the previous method of 7 | opening the files in the background does not work anymore. Now the extension will open files 8 | in the editor and close them after a delay. This means there is a lot of "blinking" on the 9 | screen as each file is being opened and closed. #4 10 | - Added a new setting "Wait Before Closing", by default 3000ms. 11 | - Default value for the "Delay" setting was raised to 200ms. 12 | 13 | ## [1.0.6] 14 | - Added progress bar, cancel button and remaining time countdown 15 | - Allow to search in error codes as well as error messages 16 | - Automatically open Problems View when the command is executed 17 | 18 | ## [1.0.5] 19 | - changes to README and a new icon 20 | 21 | ## [1.0.4] 22 | - Fixed a problem when files are not re-analysed if the command runs on a small folder 23 | for the second time. As a downside, on consequtive runs the screen might be blinking 24 | since the files will be opened and closed in the browser. 25 | 26 | ## [1.0.2] 27 | - Allow to run the command on the whole project, not just one folder 28 | 29 | ## [1.0.1] 30 | - Open only files with problems 31 | - Added settings to match problems severity and error message 32 | 33 | ## [1.0.0] 34 | - Initial release -------------------------------------------------------------------------------- /src/fileutil.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { CancellationToken, RelativePattern, Uri, window, workspace } from 'vscode'; 3 | import { extraExcludedFiles } from './extraExcludedfiles'; 4 | 5 | export async function readDirectoryRecursively(fsPath:string, token:CancellationToken): Promise|undefined> { 6 | const rootUri = getContainerRoot(fsPath); 7 | const relPath:string = path.relative(rootUri.fsPath, fsPath); 8 | 9 | let excludeFiles:Array = workspace.getConfiguration().get>('bulkProblemDiagnostics.excludeFiles') ?? []; 10 | excludeFiles = [...excludeFiles, ...await extraExcludedFiles(rootUri)]; 11 | const openFiles:string = workspace.getConfiguration().get('bulkProblemDiagnostics.openFiles') ?? '**'; 12 | 13 | const files = await workspace.findFiles( 14 | path.join(relPath, openFiles), 15 | excludeFiles.length ? new RelativePattern(rootUri, '{'+excludeFiles.join(',')+'}') : null, 16 | undefined, 17 | token 18 | ); 19 | if (token.isCancellationRequested) { return undefined; } 20 | return files.sort((n1, n2) => n1.path.localeCompare(n2.path)); 21 | } 22 | 23 | export function isSubDir(parent:string, dir:string): boolean { 24 | const relative = path.relative(parent, dir); 25 | return (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) ? true : false; 26 | } 27 | 28 | function getContainerRoot(fsPath:string): Uri { 29 | if (workspace.workspaceFolders && (workspace.workspaceFolders.length > 0)) { 30 | for (let f of workspace.workspaceFolders) { 31 | if (f.uri.fsPath === fsPath || isSubDir(f.uri.fsPath, fsPath)) { 32 | return f.uri; 33 | } 34 | } 35 | } 36 | const error = `Path '${fsPath} is not inside of any workplace folders`; 37 | window.showErrorMessage(error); 38 | throw new Error(error); 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Version](https://img.shields.io/visual-studio-marketplace/v/MarinaGlancy.bulk-problem-diagnostics)](https://marketplace.visualstudio.com/items?itemName=MarinaGlancy.bulk-problem-diagnostics) 2 | 3 | # VS Code extension "Bulk Problem Diagnostics" 4 | 5 | Opens all files with problems. Splits the large number of files in batches to prevent unloading. 6 | 7 | Inspired by the issue: 8 | https://github.com/microsoft/vscode/issues/13953 9 | 10 | Normally VS Code only shows diagnostics for the opened files and automatically unloads files 11 | and removes them from Problems view when too many files are open. 12 | 13 | This extension iterates through all files, opens them one by one, waits a little and then closes 14 | files that do not have problems. 15 | 16 | To analyse the first batch of files (by default 200): 17 | - Right click on a folder in Explorer and choose **"Open all files with problems"**, or 18 | - Choose the command **"Open** all files with problems"** from the Command Palette (Ctrl+Shift+P) 19 | 20 | To continue: 21 | - Right click on the folder again and choose **"Open all files with problems (continue)"**, or 22 | - Press `Alt+Ctrl+Shift+O` or select **"Open all files with problems (continue)"** from the Command Palette, or 23 | - Press "Continue" in the notification that appears after the first command execution 24 | 25 | ![Example](https://raw.githubusercontent.com/marinaglancy/vscode-bulk-problem-diagnostics/master/media/bulk-problem-diagnostics.png) 26 | 27 | ### Extension settings 28 | 29 | | Setting | Description | Default | 30 | |---|---|---| 31 | | Files Limit | Maximum number of files to analyse in one operation. If the folder has more files, the next command execution will start from where it finished last time. | 200 | 32 | | Open Files | Files to analyse. Examples: '**' - all files, '**/*.{php,js}' - only PHP and JS files | ** | 33 | | Exclude Files | Configure glob patterns to exclude certain files and folders from analysing. Relative paths are calculated from the workspace root (not the folder being analysed). | ```**/.git/**, **/node_modules/**, ...``` | 34 | | Auto Exclude Framework Suggestions | Automatically detect other files to exclude for some common projects or frameworks | true | 35 | | Delay | Delay (in ms) between analysing files to allow diagnostics to catch up with the newly loaded files | 200 | 36 | | Wait Before Closing | Time to wait (in ms) before closing a file that did not have problems. Increase if you have slow extensions that take longer to report the problems | 3000 | 37 | | Max Severity Level | Maximum problem severity level, file will only be open if it contains problems of this or lower levels (1 - errors, 2 - warnings, 3 - notices) | 2 | 38 | | Error Message Match | If specified, will only open files that have problems that match this setting | | -------------------------------------------------------------------------------- /src/extra/moodle.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { RelativePattern, Uri, workspace } from 'vscode'; 4 | import * as xmlParser from 'fast-xml-parser'; 5 | 6 | export async function moodleExcludedFiles(rootUri:Uri): Promise> { 7 | // If root folder is a Moodle folder, exclude build files and third party libs. 8 | if (!detectHasMoodle(rootUri)) { return []; } 9 | const libs = [ 10 | ...getThirdPartyLibraries(rootUri, 'lib'), 11 | ...await getPluginsThirdPartyLibraries(rootUri) 12 | ]; 13 | return [ 14 | '**/amd/build/**', 15 | '**/yui/build/**', 16 | ...libs, 17 | ...libs.map(l => `${l}/**`) 18 | ]; 19 | } 20 | 21 | function detectHasMoodle(rootUri:Uri): boolean { 22 | const d:string = rootUri.fsPath; 23 | return ( 24 | pathExists(path.join(d, 'lib', 'moodlelib.php')) && 25 | pathExists(path.join(d, 'version.php')) && 26 | pathExists(path.join(d, 'lib', 'db', 'install.xml')) && 27 | pathExists(path.join(d, 'lib', 'classes', 'plugin_manager.php')) && 28 | pathExists(path.join(d, 'lang', 'en', 'moodle.php')) && 29 | pathExists(path.join(d, 'lib', 'classes', 'component.php')) && 30 | pathExists(path.join(d, 'lib', 'thirdpartylibs.xml'))); 31 | } 32 | 33 | function pathExists(p: string): boolean { 34 | try { 35 | fs.accessSync(p); 36 | } catch (err) { 37 | return false; 38 | } 39 | return true; 40 | } 41 | 42 | export function getThirdPartyLibraries(rootUri:Uri, relpath:string):Array { 43 | const tplpath = path.join(rootUri.fsPath, relpath, 'thirdpartylibs.xml'); 44 | let libs:Array = []; 45 | if (pathExists(tplpath)) { 46 | const parser = new xmlParser.XMLParser(); 47 | const text = fs.readFileSync(tplpath).toString(); 48 | let jObj = parser.parse(text); 49 | if (jObj && jObj.libraries && jObj.libraries.library && jObj.libraries.library.length) { 50 | jObj.libraries.library.forEach((l:{location?:string}) => { 51 | l.location && libs.push(`${relpath}/${l.location}`); 52 | }); 53 | } else if (jObj && jObj.libraries && jObj.libraries.library && jObj.libraries.library.location) { 54 | libs.push(`${relpath}/${jObj.libraries.library.location}`); 55 | } 56 | } 57 | return libs; 58 | } 59 | 60 | async function getPluginsThirdPartyLibraries(rootUri:Uri):Promise> { 61 | const ptypes = await getPluginTypesDirs(path.join(rootUri.fsPath, 'lib', 'components.json')); 62 | let res:Array = []; 63 | for (let ptypeDir of Object.values(ptypes)) { 64 | const files = await workspace.findFiles( 65 | new RelativePattern(path.join(rootUri.fsPath, ptypeDir), '*/thirdpartylibs.xml')); 66 | for (let f of files) { 67 | const relpath = path.dirname(path.relative(rootUri.path, f.path)); 68 | res = [...res, ...getThirdPartyLibraries(rootUri, relpath)]; 69 | } 70 | } 71 | return res; 72 | } 73 | 74 | async function getPluginTypesDirs(jsonpath:string):Promise<{ [key: string]: string }> { 75 | if (pathExists(jsonpath)) { 76 | const json = await JSON.parse(fs.readFileSync(jsonpath, 'utf-8')); 77 | return json.plugintypes ? json.plugintypes : {}; 78 | } 79 | return {}; 80 | } 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bulk-problem-diagnostics", 3 | "displayName": "Bulk Problem Diagnostics", 4 | "description": "Opens all files with problems. Splits large number of files in batches to prevent unloading.", 5 | "version": "1.0.7", 6 | "publisher": "MarinaGlancy", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/marinaglancy/vscode-bulk-problem-diagnostics" 10 | }, 11 | "icon": "media/icon.png", 12 | "engines": { 13 | "vscode": "^1.75.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | ], 20 | "main": "./out/extension.js", 21 | "contributes": { 22 | "commands": [ 23 | { 24 | "command": "bulk-problem-diagnostics.openAllFiles", 25 | "title": "Open all files with problems" 26 | }, 27 | { 28 | "command": "bulk-problem-diagnostics.openAllFilesContinue", 29 | "title": "Open all files with problems (continue)" 30 | } 31 | ], 32 | "menus": { 33 | "explorer/context": [ 34 | { 35 | "command": "bulk-problem-diagnostics.openAllFiles", 36 | "when": "explorerResourceIsFolder", 37 | "group": "explorerResourceIsFolder@1" 38 | }, 39 | { 40 | "command": "bulk-problem-diagnostics.openAllFilesContinue", 41 | "when": "explorerResourceIsFolder && resourcePath in bulkProblemDiagnostics.lastFolder", 42 | "group": "explorerResourceIsFolder@2" 43 | } 44 | ], 45 | "commandPalette": [ 46 | { 47 | "command": "bulk-problem-diagnostics.openAllFilesContinue", 48 | "when": "bulkProblemDiagnostics.hasLastFolder" 49 | } 50 | ] 51 | }, 52 | "keybindings": [ 53 | { 54 | "command": "bulk-problem-diagnostics.openAllFilesContinue", 55 | "key": "ctrl+shift+alt+o", 56 | "mac": "cmd+shift+alt+o", 57 | "when": "bulkProblemDiagnostics.hasLastFolder" 58 | } 59 | ], 60 | "configuration": { 61 | "id": "bulk-problem-diagnostics", 62 | "title": "Bulk Problem Diagnostics", 63 | "properties": { 64 | "bulkProblemDiagnostics.filesLimit": { 65 | "default": 200, 66 | "description": "Maximum number of files to analyse in one operation. If the folder has more files, the next command execution will start from where it finished last time.", 67 | "type": "number" 68 | }, 69 | "bulkProblemDiagnostics.openFiles": { 70 | "default": "**", 71 | "description": "Files to analyse. Examples: '**' - all files, '**/*.{php,js}' - only PHP and JS files", 72 | "type": "string" 73 | }, 74 | "bulkProblemDiagnostics.excludeFiles": { 75 | "type": "array", 76 | "items": { 77 | "type": "string" 78 | }, 79 | "default": [ 80 | "**/.git/**", 81 | "**/.svn/**", 82 | "**/.hg/**", 83 | "**/CVS/**", 84 | "**/.DS_Store/**", 85 | "**/node_modules/**", 86 | "**/bower_components/**", 87 | "**/vendor/**", 88 | "**/.history/**" 89 | ], 90 | "description": "Configure glob patterns to exclude certain files and folders from analysing. Relative paths are calculated from the workspace root (not the folder being analysed)." 91 | }, 92 | "bulkProblemDiagnostics.autoExcludeFrameworkSuggestions": { 93 | "default": true, 94 | "description": "Automatically detect other files to exclude for some common projects or frameworks", 95 | "type": "boolean" 96 | }, 97 | "bulkProblemDiagnostics.delay": { 98 | "default": 200, 99 | "description": "Delay (in ms) between analysing files to allow diagnostics to catch up with the newly loaded files", 100 | "type": "number" 101 | }, 102 | "bulkProblemDiagnostics.waitBeforeClosing": { 103 | "default": 3000, 104 | "description": "Time to wait (in ms) before closing a file that did not have problems. Increase if you have slow extensions that take longer to report the problems", 105 | "type": "number" 106 | }, 107 | "bulkProblemDiagnostics.maxSeverityLevel": { 108 | "default": 2, 109 | "description": "Maximum problem severity level, file will only be open if it contains problems of this or lower levels (1 - errors, 2 - warnings, 3 - notices)", 110 | "type": "number" 111 | }, 112 | "bulkProblemDiagnostics.errorMessageMatch": { 113 | "default": "", 114 | "description": "If specified, will only open files that have problems that match this setting", 115 | "type": "string" 116 | } 117 | } 118 | } 119 | }, 120 | "scripts": { 121 | "vscode:prepublish": "npm run compile", 122 | "compile": "tsc -p ./", 123 | "watch": "tsc -watch -p ./", 124 | "pretest": "npm run compile && npm run lint", 125 | "lint": "eslint src --ext ts", 126 | "test": "node ./out/test/runTest.js" 127 | }, 128 | "devDependencies": { 129 | "@types/glob": "^8.0.0", 130 | "@types/mocha": "^10.0.1", 131 | "@types/node": "16.x", 132 | "@types/vscode": "^1.75.0", 133 | "@typescript-eslint/eslint-plugin": "^5.45.0", 134 | "@typescript-eslint/parser": "^5.45.0", 135 | "@vscode/test-electron": "^2.2.0", 136 | "eslint": "^8.28.0", 137 | "mocha": "^10.1.0", 138 | "typescript": "^4.9.3" 139 | }, 140 | "dependencies": { 141 | "fast-xml-parser": "^4.1.2" 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, Uri, commands, 2 | languages, window, workspace, Tab, TabInputText, ProgressLocation, CancellationToken, Progress, Diagnostic } from 'vscode'; 3 | import { readDirectoryRecursively } from './fileutil'; 4 | 5 | let lastFolder:Uri|null = null; 6 | let lastFolderIndex:number = 0; 7 | let globWatchedFiles:{[key: string]:{[file:string]:boolean}} = {}; 8 | 9 | export function activate(context: ExtensionContext) { 10 | 11 | const commandId = 'bulk-problem-diagnostics.openAllFiles'; 12 | const commandId2 = 'bulk-problem-diagnostics.openAllFilesContinue'; 13 | setLastFolder(null); 14 | context.subscriptions.push( 15 | commands.registerCommand(commandId, (uri) => openAllFilesWithProgress(uri))); 16 | context.subscriptions.push( 17 | commands.registerCommand(commandId2, () => openAllFilesContinue())); 18 | 19 | languages.onDidChangeDiagnostics((e) => { 20 | e.uris.filter(isWatched).forEach(uri => showDocumentIfItHasProblems(uri)); 21 | }); 22 | } 23 | 24 | export function deactivate() {} 25 | 26 | async function showDocumentIfItHasProblems(uri:Uri) { 27 | const maxSeverityLevel = (workspace.getConfiguration().get('bulkProblemDiagnostics.maxSeverityLevel') ?? 2); 28 | const search = (workspace.getConfiguration().get('bulkProblemDiagnostics.errorMessageMatch') ?? ''); 29 | const diag = languages.getDiagnostics(uri) 30 | .filter((d:Diagnostic) => { 31 | if (d.severity >= maxSeverityLevel) { return false; } 32 | if (!search.length) { return true; } 33 | if (d.message.includes(search)) { return true; } 34 | if (d.code && typeof d.code === 'object' && d.code.value && `${d.code.value}`.includes(search)) { return true; } 35 | if (d.code && typeof d.code !== 'object' && `${d.code}`.includes(search)) { return true; } 36 | return false; 37 | }); 38 | if (diag.length) { 39 | unWatch(uri); 40 | await commands.executeCommand('vscode.open', uri); 41 | await commands.executeCommand('workbench.action.keepEditor', uri); 42 | return true; 43 | } 44 | return false; 45 | } 46 | 47 | function isWatched(uri:Uri) { 48 | for (let key in globWatchedFiles) { 49 | if (globWatchedFiles[key][uri.path]) { return true; } 50 | } 51 | return false; 52 | } 53 | 54 | function unWatch(uri:Uri) { 55 | for (let key in globWatchedFiles) { 56 | delete globWatchedFiles[key][uri.path]; 57 | } 58 | } 59 | 60 | function setLastFolder(uri:Uri|null, idx:number = 0) { 61 | if (uri) { 62 | lastFolder = uri; 63 | commands.executeCommand('setContext', 'bulkProblemDiagnostics.lastFolder', [lastFolder.fsPath]); 64 | commands.executeCommand('setContext', 'bulkProblemDiagnostics.hasLastFolder', true); 65 | lastFolderIndex = idx; 66 | } else { 67 | lastFolder = null; 68 | commands.executeCommand('setContext', 'bulkProblemDiagnostics.lastFolder', []); 69 | commands.executeCommand('setContext', 'bulkProblemDiagnostics.hasLastFolder', false); 70 | lastFolderIndex = 0; 71 | } 72 | } 73 | 74 | function openAllFilesContinue() { 75 | if (lastFolder) { 76 | openAllFilesWithProgress(lastFolder, true); 77 | } else { 78 | window.showErrorMessage(`No more files to open`); 79 | } 80 | } 81 | 82 | async function openAllFilesWithProgress(uri:Uri|undefined = undefined, continueLastFolder:boolean=false) { 83 | uri = uri || (workspace.workspaceFolders ? workspace.workspaceFolders[0]?.uri : undefined); 84 | if (!uri) { return; } 85 | 86 | const location = ProgressLocation.Notification, cancellable = true; 87 | let title = 'Retrieving list of files...'; 88 | let files:Array|undefined = await window.withProgress({title, location, cancellable}, 89 | async (progress, token) => uri ? await readDirectoryRecursively(uri.fsPath, token) : undefined); 90 | if (!files) { return; } 91 | 92 | const maxFiles = (workspace.getConfiguration().get('bulkProblemDiagnostics.filesLimit') ?? 200); 93 | let firstIndex = 0, lastIndex = Math.min(maxFiles, files.length); 94 | if (continueLastFolder) { 95 | if (lastFolderIndex < files.length) { 96 | firstIndex = lastFolderIndex; 97 | lastIndex = Math.min(firstIndex + maxFiles, files.length); 98 | } else { 99 | window.showErrorMessage(`No more files to open`); 100 | setLastFolder(null); 101 | return; 102 | } 103 | } 104 | 105 | commands.executeCommand('workbench.action.problems.focus'); 106 | title = `Analysing ${files.length} files`; 107 | if (files.length > maxFiles || firstIndex > 0) { 108 | title = `Analysing files ${firstIndex + 1}-${lastIndex} out of ${files.length}`; 109 | } 110 | lastIndex = await window.withProgress({ title, location, cancellable }, 111 | async (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => { 112 | return await openAllFiles(files ?? [], firstIndex, lastIndex, progress, token); 113 | }); 114 | 115 | if (files.length > lastIndex && uri) { 116 | setLastFolder(uri, lastIndex); 117 | window.showInformationMessage( 118 | `Finished analysing files ${firstIndex + 1}-${lastIndex} out of ${files.length}.`+ 119 | ` To continue select "Open all files with problems (continue)" from the Command Palette or press "Continue".`, 120 | ...["Continue"] 121 | ).then((action) => { 122 | if (action === "Continue") { 123 | openAllFilesContinue(); 124 | } 125 | }); 126 | } else { 127 | setLastFolder(null); 128 | } 129 | } 130 | 131 | async function openAllFiles(files:Array, firstIndex:number, lastIndex:number, 132 | progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken): Promise { 133 | 134 | const delay:number = workspace.getConfiguration().get('bulkProblemDiagnostics.delay') ?? 200; 135 | const waitBeforeClosing:number = workspace.getConfiguration().get('bulkProblemDiagnostics.waitBeforeClosing') ?? 3000; 136 | let watchedList = []; 137 | for (let j = firstIndex; j < lastIndex; j++) { 138 | watchedList.push(files[j]); 139 | } 140 | const watchKey = (Math.random() + 1).toString(36).substring(7); 141 | globWatchedFiles[watchKey] = watchedList.reduce((p, u) => ({...p, [u.path]: true}), {}); 142 | let i = 0, timeStart = Date.now(); 143 | for (i = 0; i < watchedList.length; i++) { 144 | if (token.isCancellationRequested) { break; } 145 | const remainingTime:number = i > 20 ? ((Date.now() - timeStart) * (lastIndex - firstIndex - i) / i) : 0; 146 | progress.report({message: remainingTime ? "Approximate time left - " + formatTimeLeft(remainingTime): '', 147 | increment: 100 / (lastIndex-firstIndex)}); 148 | await ensureDocumentAnalysed(watchedList[i], waitBeforeClosing); 149 | delay > 0 && await sleep(delay); 150 | } 151 | setTimeout(() => { delete globWatchedFiles[watchKey]; }, 15000); 152 | return i + firstIndex; 153 | } 154 | 155 | async function ensureDocumentAnalysed(uri:Uri, waitBeforeClosing:number) { 156 | if (await showDocumentIfItHasProblems(uri)) { 157 | // We already know it has problems. 158 | return; 159 | } 160 | try { 161 | if (findFileInTabGroups(uri)) { 162 | // Document found in the tab groups, switch to it to ensure the event is fired and it is analysed. 163 | await commands.executeCommand('vscode.open', uri); 164 | } else { 165 | // Open document (not as preview), wait 10 times the delay interval and close it if it has no problems. 166 | await workspace.openTextDocument(uri); // this will throw an error if the file is binary. 167 | const res = await commands.executeCommand('vscode.open', uri); // open the document in the editor and switch to it. 168 | await commands.executeCommand("workbench.action.keepEditor", uri); // make sure the document is not opened in preview mode. 169 | setTimeout(async () => { 170 | if (!(await showDocumentIfItHasProblems(uri))) { 171 | let tab:Tab|null = findFileInTabGroups(uri); 172 | tab && await window.tabGroups.close(tab); 173 | } 174 | }, waitBeforeClosing); 175 | } 176 | } catch (e) { 177 | if (!(e instanceof Error) || !e.message.includes('File seems to be binary')) { console.error(e); } 178 | }; 179 | } 180 | 181 | function sleep(ms:number) { 182 | return new Promise(resolve => setTimeout(resolve, ms)); 183 | } 184 | 185 | function findFileInTabGroups(uri:Uri):Tab|null { 186 | const tabs: Tab[] = window.tabGroups.all.map(tg => tg.tabs).flat(); 187 | const index = tabs.findIndex(tab => tab.input instanceof TabInputText && tab.input.uri.path === uri.path); 188 | if (index !== -1) { 189 | return tabs[index]; 190 | } 191 | return null; 192 | } 193 | 194 | function formatTimeLeft(ms:number):string { 195 | const f = (n:number, addZero:boolean = true):string => addZero && n < 10 ? '0' + n : '' + n; 196 | const time = [ 197 | ...(ms > 3600000 ? [f(Math.floor(ms / 3600000), false)+'h'] : []), 198 | f(Math.floor(ms / 60000) % 60, ms > 3600000), 199 | f(Math.floor(ms / 1000) % 60), 200 | ]; 201 | if (ms/60000 > 2) { 202 | // Do not show seconds if over 2 minutes left. 203 | return time.slice(0, -1).join(' ')+'m'; 204 | } 205 | return time.join(':') + 's'; 206 | } 207 | -------------------------------------------------------------------------------- /media/icon2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /media/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 41 | 43 | 54 | 65 | 76 | 79 | 83 | 87 | 88 | 91 | 95 | 99 | 100 | 103 | 107 | 111 | 112 | 115 | 119 | 123 | 124 | 135 | 144 | 155 | 165 | 175 | 185 | 195 | 204 | 213 | 222 | 231 | 240 | 249 | 258 | 267 | 276 | 285 | 294 | 305 | 306 | 310 | 317 | 321 | 325 | 329 | 334 | 342 | 346 | 354 | 358 | 362 | 367 | 372 | 377 | 382 | 386 | 400 | 404 | 408 | 412 | 416 | 420 | 424 | 428 | 432 | 439 | 443 | 444 | 445 | 446 | --------------------------------------------------------------------------------