├── .eslintrc.base.json ├── .eslintrc.browser.json ├── .eslintrc.json ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── cd-pre.yml │ ├── cd-stable.yml │ ├── codeql.yml │ ├── issues-lock.yml │ └── issues-stale.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarnrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── icon.png └── preview.gif ├── package.json ├── scripts └── applyPreReleasePatch.js ├── src ├── commands.ts ├── config.ts ├── constants.ts ├── container.ts ├── env │ ├── browser │ │ ├── hrtime.ts │ │ └── platform.ts │ └── node │ │ ├── hrtime.ts │ │ └── platform.ts ├── excludeController.ts ├── extension.ts ├── statusBarController.ts └── system │ ├── command.ts │ ├── configuration.ts │ ├── context.ts │ ├── decorators │ ├── command.ts │ ├── log.ts │ ├── memoize.ts │ └── resolver.ts │ ├── function.ts │ ├── logger.constants.ts │ ├── logger.scope.ts │ ├── logger.ts │ ├── object.ts │ ├── promise.ts │ ├── storage.ts │ ├── string.ts │ └── version.ts ├── tsconfig.base.json ├── tsconfig.browser.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintrc.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 9 | "plugin:import/recommended", 10 | "plugin:import/typescript", 11 | "prettier" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2022, 16 | "sourceType": "module", 17 | "ecmaFeatures": { 18 | "impliedStrict": true 19 | }, 20 | "project": true 21 | }, 22 | "plugins": ["anti-trojan-source", "import", "@typescript-eslint"], 23 | "reportUnusedDisableDirectives": true, 24 | "root": true, 25 | "rules": { 26 | "anti-trojan-source/no-bidi": "error", 27 | "arrow-parens": ["off"], 28 | "brace-style": ["off", "stroustrup"], 29 | "consistent-return": "off", 30 | "curly": ["error", "multi-line", "consistent"], 31 | "eol-last": "error", 32 | "linebreak-style": ["error", "unix"], 33 | "new-parens": "error", 34 | "no-console": "off", 35 | "no-constant-condition": ["warn", { "checkLoops": false }], 36 | "no-constant-binary-expression": "error", 37 | "no-caller": "error", 38 | "no-debugger": "off", 39 | "no-dupe-class-members": "off", 40 | "no-else-return": "warn", 41 | "no-empty": ["warn", { "allowEmptyCatch": true }], 42 | "no-eval": "error", 43 | "no-ex-assign": "warn", 44 | "no-extend-native": "error", 45 | "no-extra-bind": "error", 46 | "no-floating-decimal": "error", 47 | "no-implicit-coercion": "error", 48 | "no-implied-eval": "error", 49 | "no-inner-declarations": "off", 50 | "no-lone-blocks": "error", 51 | "no-lonely-if": "error", 52 | "no-loop-func": "error", 53 | "no-multi-spaces": "error", 54 | "no-restricted-globals": ["error", "process"], 55 | "no-restricted-imports": [ 56 | "error", 57 | { 58 | "paths": [ 59 | // Disallow node imports below 60 | "assert", 61 | "buffer", 62 | "child_process", 63 | "cluster", 64 | "crypto", 65 | "dgram", 66 | "dns", 67 | "domain", 68 | "events", 69 | "freelist", 70 | "fs", 71 | "http", 72 | "https", 73 | "module", 74 | "net", 75 | "os", 76 | "path", 77 | "process", 78 | "punycode", 79 | "querystring", 80 | "readline", 81 | "repl", 82 | "smalloc", 83 | "stream", 84 | "string_decoder", 85 | "sys", 86 | "timers", 87 | "tls", 88 | "tracing", 89 | "tty", 90 | "url", 91 | "util", 92 | "vm", 93 | "zlib" 94 | ], 95 | "patterns": [ 96 | { 97 | "group": ["**/env/**/*"], 98 | "message": "Use @env/ instead" 99 | }, 100 | { 101 | "group": ["src/**/*"], 102 | "message": "Use relative paths instead" 103 | } 104 | ] 105 | } 106 | ], 107 | "no-return-assign": "error", 108 | "no-return-await": "warn", 109 | "no-self-compare": "error", 110 | "no-sequences": "error", 111 | "no-template-curly-in-string": "warn", 112 | "no-throw-literal": "error", 113 | "no-unmodified-loop-condition": "warn", 114 | "no-unneeded-ternary": "error", 115 | "no-unused-expressions": "error", 116 | "no-use-before-define": "off", 117 | "no-useless-call": "error", 118 | "no-useless-catch": "error", 119 | "no-useless-computed-key": "error", 120 | "no-useless-concat": "error", 121 | "no-useless-rename": "error", 122 | "no-useless-return": "error", 123 | "no-var": "error", 124 | "no-with": "error", 125 | "object-shorthand": ["error", "never"], 126 | "one-var": ["error", "never"], 127 | "prefer-arrow-callback": "error", 128 | "prefer-const": [ 129 | "error", 130 | { 131 | "destructuring": "all", 132 | "ignoreReadBeforeAssign": true 133 | } 134 | ], 135 | "prefer-numeric-literals": "error", 136 | "prefer-object-spread": "error", 137 | "prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }], 138 | "prefer-rest-params": "error", 139 | "prefer-spread": "error", 140 | "prefer-template": "error", 141 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 142 | "require-atomic-updates": "off", 143 | "semi": ["error", "always"], 144 | "semi-style": ["error", "last"], 145 | "sort-imports": [ 146 | "error", 147 | { 148 | "ignoreCase": true, 149 | "ignoreDeclarationSort": true, 150 | "ignoreMemberSort": false, 151 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] 152 | } 153 | ], 154 | "yoda": "error", 155 | "import/consistent-type-specifier-style": ["error", "prefer-top-level"], 156 | "import/extensions": ["error", "never"], 157 | "import/newline-after-import": "warn", 158 | "import/no-absolute-path": "error", 159 | "import/no-cycle": "off", 160 | "import/no-default-export": "error", 161 | "import/no-duplicates": ["error", { "prefer-inline": false }], 162 | "import/no-dynamic-require": "error", 163 | "import/no-self-import": "error", 164 | "import/no-unresolved": ["warn", { "ignore": ["vscode", "@env"] }], 165 | "import/no-useless-path-segments": "error", 166 | "import/order": [ 167 | "warn", 168 | { 169 | "alphabetize": { 170 | "order": "asc", 171 | "orderImportKind": "asc", 172 | "caseInsensitive": true 173 | }, 174 | "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], 175 | "newlines-between": "never" 176 | } 177 | ], 178 | "@typescript-eslint/ban-types": [ 179 | "error", 180 | { 181 | "extendDefaults": false, 182 | "types": { 183 | "String": { 184 | "message": "Use string instead", 185 | "fixWith": "string" 186 | }, 187 | "Boolean": { 188 | "message": "Use boolean instead", 189 | "fixWith": "boolean" 190 | }, 191 | "Number": { 192 | "message": "Use number instead", 193 | "fixWith": "number" 194 | }, 195 | "Symbol": { 196 | "message": "Use symbol instead", 197 | "fixWith": "symbol" 198 | }, 199 | // "Function": { 200 | // "message": "The `Function` type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape." 201 | // }, 202 | "Object": { 203 | "message": "The `Object` type actually means \"any non-nullish value\", so it is marginally better than `unknown`.\n- If you want a type meaning \"any object\", you probably want `Record` instead.\n- If you want a type meaning \"any value\", you probably want `unknown` instead." 204 | }, 205 | "{}": { 206 | "message": "`{}` actually means \"any non-nullish value\".\n- If you want a type meaning \"any object\", you probably want `object` or `Record` instead.\n- If you want a type meaning \"any value\", you probably want `unknown` instead.", 207 | "fixWith": "object" 208 | } 209 | // "object": { 210 | // "message": "The `object` type is currently hard to use ([see this issue](https://github.com/microsoft/TypeScript/issues/21732)).\nConsider using `Record` instead, as it allows you to more easily inspect and use the keys." 211 | // } 212 | } 213 | } 214 | ], 215 | "@typescript-eslint/consistent-type-assertions": [ 216 | "error", 217 | { 218 | "assertionStyle": "as", 219 | "objectLiteralTypeAssertions": "allow-as-parameter" 220 | } 221 | ], 222 | "@typescript-eslint/consistent-type-imports": ["error", { "disallowTypeAnnotations": false }], 223 | "@typescript-eslint/explicit-function-return-type": "off", 224 | "@typescript-eslint/explicit-member-accessibility": "off", 225 | "@typescript-eslint/explicit-module-boundary-types": "off", // TODO@eamodio revisit 226 | "@typescript-eslint/naming-convention": [ 227 | "error", 228 | { 229 | "selector": "variable", 230 | "format": ["camelCase", "PascalCase"], 231 | "leadingUnderscore": "allow", 232 | "filter": { 233 | "regex": "^_$", 234 | "match": false 235 | } 236 | }, 237 | { 238 | "selector": "variableLike", 239 | "format": ["camelCase"], 240 | "leadingUnderscore": "allow", 241 | "filter": { 242 | "regex": "^_$", 243 | "match": false 244 | } 245 | }, 246 | { 247 | "selector": "memberLike", 248 | "modifiers": ["private"], 249 | "format": ["camelCase"], 250 | "leadingUnderscore": "allow" 251 | }, 252 | { 253 | "selector": "memberLike", 254 | "modifiers": ["private", "readonly"], 255 | "format": ["camelCase", "PascalCase"], 256 | "leadingUnderscore": "allow" 257 | }, 258 | { 259 | "selector": "memberLike", 260 | "modifiers": ["static", "readonly"], 261 | "format": ["camelCase", "PascalCase"] 262 | }, 263 | { 264 | "selector": "interface", 265 | "format": ["PascalCase"], 266 | "custom": { 267 | "regex": "^I[A-Z]", 268 | "match": false 269 | } 270 | } 271 | ], 272 | "@typescript-eslint/no-dynamic-delete": "error", 273 | "@typescript-eslint/no-empty-function": "off", 274 | "@typescript-eslint/no-empty-interface": "error", 275 | "@typescript-eslint/no-explicit-any": "off", 276 | "@typescript-eslint/no-extraneous-class": "error", 277 | "@typescript-eslint/no-floating-promises": "error", 278 | "@typescript-eslint/no-inferrable-types": ["warn", { "ignoreParameters": true, "ignoreProperties": true }], 279 | "@typescript-eslint/no-invalid-void-type": "off", // Seems to error on `void` return types 280 | "@typescript-eslint/no-meaningless-void-operator": "error", 281 | "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], 282 | "@typescript-eslint/no-namespace": "error", 283 | "@typescript-eslint/no-non-null-assertion": "off", 284 | "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error", 285 | "@typescript-eslint/no-throw-literal": "error", 286 | // "@typescript-eslint/no-unnecessary-condition": ["error", { "allowConstantLoopConditions": true }], 287 | "@typescript-eslint/no-unnecessary-condition": "off", 288 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "off", 289 | "@typescript-eslint/no-unnecessary-type-arguments": "error", 290 | "@typescript-eslint/no-unsafe-argument": "off", 291 | "@typescript-eslint/no-unsafe-assignment": "off", 292 | "@typescript-eslint/no-unsafe-call": "off", 293 | "@typescript-eslint/no-unsafe-member-access": "off", 294 | "@typescript-eslint/no-unsafe-return": "error", 295 | "@typescript-eslint/no-unused-expressions": ["warn", { "allowShortCircuit": true }], 296 | "@typescript-eslint/no-unused-vars": [ 297 | "warn", 298 | { 299 | "args": "after-used", 300 | "argsIgnorePattern": "^_", 301 | "ignoreRestSiblings": true, 302 | "varsIgnorePattern": "^_$" 303 | } 304 | ], 305 | "@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": false }], 306 | "@typescript-eslint/no-useless-constructor": "error", 307 | "@typescript-eslint/non-nullable-type-assertion-style": "error", 308 | "@typescript-eslint/prefer-for-of": "warn", 309 | "@typescript-eslint/prefer-includes": "warn", 310 | "@typescript-eslint/prefer-literal-enum-member": ["warn", { "allowBitwiseExpressions": true }], 311 | "@typescript-eslint/prefer-nullish-coalescing": "off", // warn 312 | "@typescript-eslint/prefer-optional-chain": "warn", 313 | "@typescript-eslint/prefer-reduce-type-parameter": "warn", 314 | "@typescript-eslint/restrict-template-expressions": [ 315 | "error", 316 | { "allowAny": true, "allowBoolean": true, "allowNumber": true, "allowNullish": true } 317 | ], 318 | "@typescript-eslint/unbound-method": "off", // Too many bugs right now: https://github.com/typescript-eslint/typescript-eslint/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+unbound-method 319 | "@typescript-eslint/unified-signatures": ["error", { "ignoreDifferentlyNamedParameters": true }] 320 | }, 321 | "settings": { 322 | "import/parsers": { 323 | "@typescript-eslint/parser": [".ts", ".tsx"] 324 | }, 325 | "import/resolver": { 326 | "typescript": { 327 | "alwaysTryTypes": true // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist` 328 | } 329 | } 330 | }, 331 | "ignorePatterns": ["dist/*", "out/*", "**/@types/*", "emojis.json", "tsconfig*.tsbuildinfo", "webpack.config*.js"], 332 | "overrides": [ 333 | { 334 | "files": ["src/env/node/**/*"], 335 | "rules": { 336 | "no-restricted-imports": "off" 337 | } 338 | } 339 | ] 340 | } 341 | -------------------------------------------------------------------------------- /.eslintrc.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [".eslintrc.base.json"], 3 | "env": { 4 | "worker": true 5 | }, 6 | "ignorePatterns": ["src/test/**/*", "src/env/node/**/*"], 7 | "parserOptions": { 8 | "project": "tsconfig.browser.json" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [".eslintrc.base.json"], 3 | "env": { 4 | "node": true 5 | }, 6 | "ignorePatterns": ["src/test/**/*", "src/env/browser/*"], 7 | "parserOptions": { 8 | "project": "tsconfig.json" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 7bc1c43089294f6ddc9ee05b3c3a117711a1d7d3 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @eamodio -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [eamodio] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help Toggle Excluded Files improve 3 | labels: ['bug', 'triage'] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Description 8 | description: Please provide a concise description of what you're experiencing, what you expected to happen, and any steps to reproduce the behavior. 9 | placeholder: | 10 | 1. In this environment... 11 | 2. With this config... 12 | 3. Run '...' 13 | 4. See error... 14 | validations: 15 | required: true 16 | - type: input 17 | id: Toggle Excluded Files 18 | attributes: 19 | label: Toggle Excluded Files Version 20 | description: What version of Toggle Excluded Files are you using? 21 | placeholder: 12.0.0 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: vscode 26 | attributes: 27 | label: VS Code Version 28 | description: What version of VS Code are you using? Copy from Help -> About 29 | placeholder: | 30 | Version: 1.70.0-insider (user setup) 31 | Commit: 1cd90cceddf3c413673963ab6f154d2ff294b17c 32 | Date: 2022-07-15T05:16:39.110Z 33 | Electron: 18.3.5 34 | Chromium: 100.0.4896.160 35 | Node.js: 16.13.2 36 | V8: 10.0.139.17-electron.0 37 | OS: Windows_NT x64 10.0.22622 38 | validations: 39 | required: false 40 | - type: textarea 41 | attributes: 42 | label: Logs, Screenshots, Screen Captures, etc 43 | description: | 44 | Logs? Links? References? Anything that will give us more context about the issue you are encountering! 45 | placeholder: | 46 | For intermittent issues, please enable debug logging by by setting `"toggleexcludedfiles.outputLevel": "debug"` in your `settings.json`. 47 | This will enable logging to the _Toggle Excluded_ channel in the _Output_ pane. 48 | 49 | Once enabled, please reproduce the issue, and attach the logs. 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Q & A 4 | url: https://github.com/eamodio/vscode-toggle-excluded-files/discussions?discussions_q=category%3AQ%26A 5 | about: Ask and answer questions about Toggle Excluded Files 6 | - name: Documentation 7 | url: https://github.com/eamodio/vscode-toggle-excluded-files/#readme 8 | about: Read the Toggle Excluded Files support documentation 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Toggle Excluded Files 4 | title: '' 5 | labels: feature, triage 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## ❤ Thank you for contributing to Toggle Excluded Files! ❤ 2 | 3 | ### 🚨 IMPORTANT 🚨 4 | 5 | - Please create an issue _before_ creating a Pull Request 6 | - Please use the following Git commit message style 7 | - Use future tense ("Adds feature" not "Added feature") 8 | - Use a "Fixes #xxx -" or "Closes #xxx -" prefix to auto-close the issue that your PR addresses 9 | - Limit the first line to 72 characters or less 10 | - Reference issues and pull requests liberally after the first line 11 | 12 | ## ↑👆 DELETE above _before_ submitting 👆↑ 13 | 14 | --- 15 | 16 | # Description 17 | 18 | 21 | 22 | # Checklist 23 | 24 | 25 | 26 | - [ ] I have followed the guidelines in the Contributing document 27 | - [ ] My changes follow the coding style of this project 28 | - [ ] My changes build without any errors or warnings 29 | - [ ] My changes have been formatted and linted 30 | - [ ] My changes include any required corresponding changes to the documentation (including CHANGELOG.md and README.md) 31 | - [ ] My changes have been rebased and squashed to the minimal number (typically 1) of relevant commits 32 | - [ ] My changes have a descriptive commit message with a short title, including a `Fixes $XXX -` or `Closes #XXX -` prefix to auto-close the issue that your PR addresses 33 | -------------------------------------------------------------------------------- /.github/workflows/cd-pre.yml: -------------------------------------------------------------------------------- 1 | name: Publish pre-release 2 | 3 | on: 4 | schedule: 5 | - cron: '0 9 * * *' # every day at 4am EST 6 | workflow_dispatch: 7 | 8 | jobs: 9 | check: 10 | name: Check for updates 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | status: ${{ steps.earlyexit.outputs.status }} 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | - id: earlyexit 22 | run: | 23 | git config user.name github-actions 24 | git config user.email github-actions@github.com 25 | if git rev-parse origin/pre >/dev/null 2>&1; then 26 | preRef=$(git show-ref -s origin/pre) 27 | headRef=$(git show-ref --head -s head) 28 | echo "origin/pre" 29 | echo $preRef 30 | echo "HEAD" 31 | echo $headRef 32 | if [ "$preRef" = "$headRef" ]; then 33 | echo "No changes since last pre-release build. Exiting." 34 | echo "status=unchanged" >> $GITHUB_OUTPUT 35 | exit 0 36 | else 37 | echo "Updating pre" 38 | git push origin --delete pre 39 | git checkout -b pre 40 | git push origin pre 41 | fi 42 | else 43 | echo "No pre branch. Creating." 44 | git checkout -b pre 45 | git push origin pre 46 | fi 47 | echo "status=changed" >> $GITHUB_OUTPUT 48 | 49 | publish: 50 | name: Publish pre-release 51 | needs: check 52 | runs-on: ubuntu-latest 53 | if: needs.check.outputs.status == 'changed' 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v3 57 | - name: Setup node 58 | uses: actions/setup-node@v3 59 | with: 60 | node-version: '18' 61 | - name: Install 62 | run: yarn 63 | - name: Apply pre-release patch 64 | run: yarn run patch-pre 65 | - name: Setup Environment 66 | run: node -e "console.log('PACKAGE_VERSION=' + require('./package.json').version + '\nPACKAGE_NAME=' + require('./package.json').name + '-' + require('./package.json').version)" >> $GITHUB_ENV 67 | - name: Package extension 68 | run: yarn run package --pre-release 69 | - name: Publish extension 70 | run: yarn vsce publish --yarn --pre-release --packagePath ./${{ env.PACKAGE_NAME }}.vsix -p ${{ secrets.MARKETPLACE_PAT }} 71 | - name: Publish artifact 72 | uses: actions/upload-artifact@v3 73 | with: 74 | name: ${{ env.PACKAGE_NAME }}.vsix 75 | path: ./${{ env.PACKAGE_NAME }}.vsix 76 | -------------------------------------------------------------------------------- /.github/workflows/cd-stable.yml: -------------------------------------------------------------------------------- 1 | name: Publish Stable 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | build: 10 | name: Publish Stable 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Setup node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '18' 21 | - name: Setup Environment 22 | run: node -e "console.log('PACKAGE_VERSION=' + require('./package.json').version + '\nPACKAGE_NAME=' + require('./package.json').name + '-' + require('./package.json').version)" >> $GITHUB_ENV 23 | - name: Verify versions 24 | run: node -e "if ('refs/tags/v' + '${{ env.PACKAGE_VERSION }}' !== '${{ github.ref }}') { console.log('::error' + 'Version Mismatch. refs/tags/v' + '${{ env.PACKAGE_VERSION }}', '${{ github.ref }}'); throw Error('Version Mismatch')} " 25 | - name: Install 26 | run: yarn 27 | - name: Package extension 28 | run: yarn run package 29 | - name: Publish Extension 30 | run: yarn vsce publish --yarn --packagePath ./${{ env.PACKAGE_NAME }}.vsix -p ${{ secrets.MARKETPLACE_PAT }} 31 | - name: Generate Changelog 32 | id: changelog 33 | uses: mindsers/changelog-reader-action@v2 34 | with: 35 | version: ${{ env.PACKAGE_VERSION }} 36 | path: ./CHANGELOG.md 37 | - name: Create GitHub release 38 | id: create_release 39 | uses: softprops/action-gh-release@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | tag_name: ${{ github.ref }} 44 | name: v${{ env.PACKAGE_VERSION }} 45 | body: ${{ steps.changelog.outputs.changes }} 46 | draft: false 47 | prerelease: false 48 | files: ./${{ env.PACKAGE_NAME }}.vsix 49 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | # branches: [ main ] 20 | schedule: 21 | - cron: '43 22 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | # - name: Autobuild 56 | # uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.github/workflows/issues-lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock closed issues' 2 | 3 | on: 4 | schedule: 5 | - cron: '30 5 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | steps: 14 | - uses: dessant/lock-threads@v4 15 | with: 16 | github-token: ${{ secrets.GITHUB_TOKEN }} 17 | issue-comment: 'This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.' 18 | issue-inactive-days: 30 19 | process-only: 'issues' 20 | -------------------------------------------------------------------------------- /.github/workflows/issues-stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | 3 | on: 4 | schedule: 5 | - cron: '00 5 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | steps: 15 | - uses: actions/stale@v7 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | days-before-close: 7 19 | days-before-stale: 7 20 | only-labels: 'needs-more-info' 21 | exempt-issue-labels: 'triage' 22 | close-issue-message: 'Closing this issue because it needs more information and has not had recent activity. Please re-open this issue if more details can be provided. Thanks!' 23 | stale-issue-label: 'inactive' 24 | stale-issue-message: 'This issue needs more information and has not had recent activity. Please provide the missing information or it will be closed in 7 days. Thanks!' 25 | labels-to-add-when-unstale: 'triage' 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eslintcache* 2 | .vscode-clean/** 3 | .vscode-test/** 4 | .vscode-test-web/** 5 | dist 6 | out 7 | node_modules 8 | *.vsix 9 | tsconfig*.tsbuildinfo 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "lf", 4 | "trailingComma": "all", 5 | "printWidth": 120, 6 | "singleQuote": true, 7 | "tabWidth": 4, 8 | "useTabs": true, 9 | "overrides": [ 10 | { 11 | "files": ".prettierrc", 12 | "options": { "parser": "json" } 13 | }, 14 | { 15 | "files": "*.md", 16 | "options": { "tabWidth": 2 } 17 | }, 18 | { 19 | "files": "*.svg", 20 | "options": { "parser": "html" } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Run", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 11 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 12 | "presentation": { 13 | "group": "2_run", 14 | "order": 1 15 | }, 16 | "pauseForSourceMap": true, 17 | "skipFiles": ["/**", "**/node_modules/**", "**/resources/app/out/vs/**"], 18 | "smartStep": true, 19 | "sourceMapRenames": true, 20 | "sourceMaps": true 21 | }, 22 | { 23 | "name": "Watch & Run", 24 | "type": "extensionHost", 25 | "request": "launch", 26 | "runtimeExecutable": "${execPath}", 27 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 28 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 29 | "pauseForSourceMap": true, 30 | "preLaunchTask": "${defaultBuildTask}", 31 | "presentation": { 32 | "group": "1_watch", 33 | "order": 1 34 | }, 35 | "skipFiles": ["/**", "**/node_modules/**", "**/resources/app/out/vs/**"], 36 | "smartStep": true, 37 | "sourceMapRenames": true, 38 | "sourceMaps": true 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "eslint.packageManager": "yarn", 6 | "files.associations": { 7 | ".eslintrc*.json": "jsonc" 8 | }, 9 | "files.exclude": { 10 | "**/.vscode-test": true, 11 | "**/.vscode-test-web": true 12 | }, 13 | "files.insertFinalNewline": true, 14 | "files.trimTrailingWhitespace": true, 15 | "githubIssues.queries": [ 16 | { 17 | "label": "Triage", 18 | "query": "state:open repo:${owner}/${repository} label:triage sort:updated-desc" 19 | }, 20 | { 21 | "label": "Current", 22 | "query": "state:open repo:${owner}/${repository} milestone:\"January 2023\" sort:updated-desc" 23 | }, 24 | { 25 | "label": "Soon™", 26 | "query": "state:open repo:${owner}/${repository} milestone:Soon™ sort:updated-desc" 27 | }, 28 | { 29 | "label": "Verify", 30 | "query": "state:closed repo:${owner}/${repository} -milestone:Shipped label:pending-release label:needs-verification sort:updated-desc" 31 | }, 32 | { 33 | "label": "Pending Release", 34 | "query": "state:closed repo:${owner}/${repository} label:pending-release sort:updated-desc" 35 | }, 36 | { 37 | "label": "All", 38 | "query": "state:open repo:${owner}/${repository} sort:updated-desc" 39 | } 40 | ], 41 | "[html][javascript][json][jsonc][markdown][scss][svg][typescript][typescriptreact]": { 42 | "editor.defaultFormatter": "esbenp.prettier-vscode" 43 | }, 44 | "npm.packageManager": "yarn", 45 | "search.exclude": { 46 | "**/dist": true 47 | }, 48 | "typescript.preferences.importModuleSpecifier": "project-relative", 49 | "typescript.tsdk": "node_modules\\typescript\\lib" 50 | } 51 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | // A task runner that calls a custom npm script that compiles the extension. 9 | { 10 | "version": "2.0.0", 11 | "presentation": { 12 | "echo": false, 13 | "reveal": "always", 14 | "focus": false, 15 | "panel": "dedicated", 16 | "showReuseMessage": false 17 | }, 18 | "tasks": [ 19 | { 20 | "type": "npm", 21 | "script": "build", 22 | "group": "build", 23 | "problemMatcher": ["$ts-checker-webpack", "$ts-checker-eslint-webpack"] 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "lint", 28 | "group": "build", 29 | "problemMatcher": ["$eslint-stylish"] 30 | }, 31 | { 32 | "type": "npm", 33 | "script": "watch", 34 | "group": { 35 | "kind": "build", 36 | "isDefault": true 37 | }, 38 | "isBackground": true, 39 | "problemMatcher": ["$ts-checker-webpack-watch", "$ts-checker-eslint-webpack-watch"] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | .vscode/** 3 | .vscode-clean/** 4 | .vscode-test/** 5 | .vscode-test-web/** 6 | .yarn/** 7 | !images/icon.png 8 | images/** 9 | node_modules/** 10 | out/** 11 | patches/** 12 | scripts/** 13 | src/** 14 | test/** 15 | **/*.fig 16 | **/*.map 17 | **/*.pdn 18 | **/*.js.LICENSE.txt 19 | .eslintcache 20 | .eslintignore 21 | .eslintrc*.json 22 | .git-blame-ignore-revs 23 | .gitattributes 24 | .gitignore 25 | .mailmap 26 | .prettierignore 27 | .prettierrc 28 | .yarnrc 29 | CODE_OF_CONDUCT.md 30 | CONTRIBUTING.md 31 | README.pre.md 32 | tsconfig*.json 33 | tsconfig*.tsbuildinfo 34 | webpack.config*.js 35 | yarn.lock 36 | .DS_Store 37 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-engines true 2 | version-git-message "Bumps to v%s" 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [2.0.0] - 2023-03-05 10 | 11 | ### Added 12 | 13 | - Adds toggle button to the Explorer view — closes [#44](https://github.com/eamodio/vscode-toggle-excluded-files/issues/44) 14 | - Adds support for VS Code on the Web (vscode.dev / github.dev) 15 | - Adds workspace trust support 16 | 17 | ## [1.7.0] - 2019-10-18 18 | 19 | ### Fixed 20 | 21 | - Fixes [#11](https://github.com/eamodio/vscode-toggle-excluded-files/issues/11) - Doesn't re-hide anything 22 | - Fixes [#8](https://github.com/eamodio/vscode-toggle-excluded-files/issues/8) - Extension does not work when Workspace has multiple folders 23 | 24 | ## [1.6.0] 25 | 26 | ### Removed 27 | 28 | - Removes hack now that [vscode #25508](https://github.com/Microsoft/vscode/issues/25508) as been resolved 29 | 30 | ## [1.5.1] 31 | 32 | ### Fixed 33 | 34 | - Fixes marketplace badge layout 35 | 36 | ## [1.5.0] 37 | 38 | ### Added 39 | 40 | - Adds current toggle state (`Show` vs `Restore`) to the status bar button 41 | - Adds folder (workspace) detection to disable of Toggle Exclude Files if a folder (workspace) is not loaded 42 | 43 | ### Changed 44 | 45 | - Changes status bar button to only appear if `files.exclude` is in use (either in user or workspace settings) 46 | - Changes shortcut key for the `Toggle Excluded Files` command to only work when the file explorer is focused 47 | - Renames `toggleexcludedfiles.advanced.debug` setting to `toggleexcludedfiles.debug` 48 | - Renames `toggleexcludedfiles.output.level` setting to `toggleexcludedfiles.outputLevel` 49 | 50 | ### Fixed 51 | 52 | - Fixes intermittent issue where restoring existing exclude rules failed 53 | 54 | ## [1.1.3] 55 | 56 | ### Fixed 57 | 58 | - Fixes issue with output channel logging 59 | 60 | ## [1.1.2] 61 | 62 | ### Fixed 63 | 64 | - Fixes intermittent issue with restoring the previous exclude rules 65 | 66 | ## [1.1.1] 67 | 68 | ### Fixed 69 | 70 | - Fixes logging to clean up on extension deactivate 71 | 72 | ## [1.1.0] 73 | 74 | ### Changed 75 | 76 | - Completely refactored to save & restore existing exclude rules, even if they change while the toggle is active 77 | 78 | ### Fixed 79 | 80 | - Fixes [#1](https://github.com/eamodio/vscode-toggle-excluded-files/issues/1) - Doesn't restore the original rules 81 | 82 | ## [1.0.1] 83 | 84 | ### Changed 85 | 86 | - Updates the README and some dependencies 87 | 88 | ## [1.0.0] 89 | 90 | ### Changed 91 | 92 | - No longer preview since vscode 1.8 has been released 93 | 94 | ## [0.0.1] 95 | 96 | ### Added 97 | 98 | - Initial release 99 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eamodio@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | When contributing to this project, please first discuss the changes you wish to make via an issue before making changes. 6 | 7 | Please note the [Code of Conduct](CODE_OF_CONDUCT.md) document, please follow it in all your interactions with this project. 8 | 9 | ## Your First Code Contribution 10 | 11 | Unsure where to begin contributing? You can start by looking through the [`help-wanted`](https://github.com/eamodio/vscode-toggle-excluded-files/labels/help%20wanted) issues. 12 | 13 | ### Getting the code 14 | 15 | ``` 16 | git clone https://github.com/eamodio/vscode-toggle-excluded-files.git 17 | ``` 18 | 19 | Prerequisites 20 | 21 | - [Git](https://git-scm.com/), `>= 2.7.2` 22 | - [NodeJS](https://nodejs.org/), `>= 16.14.2` 23 | - [yarn](https://yarnpkg.com/), `>= 1.22.19` 24 | 25 | ### Dependencies 26 | 27 | From a terminal, where you have cloned the repository, execute the following command to install the required dependencies: 28 | 29 | ``` 30 | yarn 31 | ``` 32 | 33 | ### Build 34 | 35 | From a terminal, where you have cloned the repository, execute the following command to re-build the project from scratch: 36 | 37 | ``` 38 | yarn run rebuild 39 | ``` 40 | 41 | 👉 **NOTE!** This will run a complete rebuild of the project. 42 | 43 | Or to just run a quick build, use: 44 | 45 | ``` 46 | yarn run build 47 | ``` 48 | 49 | ### Watch 50 | 51 | During development you can use a watcher to make builds on changes quick and easy. From a terminal, where you have cloned the repository, execute the following command: 52 | 53 | ``` 54 | yarn run watch 55 | ``` 56 | 57 | Or use the provided `watch` task in VS Code, execute the following from the command palette (be sure there is no `>` at the start): 58 | 59 | ``` 60 | task watch 61 | ``` 62 | 63 | This will first do an initial full build and then watch for file changes, compiling those changes incrementally, enabling a fast, iterative coding experience. 64 | 65 | 👉 **Tip!** You can press CMD+SHIFT+B (CTRL+SHIFT+B on Windows, Linux) to start the watch task. 66 | 67 | 👉 **Tip!** You don't need to stop and restart the development version of Code after each change. You can just execute `Reload Window` from the command palette. 68 | 69 | ### Formatting 70 | 71 | This project uses [prettier](https://prettier.io/) for code formatting. You can run prettier across the code by calling `yarn run pretty` from a terminal. 72 | 73 | To format the code as you make changes you can install the [Prettier - Code formatter](https://marketplace.visualstudio.com/items/esbenp.prettier-vscode) extension. 74 | 75 | Add the following to your User Settings to run prettier: 76 | 77 | ``` 78 | "editor.formatOnSave": true, 79 | ``` 80 | 81 | ### Linting 82 | 83 | This project uses [ESLint](https://eslint.org/) for code linting. You can run ESLint across the code by calling `yarn run lint` from a terminal. Warnings from ESLint show up in the `Errors and Warnings` quick box and you can navigate to them from inside VS Code. 84 | 85 | To lint the code as you make changes you can install the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) extension. 86 | 87 | ### Bundling 88 | 89 | To generate a production bundle (without packaging) run the following from a terminal: 90 | 91 | ``` 92 | yarn run bundle 93 | ``` 94 | 95 | To generate a VSIX (installation package) run the following from a terminal: 96 | 97 | ``` 98 | yarn run package 99 | ``` 100 | 101 | ### Debugging 102 | 103 | #### Using VS Code (desktop) 104 | 105 | 1. Open the `vscode-toggle-excluded-files` folder 106 | 2. Ensure the required [dependencies](#dependencies) are installed 107 | 3. Choose the `Watch & Run` launch configuration from the launch dropdown in the Run and Debug viewlet and press `F5`. 108 | 109 | #### Using VS Code (desktop webworker) 110 | 111 | 1. Open the `vscode-toggle-excluded-files` folder 112 | 2. Ensure the required [dependencies](#dependencies) are installed 113 | 3. Choose the `Watch & Run (web)` launch configuration from the launch dropdown in the Run and Debug viewlet and press `F5`. 114 | 115 | #### Using VS Code for the Web (locally) 116 | 117 | See https://code.visualstudio.com/api/extension-guides/web-extensions#test-your-web-extension-in-a-browser-using-vscodetestweb 118 | 119 | 1. Open the `vscode-toggle-excluded-files` folder 120 | 2. Ensure the required [dependencies](#dependencies) are installed 121 | 3. Run the `build` or `watch` task from the command palette 122 | 4. Run the `Run (local web)` task from the command palette 123 | 124 | #### Using VS Code for the Web (vscode.dev) 125 | 126 | See https://code.visualstudio.com/api/extension-guides/web-extensions#test-your-web-extension-in-on-vscode.dev 127 | 128 | 1. Open the `vscode-toggle-excluded-files` folder 129 | 2. Ensure the required [dependencies](#dependencies) are installed 130 | 3. Run the `build` or `watch` task from the command palette 131 | 4. Run the `Run (vscode.dev)` task from the command palette 132 | 133 | ## Submitting a Pull Request 134 | 135 | Please follow all the instructions in the [PR template](.github/PULL_REQUEST_TEMPLATE.md). 136 | 137 | ### Update the CHANGELOG 138 | 139 | The [Change Log](CHANGELOG.md) is updated manually and an entry should be added for each change. Changes are grouped in lists by `added`, `changed` or `fixed`. 140 | 141 | Entries should be written in future tense: 142 | 143 | - Be sure to give yourself much deserved credit by adding your name and user in the entry 144 | 145 | > Added 146 | > 147 | > - Adds awesome feature — closes [#\](https://github.com/eamodio/vscode-toggle-excluded-files/issues/) thanks to [PR #\](https://github.com/eamodio/vscode-toggle-excluded-files/issues/) by Your Name ([@\](https://github.com/)) 148 | > 149 | > Changed 150 | > 151 | > - Changes or improves an existing feature — closes [#\](https://github.com/eamodio/vscode-toggle-excluded-files/issues/) thanks to [PR #\](https://github.com/eamodio/vscode-toggle-excluded-files/issues/) by Your Name ([@\](https://github.com/)) 152 | > 153 | > Fixed 154 | > 155 | > - Fixes [#\](https://github.com/eamodio/vscode-toggle-excluded-files/issues/) a bug or regression — thanks to [PR #\](https://github.com/eamodio/vscode-toggle-excluded-files/issues/) by Your Name ([@\](https://github.com/)) 156 | 157 | ### Update the README 158 | 159 | If this is your first contribution to Toggle Excluded Files, please give yourself credit by adding yourself to the `Contributors` section of the [README](README.md#contributors-) in the following format: 160 | 161 | > - `Your Name ([@](https://github.com/)) — [contributions](https://github.com/eamodio/vscode-toggle-excluded-files/commits?author=)` 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2023 Eric Amodio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://vsmarketplacebadges.dev/version-short/amodio.toggle-excluded-files.svg)](https://marketplace.visualstudio.com/items?itemName=amodio.toggle-excluded-files) 2 | [![](https://vsmarketplacebadges.dev/downloads-short/amodio.toggle-excluded-files.svg)](https://marketplace.visualstudio.com/items?itemName=amodio.toggle-excluded-files) 3 | [![](https://vsmarketplacebadges.dev/rating-short/amodio.toggle-excluded-files.svg)](https://marketplace.visualstudio.com/items?itemName=amodio.toggle-excluded-files) 4 | [![](https://img.shields.io/badge/vscode--dev--community-toggle--excluded--files-blue.svg?logo=slack&labelColor=555555)](https://vscode-slack.amod.io) 5 | 6 | # Toggle Excluded Files 7 | 8 | Quickly toggles excluded (hidden) files visibility in the file explorer. 9 | 10 | > Excluded files are configured in your [`settings.json`](https://code.visualstudio.com/docs/getstarted/settings#_copy-of-default-settings) 11 | > 12 | > ```json 13 | > "files.exclude": { 14 | > "node_modules": true, 15 | > "out": true 16 | > } 17 | > ``` 18 | 19 | ![preview](https://raw.githubusercontent.com/eamodio/vscode-toggle-excluded-files/master/images/preview.gif) 20 | 21 | ## Features 22 | 23 | - Adds a `Toggle Excluded Files` command (`toggleexcludedfiles.toggle`) with a shortcut of `ctrl+shift+a` (`cmd+shift+a` on macOS) to either show or restore the current visibility of excluded files in the file explorer 24 | 25 | - Adds a **Explorer view button** to toggle the excluded file visibility ([optional](#extension-settings), on by default) 26 | - Adds a **status bar button** to toggle the excluded file visibility ([optional](#extension-settings), on by default) 27 | 28 | - An indicator icon will show when the exclude visibility is currently toggled 29 | 30 | - Adds a `Show Excluded Files` command (`toggleexcludedfiles.show`) to show excluded files in the file explorer 31 | 32 | - Adds a `Hide Excluded Files` command (`toggleexcludedfiles.restore`) to hide (restore) excluded files in the file explorer 33 | 34 | ## Extension Settings 35 | 36 | | Name | Description | 37 | | --------------------------------------- | ---------------------------------------------------------------- | 38 | | `toggleexcludedfiles.explorer.enabled` | Specifies whether to show the toggle button in the Explorer view | 39 | | `toggleexcludedfiles.statusBar.enabled` | Specifies whether to show the toggle button in the status bar | 40 | 41 | ## Known Issues 42 | 43 | None 44 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamodio/vscode-toggle-excluded-files/98a87df11afcbd5cb7b9ba23d9704ccbcda376a2/images/icon.png -------------------------------------------------------------------------------- /images/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamodio/vscode-toggle-excluded-files/98a87df11afcbd5cb7b9ba23d9704ccbcda376a2/images/preview.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toggle-excluded-files", 3 | "displayName": "Toggle Excluded Files", 4 | "description": "Quickly toggles excluded (hidden) files visibility in the file explorer", 5 | "version": "2.0.0", 6 | "engines": { 7 | "vscode": "^1.72.0" 8 | }, 9 | "license": "SEE LICENSE IN LICENSE", 10 | "publisher": "amodio", 11 | "author": { 12 | "name": "Eric Amodio", 13 | "email": "eamodio@gmail.com" 14 | }, 15 | "homepage": "https://github.com/eamodio/vscode-toggle-excluded-files/blob/master/README.md", 16 | "bugs": { 17 | "url": "https://github.com/eamodio/vscode-toggle-excluded-files/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/eamodio/vscode-toggle-excluded-files.git" 22 | }, 23 | "main": "./dist/toggle-excluded.js", 24 | "browser": "./dist/browser/toggle-excluded.js", 25 | "icon": "images/icon.png", 26 | "preview": false, 27 | "badges": [ 28 | { 29 | "url": "https://img.shields.io/badge/vscode--dev--community-toggle--excluded--files-blue.svg?logo=slack&labelColor=555555", 30 | "href": "https://vscode-slack.amod.io", 31 | "description": "Join us in the #toggle-excluded-files channel" 32 | } 33 | ], 34 | "sponsor": { 35 | "url": "https://github.com/sponsors/eamodio" 36 | }, 37 | "categories": [ 38 | "Other" 39 | ], 40 | "galleryBanner": { 41 | "color": "#56098c", 42 | "theme": "dark" 43 | }, 44 | "keywords": [ 45 | "explorer", 46 | "files", 47 | "hidden", 48 | "exclude", 49 | "show", 50 | "toggle" 51 | ], 52 | "activationEvents": [ 53 | "onStartupFinished" 54 | ], 55 | "capabilities": { 56 | "untrustedWorkspaces": { 57 | "supported": true 58 | } 59 | }, 60 | "contributes": { 61 | "configuration": [ 62 | { 63 | "id": "toggleexcludedfiles", 64 | "title": "Toggle Excluded Files", 65 | "order": 10, 66 | "properties": { 67 | "toggleexcludedfiles.explorer.enabled": { 68 | "type": "boolean", 69 | "default": true, 70 | "markdownDescription": "Specifies whether to show the toggle button in the Explorer view", 71 | "scope": "window" 72 | }, 73 | "toggleexcludedfiles.statusBar.enabled": { 74 | "type": "boolean", 75 | "default": true, 76 | "markdownDescription": "Specifies whether to show the toggle button in the status bar", 77 | "scope": "window" 78 | }, 79 | "toggleexcludedfiles.outputLevel": { 80 | "type": "string", 81 | "default": "errors", 82 | "enum": [ 83 | "silent", 84 | "errors", 85 | "verbose", 86 | "debug" 87 | ], 88 | "enumDescriptions": [ 89 | "Logs nothing", 90 | "Logs only errors", 91 | "Logs all errors, warnings, and messages", 92 | "Logs all errors, warnings, and messages with extra context useful for debugging" 93 | ], 94 | "markdownDescription": "Specifies how much (if any) output will be sent to the _Toggle Excluded_ output channel", 95 | "scope": "window" 96 | } 97 | } 98 | } 99 | ], 100 | "commands": [ 101 | { 102 | "command": "toggleexcludedfiles.show", 103 | "title": "Show Excluded Files", 104 | "category": "Files", 105 | "icon": "$(eye)" 106 | }, 107 | { 108 | "command": "toggleexcludedfiles.restore", 109 | "title": "Hide Excluded Files", 110 | "category": "Files", 111 | "icon": "$(eye-closed)" 112 | }, 113 | { 114 | "command": "toggleexcludedfiles.toggle", 115 | "title": "Toggle Excluded Files", 116 | "category": "Files" 117 | } 118 | ], 119 | "keybindings": [ 120 | { 121 | "command": "toggleexcludedfiles.toggle", 122 | "key": "ctrl+shift+a", 123 | "mac": "cmd+shift+a", 124 | "when": "filesExplorerFocus" 125 | } 126 | ], 127 | "menus": { 128 | "commandPalette": [ 129 | { 130 | "command": "toggleexcludedfiles.show", 131 | "when": "toggleexcludedfiles:loaded && !toggleexcludedfiles:toggled" 132 | }, 133 | { 134 | "command": "toggleexcludedfiles.restore", 135 | "when": "toggleexcludedfiles:loaded && toggleexcludedfiles:toggled" 136 | } 137 | ], 138 | "view/title": [ 139 | { 140 | "command": "toggleexcludedfiles.show", 141 | "group": "navigation@21", 142 | "when": "view == 'workbench.explorer.fileView' && toggleexcludedfiles:loaded && !toggleexcludedfiles:toggled && config.toggleexcludedfiles.explorer.enabled" 143 | }, 144 | { 145 | "command": "toggleexcludedfiles.restore", 146 | "group": "navigation@21", 147 | "when": "view == 'workbench.explorer.fileView' && toggleexcludedfiles:loaded && toggleexcludedfiles:toggled && config.toggleexcludedfiles.explorer.enabled" 148 | } 149 | ] 150 | } 151 | }, 152 | "scripts": { 153 | "analyze:bundle": "webpack --mode production --env analyzeBundle", 154 | "analyze:deps": "webpack --env analyzeDeps", 155 | "build": "webpack --mode development", 156 | "bundle": "webpack --mode production", 157 | "clean": "npx rimraf dist out .vscode-test .vscode-test-web .eslintcache* tsconfig*.tsbuildinfo", 158 | "lint": "eslint \"src/**/*.ts?(x)\" --fix", 159 | "package": "vsce package --yarn", 160 | "package-pre": "yarn run patch-pre && yarn run package --pre-release", 161 | "patch-pre": "node ./scripts/applyPreReleasePatch.js", 162 | "pretty": "prettier --config .prettierrc --loglevel warn --write .", 163 | "pub": "vsce publish --yarn", 164 | "pub-pre": "vsce publish --yarn --pre-release", 165 | "rebuild": "yarn run reset && yarn run build", 166 | "reset": "yarn run clean && yarn --frozen-lockfile", 167 | "watch": "webpack --watch --mode development", 168 | "web": "vscode-test-web --extensionDevelopmentPath=. --folder-uri=vscode-vfs://github/eamodio/vscode-toggle-excluded-files", 169 | "web:serve": "npx serve --cors -l 5000", 170 | "web:tunnel": "npx localtunnel -p 5000", 171 | "update-dts": "pushd \"src/@types\" && npx vscode-dts dev && popd", 172 | "update-dts:master": "pushd \"src/@types\" && npx vscode-dts master && popd", 173 | "vscode:prepublish": "yarn run bundle" 174 | }, 175 | "devDependencies": { 176 | "@types/node": "16.11.47", 177 | "@types/vscode": "1.72.0", 178 | "@typescript-eslint/eslint-plugin": "5.57.1", 179 | "@typescript-eslint/parser": "5.57.1", 180 | "@vscode/vsce": "2.18.0", 181 | "circular-dependency-plugin": "5.2.2", 182 | "clean-webpack-plugin": "4.0.0", 183 | "esbuild": "0.17.15", 184 | "esbuild-loader": "3.0.1", 185 | "eslint": "8.38.0", 186 | "eslint-cli": "1.1.1", 187 | "eslint-config-prettier": "8.8.0", 188 | "eslint-import-resolver-typescript": "3.5.5", 189 | "eslint-plugin-anti-trojan-source": "1.1.1", 190 | "eslint-plugin-import": "2.27.5", 191 | "fork-ts-checker-webpack-plugin": "6.5.3", 192 | "prettier": "2.8.7", 193 | "terser-webpack-plugin": "5.3.7", 194 | "ts-loader": "9.4.2", 195 | "typescript": "5.0.4", 196 | "webpack": "5.78.0", 197 | "webpack-bundle-analyzer": "4.8.0", 198 | "webpack-cli": "5.0.1" 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /scripts/applyPreReleasePatch.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs'); 3 | 4 | // // Patch README 5 | // const insert = fs.readFileSync('./README.pre.md', { encoding: 'utf8' }); 6 | // if (insert.trim().length !== 0) { 7 | // const data = fs.readFileSync('./README.md', { encoding: 'utf8' }); 8 | // fs.writeFileSync('./README.md', `${insert}\n${data}`); 9 | // } 10 | 11 | // Patch package.json 12 | const date = new Date(new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })); 13 | let packageJSON = require('../package.json'); 14 | 15 | packageJSON = JSON.stringify( 16 | { 17 | ...packageJSON, 18 | version: `${String(date.getFullYear())}.${date.getMonth() + 1}.${date.getDate()}${String( 19 | date.getHours(), 20 | ).padStart(2, '0')}`, 21 | }, 22 | undefined, 23 | '\t', 24 | ); 25 | packageJSON += '\n'; 26 | 27 | fs.writeFileSync('./package.json', packageJSON); 28 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | import type { PaletteCommands } from './constants'; 3 | import type { Container } from './container'; 4 | import { registerCommand } from './system/command'; 5 | import type { Command } from './system/decorators/command'; 6 | import { createCommandDecorator } from './system/decorators/command'; 7 | 8 | const registrableCommands: Command[] = []; 9 | const command = createCommandDecorator(registrableCommands); 10 | 11 | export class CommandProvider implements Disposable { 12 | private readonly _disposable: Disposable; 13 | 14 | constructor(private readonly container: Container) { 15 | this._disposable = Disposable.from( 16 | ...registrableCommands.map(({ name, method }) => registerCommand(name, method, this)), 17 | ); 18 | } 19 | 20 | dispose() { 21 | this._disposable.dispose(); 22 | } 23 | 24 | @command('restore') 25 | restore() { 26 | return this.container.filesExclude.restoreConfiguration(); 27 | } 28 | 29 | @command('show') 30 | show() { 31 | return this.container.filesExclude.applyConfiguration(); 32 | } 33 | 34 | @command('toggle') 35 | toggle() { 36 | return this.container.filesExclude.toggleConfiguration(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from './system/logger.constants'; 2 | 3 | export enum OutputLevel { 4 | Silent = 'silent', 5 | Errors = 'errors', 6 | Verbose = 'verbose', 7 | Debug = 'debug', 8 | } 9 | 10 | export interface Config { 11 | explorer: { 12 | enabled: boolean; 13 | }; 14 | outputLevel: OutputLevel; 15 | statusBar: { 16 | enabled: boolean; 17 | }; 18 | } 19 | 20 | export function fromOutputLevel(level: LogLevel | OutputLevel): LogLevel { 21 | switch (level) { 22 | case OutputLevel.Silent: 23 | return LogLevel.Off; 24 | case OutputLevel.Errors: 25 | return LogLevel.Error; 26 | case OutputLevel.Verbose: 27 | return LogLevel.Info; 28 | case OutputLevel.Debug: 29 | return LogLevel.Debug; 30 | default: 31 | return level; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { FilesExcludeConfiguration } from './excludeController'; 2 | import type { configuration } from './system/configuration'; 3 | 4 | export const extensionPrefix = 'toggleexcludedfiles'; 5 | 6 | type StripPrefix = Key extends `${Prefix}${infer Rest}` ? Rest : never; 7 | 8 | export type PaletteCommands = { 9 | 'toggleexcludedfiles.restore': []; 10 | 'toggleexcludedfiles.show': []; 11 | 'toggleexcludedfiles.toggle': []; 12 | }; 13 | 14 | export type Commands = PaletteCommands; 15 | export type UnqualifiedPaletteCommands = StripPrefix; 16 | 17 | export type ContextKeys = `${typeof extensionPrefix}:loaded` | `${typeof extensionPrefix}:toggled`; 18 | 19 | export type CoreCommands = 'vscode.open' | 'setContext'; 20 | 21 | export type CoreConfiguration = 'files.exclude'; 22 | 23 | export type SecretKeys = never; 24 | 25 | export type DeprecatedGlobalStorage = object; 26 | 27 | export type GlobalStorage = object; 28 | 29 | export type DeprecatedWorkspaceStorage = object; 30 | 31 | export type WorkspaceStorage = { 32 | appliedState: StoredFilesExcludes; 33 | savedState: StoredFilesExcludes; 34 | }; 35 | 36 | type ConfigInspect = ReturnType>; 37 | export type StoredFilesExcludes = ConfigInspect; 38 | -------------------------------------------------------------------------------- /src/container.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigurationChangeEvent, ExtensionContext } from 'vscode'; 2 | import { CommandProvider } from './commands'; 3 | import { fromOutputLevel } from './config'; 4 | import { FilesExcludeController } from './excludeController'; 5 | import { StatusBarController } from './statusBarController'; 6 | import { configuration } from './system/configuration'; 7 | import { Logger } from './system/logger'; 8 | import { Storage } from './system/storage'; 9 | 10 | export class Container { 11 | static #instance: Container | undefined; 12 | static #proxy = new Proxy({} as Container, { 13 | get: function (target, prop) { 14 | // In case anyone has cached this instance 15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 16 | if (Container.#instance != null) return (Container.#instance as any)[prop]; 17 | 18 | // Allow access to config before we are initialized 19 | if (prop === 'config') return configuration.getAll(); 20 | 21 | // debugger; 22 | throw new Error('Container is not initialized'); 23 | }, 24 | }); 25 | 26 | static create(context: ExtensionContext) { 27 | if (Container.#instance != null) throw new Error('Container is already initialized'); 28 | 29 | Container.#instance = new Container(context); 30 | return Container.#instance; 31 | } 32 | 33 | static get instance(): Container { 34 | return Container.#instance ?? Container.#proxy; 35 | } 36 | 37 | private constructor(context: ExtensionContext) { 38 | this._context = context; 39 | 40 | const disposables = [ 41 | (this._storage = new Storage(context)), 42 | (this._filesExcludeController = new FilesExcludeController(this, this._storage)), 43 | (this._statusBar = new StatusBarController(this)), 44 | new CommandProvider(this), 45 | configuration.onDidChangeAny(this.onAnyConfigurationChanged, this), 46 | ]; 47 | 48 | context.subscriptions.push({ 49 | dispose: function () { 50 | disposables.reverse().forEach(d => void d.dispose()); 51 | }, 52 | }); 53 | } 54 | 55 | private _context: ExtensionContext; 56 | get context() { 57 | return this._context; 58 | } 59 | 60 | private _filesExcludeController: FilesExcludeController; 61 | get filesExclude() { 62 | return this._filesExcludeController; 63 | } 64 | 65 | private _statusBar: StatusBarController; 66 | get statusBar() { 67 | return this._statusBar; 68 | } 69 | 70 | private _storage: Storage; 71 | get storage() { 72 | return this._storage; 73 | } 74 | 75 | private onAnyConfigurationChanged(e: ConfigurationChangeEvent) { 76 | if (configuration.changed(e, 'outputLevel')) { 77 | Logger.logLevel = fromOutputLevel(configuration.get('outputLevel')); 78 | } 79 | } 80 | } 81 | 82 | export function isContainer(container: any): container is Container { 83 | return container instanceof Container; 84 | } 85 | -------------------------------------------------------------------------------- /src/env/browser/hrtime.ts: -------------------------------------------------------------------------------- 1 | export function hrtime(time?: [number, number]): [number, number] { 2 | const now = performance.now() * 1e-3; 3 | let seconds = Math.floor(now); 4 | let nanoseconds = Math.floor((now % 1) * 1e9); 5 | if (time !== undefined) { 6 | seconds = seconds - time[0]; 7 | nanoseconds = nanoseconds - time[1]; 8 | if (nanoseconds < 0) { 9 | seconds--; 10 | nanoseconds += 1e9; 11 | } 12 | } 13 | return [seconds, nanoseconds]; 14 | } 15 | -------------------------------------------------------------------------------- /src/env/browser/platform.ts: -------------------------------------------------------------------------------- 1 | export const isWeb = true; 2 | 3 | const _platform = (navigator as any)?.userAgentData?.platform; 4 | const _userAgent = navigator.userAgent; 5 | 6 | export const isLinux = _platform === 'Linux' || _userAgent.indexOf('Linux') >= 0; 7 | export const isMac = _platform === 'macOS' || _userAgent.indexOf('Macintosh') >= 0; 8 | export const isWindows = _platform === 'Windows' || _userAgent.indexOf('Windows') >= 0; 9 | 10 | export function getPlatform(): string { 11 | if (isWindows) return 'web-windows'; 12 | if (isMac) return 'web-macOS'; 13 | if (isLinux) return 'web-linux'; 14 | return 'web'; 15 | } 16 | -------------------------------------------------------------------------------- /src/env/node/hrtime.ts: -------------------------------------------------------------------------------- 1 | export { hrtime } from 'process'; 2 | -------------------------------------------------------------------------------- /src/env/node/platform.ts: -------------------------------------------------------------------------------- 1 | import * as process from 'process'; 2 | import { env, UIKind } from 'vscode'; 3 | 4 | export const isWeb = env.uiKind === UIKind.Web; 5 | 6 | export const isLinux = process.platform === 'linux'; 7 | export const isMac = process.platform === 'darwin'; 8 | export const isWindows = process.platform === 'win32'; 9 | 10 | export function getPlatform(): string { 11 | if (isWindows) return 'windows'; 12 | if (isMac) return 'macOS'; 13 | if (isLinux) return 'linux'; 14 | return isWeb ? 'web' : 'unknown'; 15 | } 16 | -------------------------------------------------------------------------------- /src/excludeController.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigurationChangeEvent, Disposable, Event } from 'vscode'; 2 | import { ConfigurationTarget, EventEmitter } from 'vscode'; 3 | import type { CoreConfiguration, StoredFilesExcludes } from './constants'; 4 | import type { Container } from './container'; 5 | import { configuration } from './system/configuration'; 6 | import { setContext } from './system/context'; 7 | import { Logger } from './system/logger'; 8 | import { areEqual } from './system/object'; 9 | import type { Storage } from './system/storage'; 10 | 11 | export type FilesExcludeConfiguration = Record; 12 | 13 | export class FilesExcludeController implements Disposable { 14 | private _onDidToggle = new EventEmitter(); 15 | get onDidToggle(): Event { 16 | return this._onDidToggle.event; 17 | } 18 | 19 | private readonly _disposable: Disposable; 20 | private _working: boolean = false; 21 | 22 | constructor(private readonly container: Container, private readonly storage: Storage) { 23 | this._disposable = configuration.onDidChangeAny(this.onAnyConfigurationChanged, this); 24 | this.onAnyConfigurationChanged(); 25 | } 26 | 27 | dispose() { 28 | this._disposable.dispose(); 29 | } 30 | 31 | private onAnyConfigurationChanged(e?: ConfigurationChangeEvent) { 32 | if (this._working) return; 33 | if (e != null && !configuration.changedAny(e, 'files.exclude')) return; 34 | 35 | const savedExclude = this.getSavedExcludeConfiguration(); 36 | if (savedExclude == null) return; 37 | 38 | Logger.log('FilesExcludeController.onOtherConfigurationChanged()'); 39 | 40 | const newExclude = this.getExcludeConfiguration(); 41 | if ( 42 | newExclude != null && 43 | areEqual(savedExclude.globalValue, newExclude.globalValue) && 44 | areEqual(savedExclude.workspaceValue, newExclude.workspaceValue) 45 | ) { 46 | return; 47 | } 48 | 49 | const appliedExclude = this.getAppliedExcludeConfiguration(); 50 | if ( 51 | newExclude != null && 52 | appliedExclude != null && 53 | areEqual(appliedExclude.globalValue, newExclude.globalValue) && 54 | areEqual(appliedExclude.workspaceValue, newExclude.workspaceValue) 55 | ) { 56 | return; 57 | } 58 | 59 | Logger.log('FilesExcludeController.onOtherConfigurationChanged()', 'clearing state'); 60 | 61 | // Remove the currently saved config, since it was directly edited 62 | void this.clearExcludeConfiguration(); 63 | } 64 | 65 | async applyConfiguration() { 66 | // If we have saved state, the we are already applied to exit 67 | if (this._working || this.hasSavedExcludeConfiguration()) return; 68 | 69 | Logger.log('FilesExcludeController.applyConfiguration()'); 70 | 71 | try { 72 | this._working = true; 73 | 74 | const exclude = this.getExcludeConfiguration()!; 75 | await this.saveExcludeConfiguration(exclude); 76 | 77 | const appliedExcludes: StoredFilesExcludes = { 78 | key: exclude.key, 79 | globalValue: exclude.globalValue == null ? undefined : {}, 80 | workspaceValue: exclude.workspaceValue == null ? undefined : {}, 81 | // workspaceFolderValue: exclude.workspaceFolderValue == null ? undefined : {}, 82 | }; 83 | 84 | const promises: Thenable[] = []; 85 | 86 | if (exclude.globalValue != null && appliedExcludes.globalValue != null) { 87 | const apply: FilesExcludeConfiguration = Object.create(null); 88 | for (const key of Object.keys(exclude.globalValue)) { 89 | appliedExcludes.globalValue[key] = apply[key] = false; 90 | } 91 | 92 | promises.push( 93 | configuration.updateAny( 94 | 'files.exclude', 95 | apply, 96 | ConfigurationTarget.Global, 97 | ), 98 | ); 99 | } 100 | 101 | if (exclude.workspaceValue != null && appliedExcludes.workspaceValue != null) { 102 | const apply: FilesExcludeConfiguration = Object.create(null); 103 | for (const key of Object.keys(exclude.workspaceValue)) { 104 | appliedExcludes.workspaceValue[key] = apply[key] = false; 105 | } 106 | 107 | promises.push( 108 | configuration.updateAny( 109 | 'files.exclude', 110 | apply, 111 | ConfigurationTarget.Workspace, 112 | ), 113 | ); 114 | } 115 | 116 | // if (exclude.workspaceFolderValue != null && appliedExclude.workspaceFolderValue != null) { 117 | // const apply = Object.create(null); 118 | // for (const key of Object.keys(exclude.workspaceFolderValue)) { 119 | // appliedExclude.workspaceFolderValue[key] = apply[key] = false; 120 | // } 121 | 122 | // promises.push(configuration.updateAny(this._section, apply, ConfigurationTarget.WorkspaceFolder)); 123 | // } 124 | 125 | await this.saveAppliedExcludeConfiguration(appliedExcludes); 126 | 127 | if (!promises.length) return; 128 | 129 | await Promise.allSettled(promises); 130 | } catch (ex) { 131 | Logger.error(ex); 132 | await this.clearExcludeConfiguration(); 133 | } finally { 134 | Logger.log('FilesExcludeController.applyConfiguration()', 'done'); 135 | 136 | this._working = false; 137 | this._onDidToggle.fire(); 138 | } 139 | } 140 | 141 | async restoreConfiguration() { 142 | // If we don't have saved state, the we don't have anything to restore so exit 143 | if (this._working || !this.hasSavedExcludeConfiguration()) return; 144 | 145 | Logger.log('FilesExcludeController.restoreConfiguration()'); 146 | 147 | try { 148 | this._working = true; 149 | const excludes = this.getSavedExcludeConfiguration(); 150 | 151 | const promises: Thenable[] = []; 152 | 153 | if (excludes != null) { 154 | if (excludes.globalValue != null) { 155 | promises.push( 156 | configuration.updateAny( 157 | 'files.exclude', 158 | excludes.globalValue, 159 | ConfigurationTarget.Global, 160 | ), 161 | ); 162 | } 163 | if (excludes.workspaceValue != null) { 164 | promises.push( 165 | configuration.updateAny( 166 | 'files.exclude', 167 | excludes.workspaceValue, 168 | ConfigurationTarget.Workspace, 169 | ), 170 | ); 171 | } 172 | } 173 | 174 | // Remove the currently saved config, since we just restored it 175 | await this.clearExcludeConfiguration(); 176 | 177 | if (!promises.length) return; 178 | 179 | await Promise.allSettled(promises); 180 | } catch (ex) { 181 | Logger.error(ex); 182 | await this.clearExcludeConfiguration(); 183 | } finally { 184 | Logger.log('FilesExcludeController.restoreConfiguration()', 'done'); 185 | 186 | this._working = false; 187 | this._onDidToggle.fire(); 188 | } 189 | } 190 | 191 | async toggleConfiguration() { 192 | if (this._working) return; 193 | 194 | Logger.log('FilesExcludeController.toggleConfiguration()'); 195 | 196 | if (this.hasSavedExcludeConfiguration()) { 197 | await this.restoreConfiguration(); 198 | } else { 199 | await this.applyConfiguration(); 200 | } 201 | } 202 | 203 | get canToggle() { 204 | const exclude = this.getExcludeConfiguration(); 205 | return exclude != null && (exclude.globalValue != null || exclude.workspaceValue != null); 206 | } 207 | 208 | get toggled() { 209 | return this.hasSavedExcludeConfiguration(); 210 | } 211 | 212 | private async clearExcludeConfiguration() { 213 | await this.saveAppliedExcludeConfiguration(undefined); 214 | await this.saveExcludeConfiguration(undefined); 215 | } 216 | 217 | private getAppliedExcludeConfiguration(): StoredFilesExcludes | undefined { 218 | return this.storage.getWorkspace('appliedState'); 219 | } 220 | 221 | private getExcludeConfiguration(): StoredFilesExcludes | undefined { 222 | return configuration.inspectAny>('files.exclude'); 223 | } 224 | 225 | private getSavedExcludeConfiguration(): StoredFilesExcludes | undefined { 226 | const excludes = this.storage.getWorkspace('savedState'); 227 | this.updateContext(excludes); 228 | return excludes; 229 | } 230 | 231 | private hasSavedExcludeConfiguration(): boolean { 232 | return this.getSavedExcludeConfiguration() != null; 233 | } 234 | 235 | private saveAppliedExcludeConfiguration(excludes: StoredFilesExcludes | undefined): Promise { 236 | return this.storage.storeWorkspace('appliedState', excludes); 237 | } 238 | 239 | private saveExcludeConfiguration(excludes: StoredFilesExcludes | undefined): Promise { 240 | this.updateContext(excludes); 241 | return this.storage.storeWorkspace('savedState', excludes); 242 | } 243 | 244 | private _loaded = false; 245 | private updateContext(excludes: StoredFilesExcludes | undefined) { 246 | void setContext('toggleexcludedfiles:toggled', excludes != null); 247 | if (!this._loaded) { 248 | this._loaded = true; 249 | void setContext('toggleexcludedfiles:loaded', true); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode'; 2 | import { version as codeVersion, env, ExtensionMode, Uri, window } from 'vscode'; 3 | import { isWeb } from '@env/platform'; 4 | import { fromOutputLevel } from './config'; 5 | import { Container } from './container'; 6 | import { Configuration, configuration } from './system/configuration'; 7 | import { Logger } from './system/logger'; 8 | import { satisfies } from './system/version'; 9 | 10 | export function activate(context: ExtensionContext) { 11 | const extensionVersion: string = context.extension.packageJSON.version; 12 | const prerelease = satisfies(extensionVersion, '> 2023.0.0'); 13 | 14 | Logger.configure( 15 | { 16 | name: 'Toggle Excluded', 17 | createChannel: function (name: string) { 18 | return window.createOutputChannel(name); 19 | }, 20 | toLoggable: function (o: any) { 21 | if (o instanceof Uri) return `Uri(${o.toString(true)})`; 22 | return undefined; 23 | }, 24 | }, 25 | fromOutputLevel(configuration.get('outputLevel')), 26 | context.extensionMode === ExtensionMode.Development, 27 | ); 28 | 29 | Logger.log( 30 | `Toggle Excluded Files${prerelease ? ' (pre-release)' : ''} v${extensionVersion} activating in ${ 31 | env.appName 32 | }(${codeVersion}) on the ${isWeb ? 'web' : 'desktop'}`, 33 | ); 34 | 35 | Configuration.configure(context); 36 | Container.create(context); 37 | } 38 | 39 | export function deactivate() { 40 | // nothing to do 41 | } 42 | -------------------------------------------------------------------------------- /src/statusBarController.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigurationChangeEvent, StatusBarItem } from 'vscode'; 2 | import { Disposable, StatusBarAlignment, window } from 'vscode'; 3 | import type { Commands, CoreConfiguration } from './constants'; 4 | import { extensionPrefix } from './constants'; 5 | import type { Container } from './container'; 6 | import { configuration } from './system/configuration'; 7 | 8 | export class StatusBarController implements Disposable { 9 | private _disposable: Disposable; 10 | private _statusBarItem: StatusBarItem | undefined; 11 | 12 | constructor(private readonly container: Container) { 13 | this._disposable = Disposable.from( 14 | configuration.onDidChangeAny(this.onAnyConfigurationChanged, this), 15 | container.filesExclude.onDidToggle(this._onExcludeToggled, this), 16 | ); 17 | 18 | this.onAnyConfigurationChanged(); 19 | } 20 | 21 | dispose() { 22 | this._statusBarItem?.dispose(); 23 | this._disposable?.dispose(); 24 | } 25 | 26 | private onAnyConfigurationChanged(e?: ConfigurationChangeEvent) { 27 | if ( 28 | e == null || 29 | configuration.changed(e, 'statusBar.enabled') || 30 | configuration.changedAny(e, 'files.exclude') 31 | ) { 32 | this._statusBarItem?.dispose(); 33 | 34 | const { canToggle } = this.container.filesExclude; 35 | if (configuration.get('statusBar.enabled') && canToggle) { 36 | this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 0); 37 | this._statusBarItem.command = `${extensionPrefix}.toggle` satisfies keyof Commands; 38 | this.updateStatusBarItem(this.container.filesExclude.toggled); 39 | this._statusBarItem.show(); 40 | } 41 | } 42 | } 43 | 44 | private updateStatusBarItem(toggled: boolean) { 45 | if (this._statusBarItem == null) return; 46 | 47 | this._statusBarItem.text = toggled ? '$(eye-closed)' : '$(eye)'; 48 | this._statusBarItem.tooltip = `${toggled ? 'Hide' : 'Show'} Excluded Files`; 49 | } 50 | 51 | private _onExcludeToggled() { 52 | if (this._statusBarItem == null) return; 53 | 54 | this.updateStatusBarItem(this.container.filesExclude.toggled); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/system/command.ts: -------------------------------------------------------------------------------- 1 | import type { Command, Disposable } from 'vscode'; 2 | import { commands } from 'vscode'; 3 | import type { Commands, CoreCommands } from '../constants'; 4 | 5 | export function registerCommand( 6 | command: T, 7 | callback: (...args: Commands[T]) => unknown, 8 | thisArg?: any, 9 | ): Disposable { 10 | return commands.registerCommand(command, callback, thisArg); 11 | } 12 | 13 | export function createCommand(command: T, title: string, ...args: Commands[T]): Command { 14 | return { 15 | command: command, 16 | title: title, 17 | arguments: args, 18 | }; 19 | } 20 | 21 | export function executeCommand(command: T, ...args: Commands[T]): Thenable { 22 | return commands.executeCommand(command, ...args); 23 | } 24 | 25 | export function createCoreCommand(command: CoreCommands, title: string, ...args: T): Command { 26 | return { 27 | command: command, 28 | title: title, 29 | arguments: args, 30 | }; 31 | } 32 | 33 | export function executeCoreCommand(command: CoreCommands, arg: T): Thenable; 34 | export function executeCoreCommand( 35 | command: CoreCommands, 36 | ...args: T 37 | ): Thenable; 38 | export function executeCoreCommand( 39 | command: CoreCommands, 40 | ...args: T 41 | ): Thenable { 42 | return commands.executeCommand(command, ...args); 43 | } 44 | -------------------------------------------------------------------------------- /src/system/configuration.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigurationChangeEvent, ConfigurationScope, Event, ExtensionContext } from 'vscode'; 2 | import { ConfigurationTarget, EventEmitter, workspace } from 'vscode'; 3 | import type { Config } from '../config'; 4 | import { extensionPrefix } from '../constants'; 5 | import { areEqual } from './object'; 6 | 7 | interface ConfigurationOverrides { 8 | get(section: T, value: ConfigPathValue): ConfigPathValue; 9 | getAll(config: Config): Config; 10 | onDidChange(e: ConfigurationChangeEvent): ConfigurationChangeEvent; 11 | } 12 | 13 | export class Configuration { 14 | static configure(context: ExtensionContext): void { 15 | context.subscriptions.push( 16 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 17 | workspace.onDidChangeConfiguration(configuration.onConfigurationChanged, configuration), 18 | ); 19 | } 20 | 21 | private _onDidChange = new EventEmitter(); 22 | get onDidChange(): Event { 23 | return this._onDidChange.event; 24 | } 25 | 26 | private _onDidChangeAny = new EventEmitter(); 27 | get onDidChangeAny(): Event { 28 | return this._onDidChangeAny.event; 29 | } 30 | 31 | private onConfigurationChanged(e: ConfigurationChangeEvent) { 32 | this._onDidChangeAny.fire(e); 33 | if (!e.affectsConfiguration(extensionPrefix)) return; 34 | 35 | if (this._overrides?.onDidChange != null) { 36 | e = this._overrides.onDidChange(e); 37 | } 38 | 39 | this._onDidChange.fire(e); 40 | } 41 | 42 | private _overrides: Partial | undefined; 43 | 44 | applyOverrides(overrides: ConfigurationOverrides): void { 45 | this._overrides = overrides; 46 | } 47 | 48 | clearOverrides(): void { 49 | if (this._overrides == null) return; 50 | 51 | // Don't clear the "onChange" override as we need to keep it until the stack unwinds (so the the event propagates with the override) 52 | this._overrides.get = undefined; 53 | this._overrides.getAll = undefined; 54 | queueMicrotask(() => (this._overrides = undefined)); 55 | } 56 | 57 | get(section: S, scope?: ConfigurationScope | null): ConfigPathValue; 58 | get( 59 | section: S, 60 | scope: ConfigurationScope | null | undefined, 61 | defaultValue: NonNullable>, 62 | ): NonNullable>; 63 | get( 64 | section: S, 65 | scope?: ConfigurationScope | null, 66 | defaultValue?: NonNullable>, 67 | skipOverrides?: boolean, 68 | ): ConfigPathValue { 69 | const value = 70 | defaultValue === undefined 71 | ? workspace.getConfiguration(extensionPrefix, scope).get>(section)! 72 | : workspace.getConfiguration(extensionPrefix, scope).get>(section, defaultValue)!; 73 | return skipOverrides || this._overrides?.get == null ? value : this._overrides.get(section, value); 74 | } 75 | 76 | getAll(skipOverrides?: boolean): Config { 77 | const config = workspace.getConfiguration().get(extensionPrefix)!; 78 | return skipOverrides || this._overrides?.getAll == null ? config : this._overrides.getAll(config); 79 | } 80 | 81 | getAny(section: S, scope?: ConfigurationScope | null): T | undefined; 82 | getAny(section: S, scope: ConfigurationScope | null | undefined, defaultValue: T): T; 83 | getAny(section: S, scope?: ConfigurationScope | null, defaultValue?: T): T | undefined { 84 | return defaultValue === undefined 85 | ? workspace.getConfiguration(undefined, scope).get(section) 86 | : workspace.getConfiguration(undefined, scope).get(section, defaultValue); 87 | } 88 | 89 | changed( 90 | e: ConfigurationChangeEvent | undefined, 91 | section: S | S[], 92 | scope?: ConfigurationScope | null | undefined, 93 | ): boolean { 94 | if (e == null) return true; 95 | 96 | return Array.isArray(section) 97 | ? section.some(s => e.affectsConfiguration(`${extensionPrefix}.${s}`, scope!)) 98 | : e.affectsConfiguration(`${extensionPrefix}.${section}`, scope!); 99 | } 100 | 101 | changedAny( 102 | e: ConfigurationChangeEvent | undefined, 103 | section: S | S[], 104 | scope?: ConfigurationScope | null | undefined, 105 | ): boolean { 106 | if (e == null) return true; 107 | 108 | return Array.isArray(section) 109 | ? section.some(s => e.affectsConfiguration(s, scope!)) 110 | : e.affectsConfiguration(section, scope!); 111 | } 112 | 113 | inspect>(section: S, scope?: ConfigurationScope | null) { 114 | return workspace 115 | .getConfiguration(extensionPrefix, scope) 116 | .inspect(section === undefined ? extensionPrefix : section); 117 | } 118 | 119 | inspectAny(section: S, scope?: ConfigurationScope | null) { 120 | return workspace.getConfiguration(undefined, scope).inspect(section); 121 | } 122 | 123 | isUnset(section: S, scope?: ConfigurationScope | null): boolean { 124 | const inspect = this.inspect(section, scope)!; 125 | if (inspect.workspaceFolderValue !== undefined) return false; 126 | if (inspect.workspaceValue !== undefined) return false; 127 | if (inspect.globalValue !== undefined) return false; 128 | 129 | return true; 130 | } 131 | 132 | async migrate( 133 | from: string, 134 | to: S, 135 | options: { fallbackValue?: ConfigPathValue; migrationFn?(value: any): ConfigPathValue }, 136 | ): Promise { 137 | const inspection = this.inspect(from as any); 138 | if (inspection === undefined) return false; 139 | 140 | let migrated = false; 141 | if (inspection.globalValue !== undefined) { 142 | await this.update( 143 | to, 144 | options.migrationFn != null ? options.migrationFn(inspection.globalValue) : inspection.globalValue, 145 | ConfigurationTarget.Global, 146 | ); 147 | migrated = true; 148 | // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` 149 | // if (from !== to) { 150 | // try { 151 | // await this.update(from, undefined, ConfigurationTarget.Global); 152 | // } 153 | // catch { } 154 | // } 155 | } 156 | 157 | if (inspection.workspaceValue !== undefined) { 158 | await this.update( 159 | to, 160 | options.migrationFn != null 161 | ? options.migrationFn(inspection.workspaceValue) 162 | : inspection.workspaceValue, 163 | ConfigurationTarget.Workspace, 164 | ); 165 | migrated = true; 166 | // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` 167 | // if (from !== to) { 168 | // try { 169 | // await this.update(from, undefined, ConfigurationTarget.Workspace); 170 | // } 171 | // catch { } 172 | // } 173 | } 174 | 175 | if (inspection.workspaceFolderValue !== undefined) { 176 | await this.update( 177 | to, 178 | options.migrationFn != null 179 | ? options.migrationFn(inspection.workspaceFolderValue) 180 | : inspection.workspaceFolderValue, 181 | ConfigurationTarget.WorkspaceFolder, 182 | ); 183 | migrated = true; 184 | // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` 185 | // if (from !== to) { 186 | // try { 187 | // await this.update(from, undefined, ConfigurationTarget.WorkspaceFolder); 188 | // } 189 | // catch { } 190 | // } 191 | } 192 | 193 | if (!migrated && options.fallbackValue !== undefined) { 194 | await this.update(to, options.fallbackValue, ConfigurationTarget.Global); 195 | migrated = true; 196 | } 197 | 198 | return migrated; 199 | } 200 | 201 | async migrateIfMissing( 202 | from: string, 203 | to: S, 204 | options: { migrationFn?(value: any): ConfigPathValue }, 205 | ): Promise { 206 | const fromInspection = this.inspect(from as any); 207 | if (fromInspection === undefined) return; 208 | 209 | const toInspection = this.inspect(to); 210 | if (fromInspection.globalValue !== undefined) { 211 | if (toInspection === undefined || toInspection.globalValue === undefined) { 212 | await this.update( 213 | to, 214 | options.migrationFn != null 215 | ? options.migrationFn(fromInspection.globalValue) 216 | : fromInspection.globalValue, 217 | ConfigurationTarget.Global, 218 | ); 219 | // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` 220 | // if (from !== to) { 221 | // try { 222 | // await this.update(from, undefined, ConfigurationTarget.Global); 223 | // } 224 | // catch { } 225 | // } 226 | } 227 | } 228 | 229 | if (fromInspection.workspaceValue !== undefined) { 230 | if (toInspection === undefined || toInspection.workspaceValue === undefined) { 231 | await this.update( 232 | to, 233 | options.migrationFn != null 234 | ? options.migrationFn(fromInspection.workspaceValue) 235 | : fromInspection.workspaceValue, 236 | ConfigurationTarget.Workspace, 237 | ); 238 | // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` 239 | // if (from !== to) { 240 | // try { 241 | // await this.update(from, undefined, ConfigurationTarget.Workspace); 242 | // } 243 | // catch { } 244 | // } 245 | } 246 | } 247 | 248 | if (fromInspection.workspaceFolderValue !== undefined) { 249 | if (toInspection === undefined || toInspection.workspaceFolderValue === undefined) { 250 | await this.update( 251 | to, 252 | options.migrationFn != null 253 | ? options.migrationFn(fromInspection.workspaceFolderValue) 254 | : fromInspection.workspaceFolderValue, 255 | ConfigurationTarget.WorkspaceFolder, 256 | ); 257 | // Can't delete the old setting currently because it errors with `Unable to write to User Settings because is not a registered configuration` 258 | // if (from !== to) { 259 | // try { 260 | // await this.update(from, undefined, ConfigurationTarget.WorkspaceFolder); 261 | // } 262 | // catch { } 263 | // } 264 | } 265 | } 266 | } 267 | 268 | matches(match: S, section: ConfigPath, value: unknown): value is ConfigPathValue { 269 | return match === section; 270 | } 271 | 272 | name(section: S): string { 273 | return section; 274 | } 275 | 276 | update( 277 | section: S, 278 | value: ConfigPathValue | undefined, 279 | target: ConfigurationTarget, 280 | ): Thenable { 281 | return workspace.getConfiguration(extensionPrefix).update(section, value, target); 282 | } 283 | 284 | updateAny( 285 | section: S, 286 | value: T, 287 | target: ConfigurationTarget, 288 | scope?: ConfigurationScope | null, 289 | ): Thenable { 290 | return workspace 291 | .getConfiguration(undefined, target === ConfigurationTarget.Global ? undefined : scope!) 292 | .update(section, value, target); 293 | } 294 | 295 | updateEffective(section: S, value: ConfigPathValue | undefined): Thenable { 296 | const inspect = this.inspect(section)!; 297 | if (inspect.workspaceFolderValue !== undefined) { 298 | if (value === inspect.workspaceFolderValue) return Promise.resolve(undefined); 299 | 300 | return this.update(section, value, ConfigurationTarget.WorkspaceFolder); 301 | } 302 | 303 | if (inspect.workspaceValue !== undefined) { 304 | if (value === inspect.workspaceValue) return Promise.resolve(undefined); 305 | 306 | return this.update(section, value, ConfigurationTarget.Workspace); 307 | } 308 | 309 | if (inspect.globalValue === value || (inspect.globalValue === undefined && value === inspect.defaultValue)) { 310 | return Promise.resolve(undefined); 311 | } 312 | 313 | return this.update( 314 | section, 315 | areEqual(value, inspect.defaultValue) ? undefined : value, 316 | ConfigurationTarget.Global, 317 | ); 318 | } 319 | } 320 | 321 | export const configuration = new Configuration(); 322 | 323 | type SubPath = Key extends string 324 | ? T[Key] extends Record 325 | ? 326 | | `${Key}.${SubPath> & string}` 327 | | `${Key}.${Exclude & string}` 328 | : never 329 | : never; 330 | 331 | export type Path = SubPath | keyof T; 332 | 333 | export type PathValue> = P extends `${infer Key}.${infer Rest}` 334 | ? Key extends keyof T 335 | ? Rest extends Path 336 | ? PathValue 337 | : never 338 | : never 339 | : P extends keyof T 340 | ? T[P] 341 | : never; 342 | 343 | export type ConfigPath = Path; 344 | export type ConfigPathValue

= PathValue; 345 | -------------------------------------------------------------------------------- /src/system/context.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'vscode'; 2 | import type { ContextKeys } from '../constants'; 3 | import { executeCoreCommand } from './command'; 4 | 5 | const contextStorage = new Map(); 6 | 7 | const _onDidChangeContext = new EventEmitter(); 8 | export const onDidChangeContext = _onDidChangeContext.event; 9 | 10 | export function getContext(key: ContextKeys): T | undefined; 11 | export function getContext(key: ContextKeys, defaultValue: T): T; 12 | export function getContext(key: ContextKeys, defaultValue?: T): T | undefined { 13 | return (contextStorage.get(key) as T | undefined) ?? defaultValue; 14 | } 15 | 16 | export async function setContext(key: ContextKeys, value: unknown): Promise { 17 | contextStorage.set(key, value); 18 | void (await executeCoreCommand('setContext', key, value)); 19 | _onDidChangeContext.fire(key); 20 | } 21 | -------------------------------------------------------------------------------- /src/system/decorators/command.ts: -------------------------------------------------------------------------------- 1 | import type { MessageItem } from 'vscode'; 2 | import { window } from 'vscode'; 3 | import type { Commands, UnqualifiedPaletteCommands } from '../../constants'; 4 | import { extensionPrefix } from '../../constants'; 5 | import { Logger } from '../logger'; 6 | import { LogLevel } from '../logger.constants'; 7 | 8 | type CommandCallback = (this: any, ...args: Commands[T]) => any; 9 | 10 | export function createCommandDecorator( 11 | registry: Command<`${typeof extensionPrefix}.${T}`>[], 12 | ): (command: T, options?: CommandOptions) => (target: any, key: string, descriptor: PropertyDescriptor) => void { 13 | return (command: T, options?: CommandOptions) => _command(registry, command, options); 14 | } 15 | 16 | export interface CommandOptions { 17 | args?(...args: any[]): any[]; 18 | customErrorHandling?: boolean; 19 | showErrorMessage?: string; 20 | } 21 | 22 | export interface Command { 23 | name: T; 24 | key: string; 25 | method: CommandCallback; 26 | options?: CommandOptions; 27 | } 28 | 29 | function _command( 30 | registry: Command<`${typeof extensionPrefix}.${T}`>[], 31 | command: T, 32 | options?: CommandOptions, 33 | ) { 34 | return (_target: any, key: string, descriptor: PropertyDescriptor) => { 35 | if (typeof descriptor.value !== 'function') throw new Error('not supported'); 36 | 37 | let method; 38 | if (!options?.customErrorHandling) { 39 | method = async function (this: any, ...args: any[]) { 40 | try { 41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 42 | return await descriptor.value.apply(this, options?.args?.(args) ?? args); 43 | } catch (ex) { 44 | Logger.error(ex); 45 | 46 | if (options?.showErrorMessage) { 47 | if (Logger.enabled(LogLevel.Error)) { 48 | const actions: MessageItem[] = [{ title: 'Open Output Channel' }]; 49 | 50 | const result = await window.showErrorMessage( 51 | `${options.showErrorMessage} \u00a0\u2014\u00a0 ${ex.toString()}`, 52 | ...actions, 53 | ); 54 | if (result === actions[0]) { 55 | Logger.showOutputChannel(); 56 | } 57 | } else { 58 | void window.showErrorMessage( 59 | `${options.showErrorMessage} \u00a0\u2014\u00a0 ${ex.toString()}`, 60 | ); 61 | } 62 | } 63 | 64 | return undefined; 65 | } 66 | }; 67 | } else { 68 | method = descriptor.value; 69 | } 70 | 71 | registry.push({ 72 | name: `${extensionPrefix}.${command}`, 73 | key: key, 74 | method: method, 75 | options: options, 76 | }); 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/system/decorators/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | import { hrtime } from '@env/hrtime'; 3 | import { getParameters } from '../function'; 4 | import { getLoggableName, Logger } from '../logger'; 5 | import { LogLevel, slowCallWarningThreshold } from '../logger.constants'; 6 | import type { LogScope } from '../logger.scope'; 7 | import { clearLogScope, getNextLogScopeId, setLogScope } from '../logger.scope'; 8 | import { isPromise } from '../promise'; 9 | import { getDurationMilliseconds } from '../string'; 10 | 11 | const emptyStr = ''; 12 | 13 | export interface LogContext { 14 | id: number; 15 | instance: any; 16 | instanceName: string; 17 | name: string; 18 | prefix: string; 19 | } 20 | 21 | interface LogOptions any> { 22 | args?: 23 | | false 24 | | { 25 | 0?: ((arg: Parameters[0]) => unknown) | string | false; 26 | 1?: ((arg: Parameters[1]) => unknown) | string | false; 27 | 2?: ((arg: Parameters[2]) => unknown) | string | false; 28 | 3?: ((arg: Parameters[3]) => unknown) | string | false; 29 | 4?: ((arg: Parameters[4]) => unknown) | string | false; 30 | [key: number]: (((arg: any) => unknown) | string | false) | undefined; 31 | }; 32 | condition?(...args: Parameters): boolean; 33 | enter?(...args: Parameters): string; 34 | exit?(result: PromiseType>): string; 35 | prefix?(context: LogContext, ...args: Parameters): string; 36 | sanitize?(key: string, value: any): any; 37 | logThreshold?: number; 38 | scoped?: boolean; 39 | singleLine?: boolean; 40 | timed?: boolean; 41 | } 42 | 43 | export const LogInstanceNameFn = Symbol('logInstanceNameFn'); 44 | 45 | export function logName(fn: (c: T, name: string) => string) { 46 | return (target: Function) => { 47 | (target as any)[LogInstanceNameFn] = fn; 48 | }; 49 | } 50 | 51 | export function debug any>(options?: LogOptions) { 52 | return log(options, true); 53 | } 54 | 55 | type PromiseType = T extends Promise ? U : T; 56 | 57 | export function log any>(options?: LogOptions, debug = false) { 58 | let overrides: LogOptions['args'] | undefined; 59 | let conditionFn: LogOptions['condition'] | undefined; 60 | let enterFn: LogOptions['enter'] | undefined; 61 | let exitFn: LogOptions['exit'] | undefined; 62 | let prefixFn: LogOptions['prefix'] | undefined; 63 | let sanitizeFn: LogOptions['sanitize'] | undefined; 64 | let logThreshold = 0; 65 | let scoped = false; 66 | let singleLine = false; 67 | let timed = true; 68 | if (options != null) { 69 | ({ 70 | args: overrides, 71 | condition: conditionFn, 72 | enter: enterFn, 73 | exit: exitFn, 74 | prefix: prefixFn, 75 | sanitize: sanitizeFn, 76 | logThreshold = 0, 77 | scoped = true, 78 | singleLine = false, 79 | timed = true, 80 | } = options); 81 | } 82 | 83 | if (logThreshold > 0) { 84 | singleLine = true; 85 | timed = true; 86 | } 87 | 88 | if (timed) { 89 | scoped = true; 90 | } 91 | 92 | const logFn = debug ? Logger.debug.bind(Logger) : Logger.log.bind(Logger); 93 | const warnFn = Logger.warn.bind(Logger); 94 | 95 | return (target: any, key: string, descriptor: PropertyDescriptor & Record) => { 96 | let fn: Function | undefined; 97 | let fnKey: string | undefined; 98 | if (typeof descriptor.value === 'function') { 99 | fn = descriptor.value; 100 | fnKey = 'value'; 101 | } else if (typeof descriptor.get === 'function') { 102 | fn = descriptor.get; 103 | fnKey = 'get'; 104 | } 105 | if (fn == null || fnKey == null) throw new Error('Not supported'); 106 | 107 | const parameters = getParameters(fn); 108 | 109 | descriptor[fnKey] = function (this: any, ...args: Parameters) { 110 | const scopeId = getNextLogScopeId(); 111 | 112 | if ( 113 | (!Logger.isDebugging && 114 | !Logger.enabled(LogLevel.Debug) && 115 | !(Logger.enabled(LogLevel.Info) && !debug)) || 116 | (conditionFn != null && !conditionFn(...args)) 117 | ) { 118 | return fn!.apply(this, args); 119 | } 120 | 121 | let instanceName: string; 122 | if (this != null) { 123 | instanceName = getLoggableName(this); 124 | if (this.constructor?.[LogInstanceNameFn]) { 125 | instanceName = target.constructor[LogInstanceNameFn](this, instanceName); 126 | } 127 | } else { 128 | instanceName = emptyStr; 129 | } 130 | 131 | let prefix = `${scoped ? `[${scopeId.toString(16).padStart(5)}] ` : emptyStr}${ 132 | instanceName ? `${instanceName}.` : emptyStr 133 | }${key}`; 134 | 135 | if (prefixFn != null) { 136 | prefix = prefixFn( 137 | { 138 | id: scopeId, 139 | instance: this, 140 | instanceName: instanceName, 141 | name: key, 142 | prefix: prefix, 143 | }, 144 | ...args, 145 | ); 146 | } 147 | 148 | let scope: LogScope | undefined; 149 | if (scoped) { 150 | scope = { scopeId: scopeId, prefix: prefix }; 151 | setLogScope(scopeId, scope); 152 | } 153 | 154 | const enter = enterFn != null ? enterFn(...args) : emptyStr; 155 | 156 | let loggableParams: string; 157 | if (overrides === false || args.length === 0) { 158 | loggableParams = emptyStr; 159 | 160 | if (!singleLine) { 161 | logFn(`${prefix}${enter}`); 162 | } 163 | } else { 164 | loggableParams = ''; 165 | 166 | let paramOverride; 167 | let paramIndex = -1; 168 | let paramName; 169 | let paramLogValue; 170 | let paramValue; 171 | 172 | for (paramValue of args as unknown[]) { 173 | paramName = parameters[++paramIndex]; 174 | 175 | paramOverride = overrides?.[paramIndex]; 176 | if (paramOverride != null) { 177 | if (typeof paramOverride === 'boolean') continue; 178 | 179 | if (loggableParams.length > 0) { 180 | loggableParams += ', '; 181 | } 182 | 183 | if (typeof paramOverride === 'string') { 184 | loggableParams += paramOverride; 185 | continue; 186 | } 187 | 188 | paramLogValue = String(paramOverride(paramValue)); 189 | } else { 190 | if (loggableParams.length > 0) { 191 | loggableParams += ', '; 192 | } 193 | 194 | paramLogValue = Logger.toLoggable(paramValue, sanitizeFn); 195 | } 196 | 197 | loggableParams += paramName ? `${paramName}=${paramLogValue}` : paramLogValue; 198 | } 199 | 200 | if (!singleLine) { 201 | logFn( 202 | `${prefix}${enter}${ 203 | loggableParams && (debug || Logger.enabled(LogLevel.Debug) || Logger.isDebugging) 204 | ? `(${loggableParams})` 205 | : emptyStr 206 | }`, 207 | ); 208 | } 209 | } 210 | 211 | if (singleLine || timed || exitFn != null) { 212 | const start = timed ? hrtime() : undefined; 213 | 214 | const logError = (ex: Error) => { 215 | const timing = start !== undefined ? ` \u2022 ${getDurationMilliseconds(start)} ms` : emptyStr; 216 | if (singleLine) { 217 | Logger.error( 218 | ex, 219 | `${prefix}${enter}${loggableParams ? `(${loggableParams})` : emptyStr}`, 220 | `failed${scope?.exitDetails ? scope.exitDetails : emptyStr}${timing}`, 221 | ); 222 | } else { 223 | Logger.error(ex, prefix, `failed${scope?.exitDetails ? scope.exitDetails : emptyStr}${timing}`); 224 | } 225 | 226 | if (scoped) { 227 | clearLogScope(scopeId); 228 | } 229 | }; 230 | 231 | let result; 232 | try { 233 | result = fn!.apply(this, args); 234 | } catch (ex) { 235 | logError(ex); 236 | throw ex; 237 | } 238 | 239 | const logResult = (r: any) => { 240 | let duration: number | undefined; 241 | let exitLogFn; 242 | let timing; 243 | if (start != null) { 244 | duration = getDurationMilliseconds(start); 245 | if (duration > slowCallWarningThreshold) { 246 | exitLogFn = warnFn; 247 | timing = ` \u2022 ${duration} ms (slow)`; 248 | } else { 249 | exitLogFn = logFn; 250 | timing = ` \u2022 ${duration} ms`; 251 | } 252 | } else { 253 | timing = emptyStr; 254 | exitLogFn = logFn; 255 | } 256 | 257 | let exit; 258 | if (exitFn != null) { 259 | try { 260 | exit = exitFn(r); 261 | } catch (ex) { 262 | exit = `@log.exit error: ${ex}`; 263 | } 264 | } else { 265 | exit = 'completed'; 266 | } 267 | 268 | if (singleLine) { 269 | if (logThreshold === 0 || duration! > logThreshold) { 270 | exitLogFn( 271 | `${prefix}${enter}${ 272 | loggableParams && (debug || Logger.enabled(LogLevel.Debug) || Logger.isDebugging) 273 | ? `(${loggableParams})` 274 | : emptyStr 275 | } ${exit}${scope?.exitDetails ? scope.exitDetails : emptyStr}${timing}`, 276 | ); 277 | } 278 | } else { 279 | exitLogFn( 280 | `${prefix}${ 281 | loggableParams && (debug || Logger.enabled(LogLevel.Debug) || Logger.isDebugging) 282 | ? `(${loggableParams})` 283 | : emptyStr 284 | } ${exit}${scope?.exitDetails ? scope.exitDetails : emptyStr}${timing}`, 285 | ); 286 | } 287 | 288 | if (scoped) { 289 | clearLogScope(scopeId); 290 | } 291 | }; 292 | 293 | if (result != null && isPromise(result)) { 294 | const promise = result.then(logResult); 295 | promise.catch(logError); 296 | } else { 297 | logResult(result); 298 | } 299 | 300 | return result; 301 | } 302 | 303 | return fn!.apply(this, args); 304 | }; 305 | }; 306 | } 307 | -------------------------------------------------------------------------------- /src/system/decorators/memoize.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | import { resolveProp } from './resolver'; 3 | 4 | export function memoize any>(resolver?: (...args: Parameters) => string) { 5 | return (target: any, key: string, descriptor: PropertyDescriptor & Record) => { 6 | let fn: Function | undefined; 7 | let fnKey: string | undefined; 8 | 9 | if (typeof descriptor.value === 'function') { 10 | fn = descriptor.value; 11 | fnKey = 'value'; 12 | } else if (typeof descriptor.get === 'function') { 13 | fn = descriptor.get; 14 | fnKey = 'get'; 15 | } else { 16 | throw new Error('Not supported'); 17 | } 18 | 19 | if (fn == null) throw new Error('Not supported'); 20 | 21 | const memoizeKey = `$memoize$${key}`; 22 | 23 | let result; 24 | descriptor[fnKey] = function (...args: any[]) { 25 | const prop = resolveProp(memoizeKey, resolver, ...(args as Parameters)); 26 | if (Object.prototype.hasOwnProperty.call(this, prop)) { 27 | result = this[prop]; 28 | 29 | return result; 30 | } 31 | 32 | result = fn!.apply(this, args); 33 | Object.defineProperty(this, prop, { 34 | configurable: false, 35 | enumerable: false, 36 | writable: false, 37 | value: result, 38 | }); 39 | 40 | return result; 41 | }; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/system/decorators/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { isContainer } from '../../container'; 3 | 4 | function replacer(key: string, value: any): any { 5 | if (key === '') return value; 6 | 7 | if (value == null) return value; 8 | if (typeof value !== 'object') return value; 9 | 10 | if (value instanceof Error) return String(value); 11 | if (value instanceof Uri) { 12 | if ('sha' in (value as any) && (value as any).sha) { 13 | return `${(value as any).sha}:${value.toString()}`; 14 | } 15 | return value.toString(); 16 | } 17 | if (isContainer(value)) return ''; 18 | 19 | return value; 20 | } 21 | 22 | export function defaultResolver(...args: any[]): string { 23 | if (args.length === 0) return ''; 24 | if (args.length !== 1) { 25 | return JSON.stringify(args, replacer); 26 | } 27 | 28 | const arg0 = args[0]; 29 | if (arg0 == null) return ''; 30 | switch (typeof arg0) { 31 | case 'string': 32 | return arg0; 33 | 34 | case 'number': 35 | case 'boolean': 36 | case 'undefined': 37 | case 'symbol': 38 | case 'bigint': 39 | return String(arg0); 40 | 41 | default: 42 | if (arg0 instanceof Error) return String(arg0); 43 | if (arg0 instanceof Uri) { 44 | if ('sha' in arg0 && typeof arg0.sha === 'string' && arg0.sha) { 45 | return `${arg0.sha}:${arg0.toString()}`; 46 | } 47 | return arg0.toString(); 48 | } 49 | if (isContainer(arg0)) return ''; 50 | 51 | return JSON.stringify(arg0, replacer); 52 | } 53 | } 54 | 55 | export type Resolver any> = (...args: Parameters) => string; 56 | 57 | export function resolveProp any>( 58 | key: string, 59 | resolver: Resolver | undefined, 60 | ...args: Parameters 61 | ) { 62 | if (args.length === 0) return key; 63 | 64 | let resolved; 65 | if (resolver != null) { 66 | try { 67 | resolved = resolver(...args); 68 | } catch { 69 | debugger; 70 | resolved = defaultResolver(...(args as any)); 71 | } 72 | } else { 73 | resolved = defaultResolver(...(args as any)); 74 | } 75 | 76 | return `${key}$${resolved}`; 77 | } 78 | -------------------------------------------------------------------------------- /src/system/function.ts: -------------------------------------------------------------------------------- 1 | const comma = ','; 2 | const emptyStr = ''; 3 | const equals = '='; 4 | const openBrace = '{'; 5 | const openParen = '('; 6 | const closeParen = ')'; 7 | 8 | const fnBodyRegex = /\(([\s\S]*)\)/; 9 | const fnBodyStripCommentsRegex = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/gm; 10 | const fnBodyStripParamDefaultValueRegex = /\s?=.*$/; 11 | 12 | export function getParameters(fn: Function): string[] { 13 | if (typeof fn !== 'function') throw new Error('Not supported'); 14 | 15 | if (fn.length === 0) return []; 16 | 17 | let fnBody: string = Function.prototype.toString.call(fn); 18 | fnBody = fnBody.replace(fnBodyStripCommentsRegex, emptyStr) || fnBody; 19 | fnBody = fnBody.slice(0, fnBody.indexOf(openBrace)); 20 | 21 | let open = fnBody.indexOf(openParen); 22 | let close = fnBody.indexOf(closeParen); 23 | 24 | open = open >= 0 ? open + 1 : 0; 25 | close = close > 0 ? close : fnBody.indexOf(equals); 26 | 27 | fnBody = fnBody.slice(open, close); 28 | fnBody = `(${fnBody})`; 29 | 30 | const match = fnBodyRegex.exec(fnBody); 31 | return match != null 32 | ? match[1].split(comma).map(param => param.trim().replace(fnBodyStripParamDefaultValueRegex, emptyStr)) 33 | : []; 34 | } 35 | -------------------------------------------------------------------------------- /src/system/logger.constants.ts: -------------------------------------------------------------------------------- 1 | export const slowCallWarningThreshold = 500; 2 | 3 | export const enum LogLevel { 4 | Off = 'off', 5 | Error = 'error', 6 | Warn = 'warn', 7 | Info = 'info', 8 | Debug = 'debug', 9 | } 10 | -------------------------------------------------------------------------------- /src/system/logger.scope.ts: -------------------------------------------------------------------------------- 1 | const maxSmallIntegerV8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers) 2 | 3 | const scopes = new Map(); 4 | let scopeCounter = 0; 5 | 6 | export interface LogScope { 7 | readonly scopeId?: number; 8 | readonly prefix: string; 9 | exitDetails?: string; 10 | } 11 | 12 | export function clearLogScope(scopeId: number) { 13 | scopes.delete(scopeId); 14 | } 15 | 16 | export function getLogScope(): LogScope | undefined { 17 | return scopes.get(scopeCounter); 18 | } 19 | 20 | export function getNewLogScope(prefix: string): LogScope { 21 | const scopeId = getNextLogScopeId(); 22 | return { 23 | scopeId: scopeId, 24 | prefix: `[${String(scopeId).padStart(5)}] ${prefix}`, 25 | }; 26 | } 27 | 28 | export function getLogScopeId(): number { 29 | return scopeCounter; 30 | } 31 | 32 | export function getNextLogScopeId(): number { 33 | if (scopeCounter === maxSmallIntegerV8) { 34 | scopeCounter = 0; 35 | } 36 | return ++scopeCounter; 37 | } 38 | 39 | export function setLogScope(scopeId: number, scope: LogScope) { 40 | scopes.set(scopeId, scope); 41 | } 42 | -------------------------------------------------------------------------------- /src/system/logger.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from './logger.constants'; 2 | import type { LogScope } from './logger.scope'; 3 | 4 | const emptyStr = ''; 5 | 6 | const enum OrderedLevel { 7 | Off = 0, 8 | Error = 1, 9 | Warn = 2, 10 | Info = 3, 11 | Debug = 4, 12 | } 13 | 14 | export interface LogChannelProvider { 15 | readonly name: string; 16 | createChannel(name: string): LogChannel; 17 | toLoggable?(o: unknown): string | undefined; 18 | } 19 | 20 | export interface LogChannel { 21 | readonly name: string; 22 | appendLine(value: string): void; 23 | dispose?(): void; 24 | show?(preserveFocus?: boolean): void; 25 | } 26 | 27 | export const Logger = new (class Logger { 28 | private output: LogChannel | undefined; 29 | private provider: LogChannelProvider | undefined; 30 | 31 | configure(provider: LogChannelProvider, logLevel: LogLevel, debugging: boolean = false) { 32 | this.provider = provider; 33 | 34 | this._isDebugging = debugging; 35 | this.logLevel = logLevel; 36 | } 37 | 38 | enabled(level: LogLevel): boolean { 39 | return this.level >= toOrderedLevel(level); 40 | } 41 | 42 | private _isDebugging = false; 43 | get isDebugging() { 44 | return this._isDebugging; 45 | } 46 | 47 | private level: OrderedLevel = OrderedLevel.Off; 48 | private _logLevel: LogLevel = LogLevel.Off; 49 | get logLevel(): LogLevel { 50 | return this._logLevel; 51 | } 52 | set logLevel(value: LogLevel) { 53 | this._logLevel = value; 54 | this.level = toOrderedLevel(this._logLevel); 55 | 56 | if (value === LogLevel.Off) { 57 | this.output?.dispose?.(); 58 | this.output = undefined; 59 | } else { 60 | this.output = this.output ?? this.provider!.createChannel(this.provider!.name); 61 | } 62 | } 63 | 64 | get timestamp(): string { 65 | return `[${new Date().toISOString().replace(/T/, ' ').slice(0, -1)}]`; 66 | } 67 | 68 | debug(message: string, ...params: any[]): void; 69 | debug(scope: LogScope | undefined, message: string, ...params: any[]): void; 70 | debug(scopeOrMessage: LogScope | string | undefined, ...params: any[]): void { 71 | if (this.level < OrderedLevel.Debug && !this.isDebugging) return; 72 | 73 | let message; 74 | if (typeof scopeOrMessage === 'string') { 75 | message = scopeOrMessage; 76 | } else { 77 | message = params.shift(); 78 | 79 | if (scopeOrMessage != null) { 80 | message = `${scopeOrMessage.prefix} ${message ?? emptyStr}`; 81 | } 82 | } 83 | 84 | if (this.isDebugging) { 85 | console.log(this.timestamp, `[${this.provider!.name}]`, message ?? emptyStr, ...params); 86 | } 87 | 88 | if (this.output == null || this.level < OrderedLevel.Debug) return; 89 | this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(true, params)}`); 90 | } 91 | 92 | error(ex: Error | unknown, message?: string, ...params: any[]): void; 93 | error(ex: Error | unknown, scope?: LogScope, message?: string, ...params: any[]): void; 94 | error(ex: Error | unknown, scopeOrMessage: LogScope | string | undefined, ...params: any[]): void { 95 | if (this.level < OrderedLevel.Error && !this.isDebugging) return; 96 | 97 | let message; 98 | if (scopeOrMessage == null || typeof scopeOrMessage === 'string') { 99 | message = scopeOrMessage; 100 | } else { 101 | message = `${scopeOrMessage.prefix} ${params.shift() ?? emptyStr}`; 102 | } 103 | 104 | if (message == null) { 105 | const stack = ex instanceof Error ? ex.stack : undefined; 106 | if (stack) { 107 | const match = /.*\s*?at\s(.+?)\s/.exec(stack); 108 | if (match != null) { 109 | message = match[1]; 110 | } 111 | } 112 | } 113 | 114 | if (this.isDebugging) { 115 | console.error(this.timestamp, `[${this.provider!.name}]`, message ?? emptyStr, ...params, ex); 116 | } 117 | 118 | if (this.output == null || this.level < OrderedLevel.Error) return; 119 | this.output.appendLine( 120 | `${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}\n${String(ex)}`, 121 | ); 122 | } 123 | 124 | log(message: string, ...params: any[]): void; 125 | log(scope: LogScope | undefined, message: string, ...params: any[]): void; 126 | log(scopeOrMessage: LogScope | string | undefined, ...params: any[]): void { 127 | if (this.level < OrderedLevel.Info && !this.isDebugging) return; 128 | 129 | let message; 130 | if (typeof scopeOrMessage === 'string') { 131 | message = scopeOrMessage; 132 | } else { 133 | message = params.shift(); 134 | 135 | if (scopeOrMessage != null) { 136 | message = `${scopeOrMessage.prefix} ${message ?? emptyStr}`; 137 | } 138 | } 139 | 140 | if (this.isDebugging) { 141 | console.log(this.timestamp, `[${this.provider!.name}]`, message ?? emptyStr, ...params); 142 | } 143 | 144 | if (this.output == null || this.level < OrderedLevel.Info) return; 145 | this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`); 146 | } 147 | 148 | warn(message: string, ...params: any[]): void; 149 | warn(scope: LogScope | undefined, message: string, ...params: any[]): void; 150 | warn(scopeOrMessage: LogScope | string | undefined, ...params: any[]): void { 151 | if (this.level < OrderedLevel.Warn && !this.isDebugging) return; 152 | 153 | let message; 154 | if (typeof scopeOrMessage === 'string') { 155 | message = scopeOrMessage; 156 | } else { 157 | message = params.shift(); 158 | 159 | if (scopeOrMessage != null) { 160 | message = `${scopeOrMessage.prefix} ${message ?? emptyStr}`; 161 | } 162 | } 163 | 164 | if (this.isDebugging) { 165 | console.warn(this.timestamp, `[${this.provider!.name}]`, message ?? emptyStr, ...params); 166 | } 167 | 168 | if (this.output == null || this.level < OrderedLevel.Warn) return; 169 | this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`); 170 | } 171 | 172 | showOutputChannel(preserveFocus?: boolean): void { 173 | this.output?.show?.(preserveFocus); 174 | } 175 | 176 | toLoggable(o: any, sanitize?: ((key: string, value: any) => any) | undefined) { 177 | if (typeof o !== 'object') return String(o); 178 | 179 | const loggable = this.provider!.toLoggable?.(o); 180 | if (loggable != null) return loggable; 181 | 182 | try { 183 | return JSON.stringify(o, sanitize); 184 | } catch { 185 | return ''; 186 | } 187 | } 188 | 189 | private toLoggableParams(debugOnly: boolean, params: any[]) { 190 | if (params.length === 0 || (debugOnly && this.level < OrderedLevel.Debug && !this.isDebugging)) { 191 | return emptyStr; 192 | } 193 | 194 | const loggableParams = params.map(p => this.toLoggable(p)).join(', '); 195 | return loggableParams.length !== 0 ? ` \u2014 ${loggableParams}` : emptyStr; 196 | } 197 | })(); 198 | 199 | function toOrderedLevel(logLevel: LogLevel): OrderedLevel { 200 | switch (logLevel) { 201 | case LogLevel.Off: 202 | return OrderedLevel.Off; 203 | case LogLevel.Error: 204 | return OrderedLevel.Error; 205 | case LogLevel.Warn: 206 | return OrderedLevel.Warn; 207 | case LogLevel.Info: 208 | return OrderedLevel.Info; 209 | case LogLevel.Debug: 210 | return OrderedLevel.Debug; 211 | default: 212 | return OrderedLevel.Off; 213 | } 214 | } 215 | 216 | export function getLoggableName(instance: Function | object) { 217 | let name: string; 218 | if (typeof instance === 'function') { 219 | if (instance.prototype?.constructor == null) return instance.name; 220 | 221 | name = instance.prototype.constructor.name ?? emptyStr; 222 | } else { 223 | name = instance.constructor?.name ?? emptyStr; 224 | } 225 | 226 | // Strip webpack module name (since I never name classes with an _) 227 | const index = name.indexOf('_'); 228 | return index === -1 ? name : name.substr(index + 1); 229 | } 230 | 231 | export interface LogProvider { 232 | enabled(logLevel: LogLevel): boolean; 233 | log(logLevel: LogLevel, scope: LogScope | undefined, message: string, ...params: any[]): void; 234 | } 235 | 236 | export const defaultLogProvider: LogProvider = { 237 | enabled: (logLevel: LogLevel) => Logger.enabled(logLevel), 238 | log: (logLevel: LogLevel, scope: LogScope | undefined, message: string, ...params: any[]) => { 239 | switch (logLevel) { 240 | case LogLevel.Error: 241 | Logger.error('', scope, message, ...params); 242 | break; 243 | case LogLevel.Warn: 244 | Logger.warn(scope, message, ...params); 245 | break; 246 | case LogLevel.Info: 247 | Logger.log(scope, message, ...params); 248 | break; 249 | default: 250 | Logger.debug(scope, message, ...params); 251 | break; 252 | } 253 | }, 254 | }; 255 | -------------------------------------------------------------------------------- /src/system/object.ts: -------------------------------------------------------------------------------- 1 | export function areEqual(a: any, b: any): boolean { 2 | if (a === b) return true; 3 | if (a == null || b == null) return false; 4 | 5 | const aType = typeof a; 6 | if (aType === typeof b && (aType === 'string' || aType === 'number' || aType === 'boolean')) return false; 7 | 8 | return JSON.stringify(a) === JSON.stringify(b); 9 | } 10 | -------------------------------------------------------------------------------- /src/system/promise.ts: -------------------------------------------------------------------------------- 1 | import type { CancellationToken, Disposable } from 'vscode'; 2 | 3 | export class PromiseCancelledError = Promise> extends Error { 4 | constructor(public readonly promise: T, message: string) { 5 | super(message); 6 | } 7 | } 8 | 9 | export class PromiseCancelledErrorWithId = Promise> extends PromiseCancelledError { 10 | constructor(public readonly id: TKey, promise: T, message: string) { 11 | super(promise, message); 12 | } 13 | } 14 | 15 | export function cancellable( 16 | promise: Promise, 17 | timeoutOrToken?: number | CancellationToken, 18 | options: { 19 | cancelMessage?: string; 20 | onDidCancel?(resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void): void; 21 | } = {}, 22 | ): Promise { 23 | if (timeoutOrToken == null || (typeof timeoutOrToken === 'number' && timeoutOrToken <= 0)) return promise; 24 | 25 | return new Promise((resolve, reject) => { 26 | let fulfilled = false; 27 | let timer: ReturnType | undefined; 28 | let disposable: Disposable | undefined; 29 | 30 | if (typeof timeoutOrToken === 'number') { 31 | timer = setTimeout(() => { 32 | if (typeof options.onDidCancel === 'function') { 33 | options.onDidCancel(resolve, reject); 34 | } else { 35 | reject(new PromiseCancelledError(promise, options.cancelMessage ?? 'TIMED OUT')); 36 | } 37 | }, timeoutOrToken); 38 | } else { 39 | disposable = timeoutOrToken.onCancellationRequested(() => { 40 | disposable?.dispose(); 41 | if (fulfilled) return; 42 | 43 | if (typeof options.onDidCancel === 'function') { 44 | options.onDidCancel(resolve, reject); 45 | } else { 46 | reject(new PromiseCancelledError(promise, options.cancelMessage ?? 'CANCELLED')); 47 | } 48 | }); 49 | } 50 | 51 | promise.then( 52 | () => { 53 | fulfilled = true; 54 | if (timer != null) { 55 | clearTimeout(timer); 56 | } 57 | disposable?.dispose(); 58 | resolve(promise); 59 | }, 60 | ex => { 61 | fulfilled = true; 62 | if (timer != null) { 63 | clearTimeout(timer); 64 | } 65 | disposable?.dispose(); 66 | reject(ex); 67 | }, 68 | ); 69 | }); 70 | } 71 | 72 | export function isPromise(obj: PromiseLike | T): obj is Promise { 73 | return obj instanceof Promise || typeof (obj as PromiseLike)?.then === 'function'; 74 | } 75 | -------------------------------------------------------------------------------- /src/system/storage.ts: -------------------------------------------------------------------------------- 1 | import type { Disposable, Event, ExtensionContext, SecretStorageChangeEvent } from 'vscode'; 2 | import { EventEmitter } from 'vscode'; 3 | import type { 4 | DeprecatedGlobalStorage, 5 | DeprecatedWorkspaceStorage, 6 | GlobalStorage, 7 | SecretKeys, 8 | WorkspaceStorage, 9 | } from '../constants'; 10 | import { extensionPrefix } from '../constants'; 11 | import { debug } from './decorators/log'; 12 | 13 | export type StorageChangeEvent = 14 | | { 15 | /** 16 | * The key of the stored value that has changed. 17 | */ 18 | readonly key: keyof (GlobalStorage & DeprecatedGlobalStorage); 19 | readonly workspace: false; 20 | } 21 | | { 22 | /** 23 | * The key of the stored value that has changed. 24 | */ 25 | readonly key: keyof (WorkspaceStorage & DeprecatedWorkspaceStorage); 26 | readonly workspace: true; 27 | }; 28 | 29 | export class Storage implements Disposable { 30 | private _onDidChange = new EventEmitter(); 31 | get onDidChange(): Event { 32 | return this._onDidChange.event; 33 | } 34 | 35 | private _onDidChangeSecrets = new EventEmitter(); 36 | get onDidChangeSecrets(): Event { 37 | return this._onDidChangeSecrets.event; 38 | } 39 | 40 | private readonly _disposable: Disposable; 41 | constructor(private readonly context: ExtensionContext) { 42 | this._disposable = this.context.secrets.onDidChange(e => this._onDidChangeSecrets.fire(e)); 43 | } 44 | 45 | dispose(): void { 46 | this._disposable.dispose(); 47 | } 48 | 49 | get(key: T): GlobalStorage[T] | undefined; 50 | /** @deprecated */ 51 | get(key: T): DeprecatedGlobalStorage[T] | undefined; 52 | get(key: T, defaultValue: GlobalStorage[T]): GlobalStorage[T]; 53 | @debug({ logThreshold: 50 }) 54 | get(key: keyof (GlobalStorage & DeprecatedGlobalStorage), defaultValue?: unknown): unknown | undefined { 55 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 56 | return this.context.globalState.get(`${extensionPrefix}:${key}`, defaultValue); 57 | } 58 | 59 | @debug({ logThreshold: 250 }) 60 | async delete(key: keyof (GlobalStorage & DeprecatedGlobalStorage)): Promise { 61 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 62 | await this.context.globalState.update(`${extensionPrefix}:${key}`, undefined); 63 | this._onDidChange.fire({ key: key, workspace: false }); 64 | } 65 | 66 | @debug({ args: { 1: false }, logThreshold: 250 }) 67 | async store(key: T, value: GlobalStorage[T] | undefined): Promise { 68 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 69 | await this.context.globalState.update(`${extensionPrefix}:${key}`, value); 70 | this._onDidChange.fire({ key: key, workspace: false }); 71 | } 72 | 73 | @debug({ args: false, logThreshold: 250 }) 74 | async getSecret(key: SecretKeys): Promise { 75 | return this.context.secrets.get(key); 76 | } 77 | 78 | @debug({ args: false, logThreshold: 250 }) 79 | async deleteSecret(key: SecretKeys): Promise { 80 | return this.context.secrets.delete(key); 81 | } 82 | 83 | @debug({ args: false, logThreshold: 250 }) 84 | async storeSecret(key: SecretKeys, value: string): Promise { 85 | return this.context.secrets.store(key, value); 86 | } 87 | 88 | getWorkspace(key: T): WorkspaceStorage[T] | undefined; 89 | /** @deprecated */ 90 | getWorkspace(key: T): DeprecatedWorkspaceStorage[T] | undefined; 91 | getWorkspace(key: T, defaultValue: WorkspaceStorage[T]): WorkspaceStorage[T]; 92 | @debug({ logThreshold: 25 }) 93 | getWorkspace( 94 | key: keyof (WorkspaceStorage & DeprecatedWorkspaceStorage), 95 | defaultValue?: unknown, 96 | ): unknown | undefined { 97 | return this.context.workspaceState.get(`${extensionPrefix}:${key}`, defaultValue); 98 | } 99 | 100 | @debug({ logThreshold: 250 }) 101 | async deleteWorkspace(key: keyof (WorkspaceStorage & DeprecatedWorkspaceStorage)): Promise { 102 | await this.context.workspaceState.update(`${extensionPrefix}:${key}`, undefined); 103 | this._onDidChange.fire({ key: key, workspace: true }); 104 | } 105 | 106 | @debug({ args: { 1: false }, logThreshold: 250 }) 107 | async storeWorkspace( 108 | key: T, 109 | value: WorkspaceStorage[T] | undefined, 110 | ): Promise { 111 | await this.context.workspaceState.update(`${extensionPrefix}:${key}`, value); 112 | this._onDidChange.fire({ key: key, workspace: true }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/system/string.ts: -------------------------------------------------------------------------------- 1 | import { hrtime } from '@env/hrtime'; 2 | 3 | let compareCollator: Intl.Collator | undefined; 4 | export function compareIgnoreCase(a: string, b: string): 0 | -1 | 1 { 5 | if (compareCollator == null) { 6 | compareCollator = new Intl.Collator(undefined, { sensitivity: 'accent' }); 7 | } 8 | const result = compareCollator.compare(a, b); 9 | // Intl.Collator.compare isn't guaranteed to always return 1 or -1 on all platforms so normalize it 10 | return result === 0 ? 0 : result > 0 ? 1 : -1; 11 | } 12 | 13 | export function equalsIgnoreCase(a: string | null | undefined, b: string | null | undefined): boolean { 14 | // Treat `null` & `undefined` as equivalent 15 | if (a == null && b == null) return true; 16 | if (a == null || b == null) return false; 17 | return compareIgnoreCase(a, b) === 0; 18 | } 19 | 20 | export function getDurationMilliseconds(start: [number, number]) { 21 | const [secs, nanosecs] = hrtime(start); 22 | return secs * 1000 + Math.floor(nanosecs / 1000000); 23 | } 24 | -------------------------------------------------------------------------------- /src/system/version.ts: -------------------------------------------------------------------------------- 1 | import { compareIgnoreCase } from './string'; 2 | 3 | declare type VersionComparisonResult = -1 | 0 | 1; 4 | 5 | export interface Version { 6 | major: number; 7 | minor: number; 8 | patch: number; 9 | pre?: string; 10 | } 11 | 12 | export function compare(v1: string | Version, v2: string | Version): VersionComparisonResult { 13 | if (typeof v1 === 'string') { 14 | v1 = fromString(v1); 15 | } 16 | if (typeof v2 === 'string') { 17 | v2 = fromString(v2); 18 | } 19 | 20 | if (v1.major > v2.major) return 1; 21 | if (v1.major < v2.major) return -1; 22 | 23 | if (v1.minor > v2.minor) return 1; 24 | if (v1.minor < v2.minor) return -1; 25 | 26 | if (v1.patch > v2.patch) return 1; 27 | if (v1.patch < v2.patch) return -1; 28 | 29 | if (v1.pre === undefined && v2.pre !== undefined) return 1; 30 | if (v1.pre !== undefined && v2.pre === undefined) return -1; 31 | 32 | if (v1.pre !== undefined && v2.pre !== undefined) return compareIgnoreCase(v1.pre, v2.pre); 33 | 34 | return 0; 35 | } 36 | 37 | export function from(major: string | number, minor: string | number, patch?: string | number, pre?: string): Version { 38 | return { 39 | major: typeof major === 'string' ? parseInt(major, 10) : major, 40 | minor: typeof minor === 'string' ? parseInt(minor, 10) : minor, 41 | patch: patch == null ? 0 : typeof patch === 'string' ? parseInt(patch, 10) : patch, 42 | pre: pre, 43 | }; 44 | } 45 | 46 | export function fromString(version: string): Version { 47 | const [ver, pre] = version.split('-'); 48 | const [major, minor, patch] = ver.split('.'); 49 | return from(major, minor, patch, pre); 50 | } 51 | 52 | export function satisfies( 53 | v: string | Version | null | undefined, 54 | requirement: `${'=' | '>' | '>=' | '<' | '<='} ${string}`, 55 | ): boolean { 56 | if (v == null) return false; 57 | 58 | const [op, version] = requirement.split(' '); 59 | 60 | if (op === '=') { 61 | return compare(v, version) === 0; 62 | } else if (op === '>') { 63 | return compare(v, version) > 0; 64 | } else if (op === '>=') { 65 | return compare(v, version) >= 0; 66 | } else if (op === '<') { 67 | return compare(v, version) < 0; 68 | } else if (op === '<=') { 69 | return compare(v, version) <= 0; 70 | } 71 | 72 | throw new Error(`Unknown operator: ${op}`); 73 | } 74 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "esModuleInterop": true, 5 | "experimentalDecorators": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "incremental": true, 8 | "isolatedModules": true, 9 | "lib": ["es2022"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": false, 16 | "outDir": "dist", 17 | "resolveJsonModule": true, 18 | "rootDir": "src", 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "strict": true, 22 | "target": "es2022", 23 | "useDefineForClassFields": false, 24 | "useUnknownInCatchVariables": false 25 | }, 26 | "include": ["src/**/*"] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "es2022"], 5 | "paths": { 6 | "@env/*": ["src/env/browser/*"], 7 | "path": ["node_modules/path-browserify"] 8 | }, 9 | "tsBuildInfoFile": "tsconfig.browser.tsbuildinfo" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["src/test/**/*", "src/env/node/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@env/*": ["src/env/node/*"] 6 | }, 7 | "tsBuildInfoFile": "tsconfig.tsbuildinfo" 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["src/test/**/*", "src/env/browser/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 3 | 4 | const { spawnSync } = require('child_process'); 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | const CircularDependencyPlugin = require('circular-dependency-plugin'); 7 | const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin'); 8 | const esbuild = require('esbuild'); 9 | const { EsbuildPlugin } = require('esbuild-loader'); 10 | const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin'); 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const TerserPlugin = require('terser-webpack-plugin'); 14 | const { optimize, WebpackError } = require('webpack'); 15 | 16 | module.exports = 17 | /** 18 | * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; esbuildMinify?: boolean } | undefined } env 19 | * @param {{ mode: 'production' | 'development' | 'none' | undefined }} argv 20 | * @returns { WebpackConfig[] } 21 | */ 22 | function (env, argv) { 23 | const mode = argv.mode || 'none'; 24 | 25 | env = { 26 | analyzeBundle: false, 27 | analyzeDeps: false, 28 | esbuild: true, 29 | esbuildMinify: false, 30 | ...env, 31 | }; 32 | 33 | return [getExtensionConfig('node', mode, env), getExtensionConfig('webworker', mode, env)]; 34 | }; 35 | 36 | /** 37 | * @param { 'node' | 'webworker' } target 38 | * @param { 'production' | 'development' | 'none' } mode 39 | * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; esbuildMinify?: boolean } } env 40 | * @returns { WebpackConfig } 41 | */ 42 | function getExtensionConfig(target, mode, env) { 43 | /** 44 | * @type WebpackConfig['plugins'] | any 45 | */ 46 | const plugins = [ 47 | new CleanPlugin(), 48 | new ForkTsCheckerPlugin({ 49 | async: false, 50 | eslint: { 51 | enabled: true, 52 | files: 'src/**/*.ts?(x)', 53 | options: { 54 | cache: true, 55 | cacheLocation: path.join(__dirname, '.eslintcache/', target === 'webworker' ? 'browser/' : ''), 56 | cacheStrategy: 'content', 57 | fix: mode !== 'production', 58 | overrideConfigFile: path.join( 59 | __dirname, 60 | target === 'webworker' ? '.eslintrc.browser.json' : '.eslintrc.json', 61 | ), 62 | }, 63 | }, 64 | formatter: 'basic', 65 | typescript: { 66 | configFile: path.join(__dirname, target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json'), 67 | }, 68 | }), 69 | ]; 70 | 71 | if (target === 'webworker') { 72 | plugins.push(new optimize.LimitChunkCountPlugin({ maxChunks: 1 })); 73 | } 74 | 75 | if (env.analyzeDeps) { 76 | plugins.push( 77 | new CircularDependencyPlugin({ 78 | cwd: __dirname, 79 | exclude: /node_modules/, 80 | failOnError: false, 81 | onDetected: function ({ module: _webpackModuleRecord, paths, compilation }) { 82 | if (paths.some(p => p.includes('container.ts'))) return; 83 | 84 | // @ts-ignore 85 | compilation.warnings.push(new WebpackError(paths.join(' -> '))); 86 | }, 87 | }), 88 | ); 89 | } 90 | 91 | if (env.analyzeBundle) { 92 | const out = path.join(__dirname, 'out'); 93 | if (!fs.existsSync(out)) { 94 | fs.mkdirSync(out); 95 | } 96 | 97 | plugins.push( 98 | new BundleAnalyzerPlugin({ 99 | analyzerMode: 'static', 100 | generateStatsFile: true, 101 | openAnalyzer: false, 102 | reportFilename: path.join(out, `extension-${target}-bundle-report.html`), 103 | statsFilename: path.join(out, 'stats.json'), 104 | }), 105 | ); 106 | } 107 | 108 | return { 109 | name: `extension:${target}`, 110 | entry: { 111 | extension: './src/extension.ts', 112 | }, 113 | mode: mode, 114 | target: target, 115 | devtool: mode === 'production' ? false : 'source-map', 116 | output: { 117 | chunkFilename: 'feature-[name].js', 118 | filename: 'toggle-excluded.js', 119 | libraryTarget: 'commonjs2', 120 | path: target === 'webworker' ? path.join(__dirname, 'dist', 'browser') : path.join(__dirname, 'dist'), 121 | }, 122 | optimization: { 123 | minimizer: [ 124 | env.esbuildMinify 125 | ? new EsbuildPlugin({ 126 | drop: ['debugger'], 127 | format: 'cjs', 128 | // Keep the class names otherwise @log won't provide a useful name 129 | keepNames: true, 130 | legalComments: 'none', 131 | minify: true, 132 | target: 'es2022', 133 | treeShaking: true, 134 | }) 135 | : new TerserPlugin({ 136 | extractComments: false, 137 | parallel: true, 138 | terserOptions: { 139 | compress: { 140 | drop_debugger: true, 141 | ecma: 2020, 142 | module: true, 143 | }, 144 | ecma: 2020, 145 | format: { 146 | comments: false, 147 | ecma: 2020, 148 | }, 149 | // Keep the class names otherwise @log won't provide a useful name 150 | keep_classnames: true, 151 | module: true, 152 | }, 153 | }), 154 | ], 155 | splitChunks: 156 | target === 'webworker' 157 | ? false 158 | : { 159 | // Disable all non-async code splitting 160 | chunks: () => false, 161 | cacheGroups: { 162 | default: false, 163 | vendors: false, 164 | }, 165 | }, 166 | }, 167 | externals: { 168 | vscode: 'commonjs vscode', 169 | }, 170 | module: { 171 | rules: [ 172 | { 173 | exclude: /\.d\.ts$/, 174 | include: path.join(__dirname, 'src'), 175 | test: /\.tsx?$/, 176 | use: env.esbuild 177 | ? { 178 | loader: 'esbuild-loader', 179 | options: { 180 | format: 'esm', 181 | implementation: esbuild, 182 | target: ['es2022', 'chrome102', 'node16.14.2'], 183 | tsconfig: path.join( 184 | __dirname, 185 | target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json', 186 | ), 187 | }, 188 | } 189 | : { 190 | loader: 'ts-loader', 191 | options: { 192 | configFile: path.join( 193 | __dirname, 194 | target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json', 195 | ), 196 | experimentalWatchApi: true, 197 | transpileOnly: true, 198 | }, 199 | }, 200 | }, 201 | ], 202 | }, 203 | resolve: { 204 | alias: { 205 | '@env': path.resolve(__dirname, 'src', 'env', target === 'webworker' ? 'browser' : target), 206 | }, 207 | mainFields: target === 'webworker' ? ['browser', 'module', 'main'] : ['module', 'main'], 208 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], 209 | }, 210 | plugins: plugins, 211 | infrastructureLogging: 212 | mode === 'production' 213 | ? undefined 214 | : { 215 | level: 'log', // enables logging required for problem matchers 216 | }, 217 | stats: { 218 | preset: 'errors-warnings', 219 | assets: true, 220 | assetsSort: 'name', 221 | assetsSpace: 100, 222 | colors: true, 223 | env: true, 224 | errorsCount: true, 225 | warningsCount: true, 226 | timings: true, 227 | }, 228 | }; 229 | } 230 | --------------------------------------------------------------------------------