From a23f50239d03f0afcf408ca8b061629604a759d2 Mon Sep 17 00:00:00 2001 From: Nathan Trudeau Date: Sat, 12 Sep 2026 09:42:40 -0400 Subject: [PATCH 1/4] fix: restore colour-scheme opt-in and harden extension lifecycle Restore explicit colour-scheme activation so foreground-only custom highlights retain their configured colours without injected backgrounds. Highlight configuration: - default useColourScheme to false in both settings namespaces and runtime - delegate settings snapshots to config.shouldUseColourScheme() - cancel pending highlights and prune decoration state for hidden editors - skip delayed highlights for closed or no-longer-visible editors Runtime lifecycle and tree interactions: - add an activation scheduler for timeouts, intervals, and immediates - dispose the previous runtime before reactivation and make teardown idempotent across deactivation and subscription disposal - cancel scheduled work, clear scan/document state and highlight caches, settle progress sessions, and dispose output channels and tree resources - guard queued commands, context changes, tree mutations, and asynchronous completions against disposed runtimes - settle pending tree UI turns during teardown without revealing nodes - cancel stale active-editor tracking and recheck editor and settings state before passive reveals - return indexed tree elements directly and preserve reveal error propagation through command boundaries - dispose line-flash decorations without writing to closed editors - add generation queries to repeated exports to avoid cached documents - dispose tree change emitters and pending expansion-state writes once Search and cleanup: - track every active ripgrep process and cancel concurrent searches - guard process completion and pattern cleanup against repeated events - stop consuming output after process completion begins - remove the inert ripgrepMaxBuffer setting from both namespaces, runtime options, legacy migration, localization, documentation, and fixtures - remove unused detection and colour/icon parameters, helper functions, imports, and obsolete runtime state Release and packaging: - restrict the canonical VSIX matrix to nine native targets - remove web target handling from ripgrep mappings, package staging, PR preview labels, and bundle checks - require one matching ripgrep executable and its metadata per target - derive stable and nightly build targets from targets.json - exclude .forge from Git, npm, and VSIX packages - exclude development-container files from npm packages and omit packaging ignore files from the opposite distribution format Documentation and dependencies: - document colour-scheme opt-in, native platform coverage, the Node extension-host requirement, and incremental ripgrep output parsing - correct the foregroundColourScheme setting name in README - add missing Simplified Chinese diagnostic and current-scan commands and translate remaining manifest command and setting strings - refresh browserslist, baseline-browser-mapping, caniuse-lite, electron-to-chromium, node-releases, and update-browserslist-db - refresh brace-expansion, fast-uri, js-yaml, qs, side-channel, and undici, including associated lockfile integrity and dependency metadata Regression coverage and evidence: - cover manifest/runtime opt-in, authoritative snapshot policy, and foreground-only custom highlights while retaining explicit palettes - cover scheduler cancellation, synchronous clocks, repeated disposal, hidden-editor highlights, and pending tree UI teardown - cover stale tracking, reveal failures, closed-editor line flashes, repeated export generations, and activation resource cleanup - assert concurrent ripgrep cancellation and single pattern cleanup - enforce localization coverage, 72 settings per namespace, native target membership, executable metadata, and packaging exclusions - align release, Marketplace, and PR preview fixtures with native targets - extend performance harnesses with editor visibility events and disposal - add the two-scenario highlight latency and RSS comparison artifact - refresh the regex-registry equivalence report Fixes #110 --- .github/workflows/latest.yml | 7 +- .github/workflows/release.yml | 7 +- .gitignore | 2 + .npmignore | 3 + .vscodeignore | 2 + README.md | 13 +- .../accuracy/regex-registry-equivalence.md | 10 +- artifacts/perf/issue110-highlight-opt-in.md | 66 +++ package-lock.json | 198 +++++---- package.json | 16 +- package.nls.json | 2 - package.nls.zh-cn.json | 78 ++-- scripts/ci/sync-pr-vsix-comment.mjs | 3 +- scripts/ci/verify-pr-vsix.mjs | 9 +- scripts/perf/extensionScenarios.js | 5 +- scripts/perf/run-all.js | 6 + scripts/release/build-vsix.mjs | 7 +- scripts/release/ripgrep-targets.mjs | 3 +- scripts/release/targets.json | 3 +- src/colours.js | 5 +- src/config.js | 2 +- src/detection.js | 8 +- src/extension.js | 398 ++++++++++++++---- src/highlights.js | 43 +- src/icons.js | 2 +- src/ripgrep.js | 77 +++- src/runtime/activationScheduler.js | 120 ++++++ src/runtime/settingsSnapshot.js | 2 +- src/searchResults.js | 5 - src/tree.js | 17 +- src/utils.js | 5 - test/activationScheduler.behavior.test.js | 126 ++++++ test/config.behavior.test.js | 13 + test/extension.scan-parity.test.js | 355 +++++++++++++++- test/highlights.behavior.test.js | 174 +++++++- test/highlights.matrix.test.js | 4 + test/package.manifest.test.js | 91 +++- test/packaging.ignore.test.js | 5 + test/pr-vsix-bundle.test.js | 19 +- test/pr-vsix-comment.test.js | 9 +- test/release.workflow-scripts.test.js | 34 +- test/ripgrep.test.js | 29 +- test/settings.compatibility.test.js | 4 +- test/settingsSnapshot.behavior.test.js | 60 +++ test/tree.behavior.test.js | 7 + test/tree.matrix.test.js | 1 + test/workflows.github.test.js | 7 + 47 files changed, 1703 insertions(+), 359 deletions(-) create mode 100644 artifacts/perf/issue110-highlight-opt-in.md create mode 100644 src/runtime/activationScheduler.js create mode 100644 test/activationScheduler.behavior.test.js create mode 100644 test/settingsSnapshot.behavior.test.js diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml index 07f646cd..d8e9d660 100644 --- a/.github/workflows/latest.yml +++ b/.github/workflows/latest.yml @@ -22,6 +22,7 @@ jobs: release_ref: ${{ steps.meta.outputs.release_ref }} release_sha: ${{ steps.meta.outputs.release_sha }} release_short_sha: ${{ steps.meta.outputs.release_short_sha }} + targets: ${{ steps.targets.outputs.targets }} steps: - name: Check out repository @@ -52,6 +53,10 @@ jobs: echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" echo "release_short_sha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT" + - name: Resolve platform targets + id: targets + run: echo "targets=$(node -p "JSON.stringify(require('./scripts/release/targets.json'))")" >> "$GITHUB_OUTPUT" + - name: Run test suite run: npm test @@ -63,7 +68,7 @@ jobs: uses: ./.github/workflows/reusable-build-vsix.yml with: ref: ${{ needs.preflight.outputs.release_ref }} - targets: '["win32-x64","win32-arm64","linux-x64","linux-arm64","linux-armhf","darwin-x64","darwin-arm64","alpine-x64","alpine-arm64","web"]' + targets: ${{ needs.preflight.outputs.targets }} secrets: inherit github-release: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b2b7c8b..d75d7b49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,7 @@ jobs: prerelease: ${{ steps.meta.outputs.prerelease }} release_ref: ${{ steps.meta.outputs.release_ref }} release_sha: ${{ steps.meta.outputs.release_sha }} + targets: ${{ steps.targets.outputs.targets }} steps: - name: Check out repository @@ -57,6 +58,10 @@ jobs: REF_TYPE: ${{ github.ref_type }} run: bash scripts/release/resolve-release-metadata.sh + - name: Resolve platform targets + id: targets + run: echo "targets=$(node -p "JSON.stringify(require('./scripts/release/targets.json'))")" >> "$GITHUB_OUTPUT" + - name: Run release test suite run: npm test @@ -69,7 +74,7 @@ jobs: with: ref: ${{ needs.preflight.outputs.release_ref }} release_tag: ${{ needs.preflight.outputs.tag }} - targets: '["win32-x64","win32-arm64","linux-x64","linux-arm64","linux-armhf","darwin-x64","darwin-arm64","alpine-x64","alpine-arm64","web"]' + targets: ${{ needs.preflight.outputs.targets }} secrets: inherit publish: diff --git a/.gitignore b/.gitignore index efbdd86a..79f5818f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ artifacts/**/*.log artifacts/perf/runtime-benchmarks.json artifacts/**/*.json Better.TODO.Tree.settings.txt +.forge/ + diff --git a/.npmignore b/.npmignore index d2d77ecc..5b9c1bcb 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,6 @@ .vscode/ +.devcontainer/ +.vscodeignore .gitignore node_modules/ src/ @@ -16,6 +18,7 @@ TODOS_LISTS/ TODOS_LISTS/** .tools/ .act-artifacts/ +.forge/ MIGRATION.md OPEN_VSX_CERTIFICATE_REPORT.md CHANGELOG.upstream.md diff --git a/.vscodeignore b/.vscodeignore index 728bdd2a..e4c79d70 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -17,6 +17,7 @@ TODOS_LISTS/ TODOS_LISTS/** .tools/ .act-artifacts/ +.forge/ MIGRATION.md OPEN_VSX_CERTIFICATE_REPORT.md CHANGELOG.upstream.md @@ -26,3 +27,4 @@ old-*.js *.bak *~ .devcontainer/ +.npmignore diff --git a/README.md b/README.md index 9d424072..dfc3ea89 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Found TODOs can also be highlighted in open files. ## Highlighting ->`better-todo-tree.highlights.useColourScheme` applies foreground and background colour sequences to tags in tag order. +>`better-todo-tree.highlights.useColourScheme` is opt-in. When enabled, it applies foreground and background colour sequences to tags in tag order. Highlighting tags is configurable. Use `defaultHighlight` to set up highlights for all tags. If you need to configure individual tags differently, use `customHighlight`. If settings are not specified in `customHighlight`, the value from `defaultHighlight` is used. @@ -178,6 +178,8 @@ Alternatively, open Visual Studio code, press `Ctrl+P` or `Cmd+P` and type: > ext install FanaticPythoner.better-todo-tree +Native packages cover Windows x64/Arm64, Linux x64/Arm64/Armhf, Alpine x64/Arm64, and macOS x64/Arm64. Browser-hosted VS Code is excluded because workspace scanning requires the Node extension host and the packaged ripgrep executable. + ### Source Code The source code is available on GitHub [here](https://github.com/FanaticPythoner/better-todo-tree). @@ -380,15 +382,15 @@ Set highlights per tag (or tag group). Example: } ``` -**better-todo-tree.highlights.useColourScheme** (`true`)
-Applies foreground and background colour sequences to tags in tag order. This is a smaller configuration surface than per-tag `customHighlight` entries. +**better-todo-tree.highlights.useColourScheme** (`false`)
+Enable foreground and background colour sequences for tags in tag order. Custom and default highlight styles remain unchanged while this setting is disabled. *Note: The colour scheme overrides the colours defined in* `better-todo-tree.highlights.defaultHighlight` *but not* `better-todo-tree.highlights.customHighlight`*.* **better-todo-tree.highlights.backgroundColourScheme** (`["red","orange","yellow","green","blue","indigo","violet"]`)
Defines colours for use in conjunction with `better-todo-tree.highlights.useColourScheme` to colour highlights. Colours can be defined in the same way as other colours (e.g. hex code, theme names, etc.). If there are more tags than colours, the sequence is repeated. -**better-todo-tree.highlights.foreroundColourScheme** (`["white","black","black","white","white","white","black"]`)
+**better-todo-tree.highlights.foregroundColourScheme** (`["white","black","black","white","white","white","black"]`)
Defines colours for use in conjunction with `better-todo-tree.highlights.backgroundColourScheme` to colour highlights. These colours should be complementary to the background colours. **better-todo-tree.regex.enableMultiLine** (`false`)
@@ -419,8 +421,7 @@ Empty value uses the packaged ripgrep binary. Set an absolute executable path to **better-todo-tree.ripgrep.ripgrepArgs** (`"--max-columns=1000"`)
Pass additional arguments to ripgrep. Engine arguments such as `--pcre2`, `-P`, and `--engine` override automatic engine selection. -**better-todo-tree.ripgrep.ripgrepMaxBuffer** (`200`)
-By default, the ripgrep process will have a buffer of 200KB. However, this is sometimes not enough for all the tags you might want to see. This setting can be used to increase the buffer size accordingly. +Ripgrep output is parsed incrementally. No output-buffer size setting is required. **better-todo-tree.ripgrep.usePatternFile** (`true`)
A pattern file is used with ripgrep by default. If you experience issues with deleting the pattern file, set this to false to use the legacy method of providing the regex to ripgrep. diff --git a/artifacts/accuracy/regex-registry-equivalence.md b/artifacts/accuracy/regex-registry-equivalence.md index 4c874c4c..8c98c132 100644 --- a/artifacts/accuracy/regex-registry-equivalence.md +++ b/artifacts/accuracy/regex-registry-equivalence.md @@ -5,18 +5,18 @@ | Baseline regex entries | 118 | | Baseline unique regexes | 83 | | Current hardcoded regex entries | 0 | -| Registry fragments | 188 | -| Registry patterns | 210 | +| Registry fragments | 202 | +| Registry patterns | 224 | | Source coverage | 83/83 | -| Behavior parity | 73/73 | +| Behavior parity | 72/72 | | Compiled baseline regexes | 83/83 | -| Elapsed ms | 147.762 | +| Elapsed ms | 165.377 | ## Behavior Matrix | Group | Passed | Total | | --- | ---: | ---: | -| utils | 11 | 11 | +| utils | 10 | 10 | | attributes | 5 | 5 | | detection | 5 | 5 | | regexEngine | 52 | 52 | diff --git a/artifacts/perf/issue110-highlight-opt-in.md b/artifacts/perf/issue110-highlight-opt-in.md new file mode 100644 index 00000000..b4e1d0b8 --- /dev/null +++ b/artifacts/perf/issue110-highlight-opt-in.md @@ -0,0 +1,66 @@ +# Runtime Benchmarks + +- Baseline ref: `HEAD` +- Current source: working tree +- Node: `v25.2.0` +- Selection mode: `scenario-list` +- Declared suite: `user-flow` +- Result-count validation: `2 rows, suite-consistent=true, all-user-flow=true` + +## Machine Profile + +| Category | Field | Value | +| --- | --- | --- | +| Host | Hostname | n00ne-AERO-17-YD | +| Host | OS | Ubuntu 22.04.5 LTS | +| Host | Kernel | 6.8.0-138-generic | +| Host | Architecture | x64 | +| Host | Load Average | 4.04, 5.25, 5.5 | +| Host | Available Parallelism | - | +| CPU | Model | Intel(R) Core(TM) i9-14900HX | +| CPU | Vendor | GenuineIntel | +| CPU | Topology | 16 logical CPU(s), 2 thread(s)/core, 8 core(s)/socket, 1 socket(s), 1 NUMA node(s) | +| CPU | Frequency | 800 MHz to 5,800 MHz | +| CPU | Cache | L1d 384 KiB (8 instances), L1i 256 KiB (8 instances), L2 16 MiB (8 instances), L3 36 MiB (1 instance) | +| Memory | Total RAM | 62.51 GiB (`67,119,751,168 bytes`) | +| Memory | Available At Collection | 7.86 GiB (`8,434,503,680 bytes`) | +| Memory | Online Physical RAM | 66.00 GiB (`70,866,960,384 bytes`) | +| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 86.92 GiB (`93,329,948,672 bytes`) | +| Memory | DMI / SPD | Unavailable: /sys/firmware/dmi/tables/smbios_entry_point: Permission denied /dev/mem: Permission denied | +| Storage | Root Device | nvme0n1 (Samsung SSD 9100 PRO 4TB), 3.64 TiB (`4,000,787,030,016 bytes`), transport nvme, rotational=false, readOnly=false | + +## Scenario Model + +| Scenario | Kind | User flow | Measurement scope | Input model | +| --- | --- | --- | --- | --- | +| visible-editor-highlight-open-file | user-flow | Focus or open a visible editor and apply highlights to that editor. | Active-editor event handling, decoration creation/update, and highlight application. | Fixture scan results fed into the real highlight pipeline in a VS Code event harness. | +| visible-editor-custom-highlight-config-open-file | user-flow | Open a visible editor while a large custom highlight configuration is active and apply highlights. | Custom-highlight attribute lookup, decoration creation/update, and highlight application. | Fixture scan results plus a large custom-highlight config in a VS Code event harness. | + +## Metric Model + +| Table | Value model | Accuracy model | +| --- | --- | --- | +| Latency | Wall-clock elapsed time around each harness flow iteration, summarized as min/p50/p90/p95/max. | Exact for each sampled iteration in this run. | +| Profiled RSS Burst | Difference between the isolated scenario worker RSS measured immediately before the flow and that worker iteration's OS high-water-mark peak RSS. | Exact for the measured worker iteration, using `process.memoryUsage().rss` at flow start and `process.resourceUsage().maxRSS` for the peak. | +| Profiled Peak RSS | Highest process RSS reached by each isolated scenario worker iteration. | Exact worker-process high-water mark from `process.resourceUsage().maxRSS`. | + +## Latency + +| Scenario | Kind | Baseline p50 ms | Current p50 ms | Baseline p90 ms | Current p90 ms | Baseline p95 ms | Current p95 ms | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| visible-editor-highlight-open-file | user-flow | 2.43 | 2.4 | 2.63 | 2.56 | 2.81 | 2.65 | +| visible-editor-custom-highlight-config-open-file | user-flow | 3.19 | 3.32 | 4.29 | 4.29 | 4.42 | 4.44 | + +## Profiled RSS Burst + +| Scenario | Kind | Baseline p50 MiB | Current p50 MiB | Baseline p90 MiB | Current p90 MiB | Baseline p95 MiB | Current p95 MiB | Baseline Max MiB | Current Max MiB | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| visible-editor-highlight-open-file | user-flow | 15.11 | 14.75 | 15.61 | 15.13 | 15.82 | 15.33 | 15.82 | 15.33 | +| visible-editor-custom-highlight-config-open-file | user-flow | 15.69 | 15.46 | 15.99 | 16.07 | 16.23 | 16.25 | 16.23 | 16.25 | + +## Profiled Peak RSS + +| Scenario | Kind | Baseline p50 RSS MiB | Current p50 RSS MiB | Baseline p90 RSS MiB | Current p90 RSS MiB | Baseline p95 RSS MiB | Current p95 RSS MiB | Baseline Max RSS MiB | Current Max RSS MiB | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| visible-editor-highlight-open-file | user-flow | 72.21 | 71.64 | 72.56 | 72.01 | 72.72 | 72.09 | 72.72 | 72.09 | +| visible-editor-custom-highlight-config-open-file | user-flow | 72.5 | 72.43 | 72.85 | 72.97 | 73.22 | 73.04 | 73.22 | 73.04 | diff --git a/package-lock.json b/package-lock.json index 9c66754b..a398635e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1472,6 +1472,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1508,6 +1509,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1660,9 +1662,9 @@ "optional": true }, "node_modules/baseline-browser-mapping": { - "version": "2.10.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", - "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1716,10 +1718,11 @@ "license": "BSD-2-Clause" }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -1740,9 +1743,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -1759,12 +1762,13 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -1870,9 +1874,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001787", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", - "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -2356,9 +2360,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.334", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", - "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "version": "1.5.425", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.425.tgz", + "integrity": "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==", "dev": true, "license": "ISC" }, @@ -2609,9 +2613,9 @@ "integrity": "sha1-7hsUbWfN31AD6RJwzKIiUFtH0/M=" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "dev": true, "funding": [ { @@ -2622,7 +2626,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.20.1", @@ -3341,9 +3346,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -3355,6 +3360,7 @@ "url": "https://github.com/sponsors/nodeca" } ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3879,11 +3885,14 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/node-sarif-builder": { "version": "3.4.0", @@ -4366,12 +4375,14 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -4779,15 +4790,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -5399,10 +5410,11 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=20.18.1" } @@ -5437,9 +5449,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { @@ -6882,7 +6894,8 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true + "dev": true, + "peer": true }, "acorn-import-phases": { "version": "1.0.4", @@ -6902,6 +6915,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6993,9 +7007,9 @@ "optional": true }, "baseline-browser-mapping": { - "version": "2.10.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", - "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true }, "binaryextensions": { @@ -7032,9 +7046,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -7049,16 +7063,17 @@ } }, "browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, + "peer": true, "requires": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" } }, "buffer": { @@ -7120,9 +7135,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001787", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", - "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true }, "chalk": { @@ -7436,9 +7451,9 @@ } }, "electron-to-chromium": { - "version": "1.5.334", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", - "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "version": "1.5.425", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.425.tgz", + "integrity": "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==", "dev": true }, "emoji-regex": { @@ -7611,9 +7626,9 @@ "integrity": "sha1-7hsUbWfN31AD6RJwzKIiUFtH0/M=" }, "fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "dev": true }, "fastq": { @@ -8076,9 +8091,9 @@ "dev": true }, "js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -8419,9 +8434,9 @@ "optional": true }, "node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true }, "node-sarif-builder": { @@ -8753,12 +8768,13 @@ "dev": true }, "qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, "requires": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" } }, "queue-microtask": { @@ -9004,14 +9020,14 @@ "dev": true }, "side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "requires": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } @@ -9426,9 +9442,9 @@ "dev": true }, "undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true }, "undici-types": { @@ -9450,9 +9466,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "requires": { "escalade": "^3.2.0", diff --git a/package.json b/package.json index 71f1fee2..e0ab3aad 100644 --- a/package.json +++ b/package.json @@ -1604,12 +1604,12 @@ "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" }, "better-todo-tree.highlights.useColourScheme": { - "default": true, + "default": false, "markdownDescription": "%better-todo-tree.configuration.highlights.useColourScheme.markdownDescription%", "type": "boolean" }, "todo-tree.highlights.useColourScheme": { - "default": true, + "default": false, "markdownDescription": "%better-todo-tree.configuration.highlights.useColourScheme.markdownDescription%", "type": "boolean", "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", @@ -2371,18 +2371,6 @@ "order": 8, "type": "object", "properties": { - "better-todo-tree.ripgrep.ripgrepMaxBuffer": { - "default": 200, - "markdownDescription": "%better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription%", - "type": "integer" - }, - "todo-tree.ripgrep.ripgrepMaxBuffer": { - "default": 200, - "markdownDescription": "%better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription%", - "type": "integer", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.ripgrep.usePatternFile": { "default": true, "markdownDescription": "%better-todo-tree.configuration.ripgrep.usePatternFile.markdownDescription%", diff --git a/package.nls.json b/package.nls.json index 408a8345..bc76cf62 100644 --- a/package.nls.json +++ b/package.nls.json @@ -218,8 +218,6 @@ "better-todo-tree.configuration.ripgrep.ripgrep.markdownDescription": "Custom ripgrep executable path. Empty value uses the packaged ripgrep binary.", "todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "Additional arguments to pass to ripgrep. Engine arguments such as `--pcre2`, `-P`, and `--engine` override automatic engine selection.", "better-todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "Additional arguments to pass to ripgrep. Engine arguments such as `--pcre2`, `-P`, and `--engine` override automatic engine selection.", - "todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "Size of the buffer to use for retrieving output from ripgrep (kilobytes).", - "better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "Size of the buffer to use for retrieving output from ripgrep (kilobytes).", "todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "A pattern file is used with ripgrep by default. Set to false to use the legacy regex argument mode.", "better-todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "A pattern file is used with ripgrep by default. Set to false to use the legacy regex argument mode.", "todo-tree.configuration.title": "Better Todo Tree", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 704bf2fb..b7dd713b 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -18,16 +18,18 @@ "better-todo-tree.command.expand.title": "展开树状图", "todo-tree.command.exportTree.title": "导出树状图", "better-todo-tree.command.exportTree.title": "导出树状图", + "todo-tree.command.exportScanDiagnostics.title": "导出扫描诊断", + "better-todo-tree.command.exportScanDiagnostics.title": "导出扫描诊断", "todo-tree.command.filter.title": "过滤器", "better-todo-tree.command.filter.title": "过滤器", "todo-tree.command.filterClear.title": "重置树状图过滤器", "better-todo-tree.command.filterClear.title": "重置树状图过滤器", - "todo-tree.command.goToNext.title": "Go To Next", - "better-todo-tree.command.goToNext.title": "Go To Next", - "todo-tree.command.goToPrevious.title": "Go To Previous", - "better-todo-tree.command.goToPrevious.title": "Go To Previous", - "todo-tree.command.groupBySubTag.title": "Group by Sub Tag", - "better-todo-tree.command.groupBySubTag.title": "Group by Sub Tag", + "todo-tree.command.goToNext.title": "下一项", + "better-todo-tree.command.goToNext.title": "下一项", + "todo-tree.command.goToPrevious.title": "上一项", + "better-todo-tree.command.goToPrevious.title": "上一项", + "todo-tree.command.groupBySubTag.title": "按子标签分组", + "better-todo-tree.command.groupBySubTag.title": "按子标签分组", "todo-tree.command.groupByTag.title": "按标签分组", "better-todo-tree.command.groupByTag.title": "按标签分组", "todo-tree.command.refresh.title": "刷新", @@ -36,14 +38,16 @@ "better-todo-tree.command.removeFilter.title": "移除过滤器", "todo-tree.command.removeTag.title": "移除标签类型", "better-todo-tree.command.removeTag.title": "移除标签类型", + "todo-tree.command.openCurrentScanFile.title": "打开当前扫描文件", + "better-todo-tree.command.openCurrentScanFile.title": "打开当前扫描文件", "todo-tree.command.resetAllFilters.title": "重置所有过滤器", "better-todo-tree.command.resetAllFilters.title": "重置所有过滤器", "todo-tree.command.resetCache.title": "重置缓存", "better-todo-tree.command.resetCache.title": "重置缓存", "todo-tree.command.reveal.title": "在树状图中显示当前文件", "better-todo-tree.command.reveal.title": "在树状图中显示当前文件", - "todo-tree.command.revealInFile.title": "Reveal In File", - "better-todo-tree.command.revealInFile.title": "Reveal In File", + "todo-tree.command.revealInFile.title": "在文件中显示", + "better-todo-tree.command.revealInFile.title": "在文件中显示", "todo-tree.command.scanCurrentFileOnly.title": "仅扫描当前文件", "better-todo-tree.command.scanCurrentFileOnly.title": "仅扫描当前文件", "todo-tree.command.scanOpenFilesOnly.title": "仅扫描打开的文件", @@ -64,8 +68,8 @@ "better-todo-tree.command.showTagsOnlyView.title": "仅显示标签", "todo-tree.command.showTreeView.title": "显示树状图", "better-todo-tree.command.showTreeView.title": "显示树状图", - "todo-tree.command.switchScope.title": "Switch Scope", - "better-todo-tree.command.switchScope.title": "Switch Scope", + "todo-tree.command.switchScope.title": "切换范围", + "better-todo-tree.command.switchScope.title": "切换范围", "todo-tree.command.toggleTreeExpansion.title": "切换树展开状态", "better-todo-tree.command.toggleTreeExpansion.title": "切换树展开状态", "todo-tree.command.toggleBadges.title": "显示/隐藏 badges", @@ -74,8 +78,8 @@ "better-todo-tree.command.toggleCompactFolders.title": "紧凑/宽松形式呈现文件夹", "todo-tree.command.toggleItemCounts.title": "显示/隐藏待办事项的数目", "better-todo-tree.command.toggleItemCounts.title": "显示/隐藏待办事项的数目", - "todo-tree.command.ungroupBySubTag.title": "Ungroup by Sub Tag", - "better-todo-tree.command.ungroupBySubTag.title": "Ungroup by Sub Tag", + "todo-tree.command.ungroupBySubTag.title": "取消按子标签分组", + "better-todo-tree.command.ungroupBySubTag.title": "取消按子标签分组", "todo-tree.command.ungroupByTag.title": "取消按标签分组", "better-todo-tree.command.ungroupByTag.title": "取消按标签分组", "todo-tree.command.treeStateBusy.title": "正在处理", @@ -98,8 +102,8 @@ "better-todo-tree.configuration.filtering.includedWorkspaces.markdownDescription": "配置一个工作区名称的列表,列表中的工作区允许作为树状图的根(允许使用通配符)。若列表是空的,则包括所有工作区文件夹。", "todo-tree.configuration.filtering.passGlobsToRipgrep.markdownDescription": "默认设置时 globs 先被传递至 ripgrep 以缩小扫描范围。当此项设置为 false 时,则在扫描后再应用 globs 以限制扫描结果(pre 0.0.132 behaviour)。", "better-todo-tree.configuration.filtering.passGlobsToRipgrep.markdownDescription": "默认设置时 globs 先被传递至 ripgrep 以缩小扫描范围。当此项设置为 false 时,则在扫描后再应用 globs 以限制扫描结果(pre 0.0.132 behaviour)。", - "todo-tree.configuration.filtering.scopes.markdownDescription": "Scopes (sets of globs) that can be switched between", - "better-todo-tree.configuration.filtering.scopes.markdownDescription": "Scopes (sets of globs) that can be switched between", + "todo-tree.configuration.filtering.scopes.markdownDescription": "可在其间切换的范围(glob 集合)。", + "better-todo-tree.configuration.filtering.scopes.markdownDescription": "可在其间切换的范围(glob 集合)。", "todo-tree.configuration.filtering.useBuiltInExcludes.markdownDescription": "通过 VSCode 的 `files.exclude` 和/或 `search.exclude` 列表来确定扫描的忽略路径。", "better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownDescription": "通过 VSCode 的 `files.exclude` 和/或 `search.exclude` 列表来确定扫描的忽略路径。", "todo-tree.configuration.filtering.useBuiltInExcludes.markdownEnumDescriptions.1": "不使用任何内建排除。", @@ -122,8 +126,8 @@ "better-todo-tree.configuration.general.exportPath.markdownDescription": "导出树状图时使用的路径。路径中的环境变量会被展开(例如 `${HOME}`),且路径会传递通过 strftime(参考 )。将拓展名设置为 `.json` 以导出为 JSON 记录。", "todo-tree.configuration.general.fileWatcherGlob.markdownDescription": "与 file watcher 一起使用的 glob。若默认设置导致性能问题,请更改此设置。", "better-todo-tree.configuration.general.fileWatcherGlob.markdownDescription": "与 file watcher 一起使用的 glob。若默认设置导致性能问题,请更改此设置。", - "todo-tree.configuration.general.periodicRefreshInterval.markdownDescription": "Periodic refresh interval (in minutes) for automatically refreshing the tree. Set to '0' to disable.", - "better-todo-tree.configuration.general.periodicRefreshInterval.markdownDescription": "Periodic refresh interval (in minutes) for automatically refreshing the tree. Set to '0' to disable.", + "todo-tree.configuration.general.periodicRefreshInterval.markdownDescription": "自动刷新树的周期(分钟)。设为 `0` 可禁用。", + "better-todo-tree.configuration.general.periodicRefreshInterval.markdownDescription": "自动刷新树的周期(分钟)。设为 `0` 可禁用。", "todo-tree.configuration.general.revealBehaviour.markdownDescription": "控制从侧边栏中选择待办事项时光标的位置或行为。", "better-todo-tree.configuration.general.revealBehaviour.markdownDescription": "控制从侧边栏中选择待办事项时光标的位置或行为。", "todo-tree.configuration.general.revealBehaviour.markdownEnumDescriptions.1": "光标移至待办事项所在行的起始。", @@ -136,10 +140,10 @@ "better-todo-tree.configuration.general.rootFolder.markdownDescription": "指定作树视图的根的文件夹(如不加指定则是工作区文件夹)。文件夹路径中可以使用环境变量,例如 `${workspaceFolder}` 和 `${HOME}`。注意:打开不在根文件夹中的其他文件的待办事项也会在侧边栏中显示,并在文件名后的括号内展示完整路径。", "todo-tree.configuration.general.schemes.markdownDescription": "编辑器计划用于查找TODO。例如,要在设置文件中查找TODO,请添加“ vscode-userdata”,或者对于输出窗口,请添加“ output”。", "better-todo-tree.configuration.general.schemes.markdownDescription": "编辑器计划用于查找TODO。例如,要在设置文件中查找TODO,请添加“ vscode-userdata”,或者对于输出窗口,请添加“ output”。", - "todo-tree.configuration.general.showActivityBarBadge.markdownDescription": "Show a badge in the activity bar indicating the total number of TODOs", - "better-todo-tree.configuration.general.showActivityBarBadge.markdownDescription": "Show a badge in the activity bar indicating the total number of TODOs", - "todo-tree.configuration.general.showIconsInsteadOfTagsInStatusBar.markdownDescription": "Show icons instead of tags in the status bar", - "better-todo-tree.configuration.general.showIconsInsteadOfTagsInStatusBar.markdownDescription": "Show icons instead of tags in the status bar", + "todo-tree.configuration.general.showActivityBarBadge.markdownDescription": "在活动栏中显示待办事项总数徽章。", + "better-todo-tree.configuration.general.showActivityBarBadge.markdownDescription": "在活动栏中显示待办事项总数徽章。", + "todo-tree.configuration.general.showIconsInsteadOfTagsInStatusBar.markdownDescription": "在状态栏中以图标代替标签。", + "better-todo-tree.configuration.general.showIconsInsteadOfTagsInStatusBar.markdownDescription": "在状态栏中以图标代替标签。", "todo-tree.configuration.general.showScanningProgress.markdownDescription": "控制扫描进度的可见位置。", "better-todo-tree.configuration.general.showScanningProgress.markdownDescription": "控制扫描进度的可见位置。", "todo-tree.configuration.general.showScanningProgress.markdownEnumDescriptions.1": "隐藏扫描进度。", @@ -170,8 +174,8 @@ "better-todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.1": "切换状态栏图标显示内容。", "todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.2": "显示资源管理器中的待办事项树。", "better-todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.2": "显示资源管理器中的待办事项树。", - "todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.3": "Toggle highlighting", - "better-todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.3": "Toggle highlighting", + "todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.3": "切换高亮显示。", + "better-todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.3": "切换高亮显示。", "todo-tree.configuration.general.tagGroups.markdownDescription": "将多个相似的标签视作同一种标签组类型,例如 `{ \"FIX\": [\"FIXME\",\"FIXIT\"] }`。注意:所有的标签必须出现于 `better-todo-tree.general.tags` 列表。若一个标签组被定义,自定义突出显示将被应用于整个标签组,而不是组内的标签。", "better-todo-tree.configuration.general.tagGroups.markdownDescription": "将多个相似的标签视作同一种标签组类型,例如 `{ \"FIX\": [\"FIXME\",\"FIXIT\"] }`。注意:所有的标签必须出现于 `better-todo-tree.general.tags` 列表。若一个标签组被定义,自定义突出显示将被应用于整个标签组,而不是组内的标签。", "todo-tree.configuration.general.tags.markdownDescription": "标签的列表。注意:若两个标签具有相同的起始部分,则应该先指定较长的标签,以防止扫描时匹配到较短的标签。", @@ -200,22 +204,20 @@ "better-todo-tree.configuration.highlights.useColourScheme.markdownDescription": "使用配色方案按标签顺序为标签着色。可以通过修改 `better-todo-tree.highlights.foregroundColourScheme` 和 `better-todo-tree.highlights.backgroundColourScheme` 来改变颜色。配色方案将覆盖默认突出显示的颜色,但不会覆盖自定义突出显示的颜色。", "todo-tree.configuration.regex": "雷格克斯", "better-todo-tree.configuration.regex": "雷格克斯", - "todo-tree.configuration.regex.enableMultiLine": "Force the regex to match over multiple lines. Allows use of `[\\s\\S]` to match anything including newlines.", - "better-todo-tree.configuration.regex.enableMultiLine": "Force the regex to match over multiple lines. Allows use of `[\\s\\S]` to match anything including newlines.", + "todo-tree.configuration.regex.enableMultiLine": "强制正则表达式跨多行匹配。可使用 `[\\s\\S]` 匹配包括换行符在内的任意字符。", + "better-todo-tree.configuration.regex.enableMultiLine": "强制正则表达式跨多行匹配。可使用 `[\\s\\S]` 匹配包括换行符在内的任意字符。", "todo-tree.configuration.regex.regex.markdownDescription": "用于匹配待办事项的正则表达式。`$TAGS` 会替换为展开后的标签列表。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。工作区搜索需要环视或反向引用时会自动使用 PCRE2。", "better-todo-tree.configuration.regex.regex.markdownDescription": "用于匹配待办事项的正则表达式。`$TAGS` 会替换为展开后的标签列表。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。工作区搜索需要环视或反向引用时会自动使用 PCRE2。", "todo-tree.configuration.regex.regexCaseSensitive.markdownDescription": "使用区分大小写的正则表达式。", "better-todo-tree.configuration.regex.regexCaseSensitive.markdownDescription": "使用区分大小写的正则表达式。", - "todo-tree.configuration.regex.subTagRegex.markdownDescription": "Regular expression for processing the text to the right of the tag, e.g. for extracting a sub tag, or removing unwanted characters.", - "better-todo-tree.configuration.regex.subTagRegex.markdownDescription": "Regular expression for processing the text to the right of the tag, e.g. for extracting a sub tag, or removing unwanted characters.", + "todo-tree.configuration.regex.subTagRegex.markdownDescription": "用于处理标签右侧文本的正则表达式,例如提取子标签或移除不需要的字符。", + "better-todo-tree.configuration.regex.subTagRegex.markdownDescription": "用于处理标签右侧文本的正则表达式,例如提取子标签或移除不需要的字符。", "todo-tree.configuration.ripgrep": "Ripgrep", "better-todo-tree.configuration.ripgrep": "Ripgrep", "todo-tree.configuration.ripgrep.ripgrep.markdownDescription": "自定义 ripgrep 可执行文件路径。空值使用随扩展打包的 ripgrep。", "better-todo-tree.configuration.ripgrep.ripgrep.markdownDescription": "自定义 ripgrep 可执行文件路径。空值使用随扩展打包的 ripgrep。", "todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "传递给 ripgrep 的额外参数。`--pcre2`、`-P` 和 `--engine` 等引擎参数会覆盖自动引擎选择。", "better-todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "传递给 ripgrep 的额外参数。`--pcre2`、`-P` 和 `--engine` 等引擎参数会覆盖自动引擎选择。", - "todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "配置 ripgrep 缓冲区的大小(KB)。当目前缓冲区大小不足以查看所有标签时,可适当增加缓冲区大小。", - "better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "配置 ripgrep 缓冲区的大小(KB)。当目前缓冲区大小不足以查看所有标签时,可适当增加缓冲区大小。", "todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "默认情况下,模板文件与 ripgrep 一起使用。当删除模板文件时遇到问题,请将此项设置为 false,即可使用向 ripgrep 提供正则表达式的旧方法。", "better-todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "默认情况下,模板文件与 ripgrep 一起使用。当删除模板文件时遇到问题,请将此项设置为 false,即可使用向 ripgrep 提供正则表达式的旧方法。", "todo-tree.configuration.title": "待办事项树", @@ -230,8 +232,8 @@ "better-todo-tree.configuration.tree.buttons.export.markdownDescription": "在侧边栏顶部显示一个导出树状图的按钮。", "todo-tree.configuration.tree.buttons.filter.markdownDescription": "在侧边栏顶部显示一个待办事项过滤器的按钮。", "better-todo-tree.configuration.tree.buttons.filter.markdownDescription": "在侧边栏顶部显示一个待办事项过滤器的按钮。", - "todo-tree.configuration.tree.buttons.groupBySubTag.markdownDescription": "Show a button in the tree view title bar to enable grouping items by sub tag.", - "better-todo-tree.configuration.tree.buttons.groupBySubTag.markdownDescription": "Show a button in the tree view title bar to enable grouping items by sub tag.", + "todo-tree.configuration.tree.buttons.groupBySubTag.markdownDescription": "在树视图标题栏中显示按子标签分组按钮。", + "better-todo-tree.configuration.tree.buttons.groupBySubTag.markdownDescription": "在树视图标题栏中显示按子标签分组按钮。", "todo-tree.configuration.tree.buttons.groupByTag.markdownDescription": "在侧边栏顶部显示一个切换待办事项是否按标签分组的按钮。", "better-todo-tree.configuration.tree.buttons.groupByTag.markdownDescription": "在侧边栏顶部显示一个切换待办事项是否按标签分组的按钮。", "todo-tree.configuration.tree.buttons.refresh.markdownDescription": "在侧边栏顶部显示一个刷新待办事项的按钮。", @@ -254,10 +256,10 @@ "better-todo-tree.configuration.tree.flat.markdownDescription": "当打开新的工作区时,默认使用平直树状图。", "todo-tree.configuration.tree.grouped.markdownDescription": "当打开新的工作区时,默认按标签分组。", "better-todo-tree.configuration.tree.grouped.markdownDescription": "当打开新的工作区时,默认按标签分组。", - "todo-tree.configuration.tree.groupedBySubTag.markdownDescription": "When opening new workspaces, show the tree initially grouped by sub tag.", - "better-todo-tree.configuration.tree.groupedBySubTag.markdownDescription": "When opening new workspaces, show the tree initially grouped by sub tag.", - "todo-tree.configuration.tree.groupedByTag.markdownDescription": "When opening new workspaces, show the tree initially grouped by tag.", - "better-todo-tree.configuration.tree.groupedByTag.markdownDescription": "When opening new workspaces, show the tree initially grouped by tag.", + "todo-tree.configuration.tree.groupedBySubTag.markdownDescription": "打开新工作区时,树默认按子标签分组。", + "better-todo-tree.configuration.tree.groupedBySubTag.markdownDescription": "打开新工作区时,树默认按子标签分组。", + "todo-tree.configuration.tree.groupedByTag.markdownDescription": "打开新工作区时,树默认按标签分组。", + "better-todo-tree.configuration.tree.groupedByTag.markdownDescription": "打开新工作区时,树默认按标签分组。", "todo-tree.configuration.tree.hideIconsWhenGroupedByTag.markdownDescription": "按标签分组时,省略待办事项前的图标。", "better-todo-tree.configuration.tree.hideIconsWhenGroupedByTag.markdownDescription": "按标签分组时,省略待办事项前的图标。", "todo-tree.configuration.tree.hideTreeWhenEmpty.markdownDescription": "如果视图为空,则将其隐藏.", @@ -286,14 +288,14 @@ "better-todo-tree.configuration.tree.showInExplorer.deprecationMessage": "该设置不再使用,请拖动视图来进行移动。", "todo-tree.configuration.tree.showScanOpenFilesOrWorkspaceButton.deprecationMessage": "该设置不再使用,请使用 \"better-todo-tree.tree.scanMode\" 替代。", "better-todo-tree.configuration.tree.showScanOpenFilesOrWorkspaceButton.deprecationMessage": "该设置不再使用,请使用 \"better-todo-tree.tree.scanMode\" 替代。", - "todo-tree.configuration.tree.showTagsFromOpenFilesOnly.deprecationMessage": "This setting is no longer used, please use \"better-todo-tree.tree.scanMode\" instead.", - "better-todo-tree.configuration.tree.showTagsFromOpenFilesOnly.deprecationMessage": "This setting is no longer used, please use \"better-todo-tree.tree.scanMode\" instead.", + "todo-tree.configuration.tree.showTagsFromOpenFilesOnly.deprecationMessage": "此设置已停用。请改用 \"better-todo-tree.tree.scanMode\"。", + "better-todo-tree.configuration.tree.showTagsFromOpenFilesOnly.deprecationMessage": "此设置已停用。请改用 \"better-todo-tree.tree.scanMode\"。", "todo-tree.configuration.tree.sort.markdownDescription": "使用多线程进行 ripgrep 搜索以提升性能。此时,当所有搜索结束后再进行排序。如果希望使用 ripgrep 的排序参数,可以将此项设置为 false。", "better-todo-tree.configuration.tree.sort.markdownDescription": "使用多线程进行 ripgrep 搜索以提升性能。此时,当所有搜索结束后再进行排序。如果希望使用 ripgrep 的排序参数,可以将此项设置为 false。", "todo-tree.configuration.tree.sortTagsOnlyViewAlphabetically.markdownDescription": "在仅显示标签视图中,待办事项按字母排序。若不选择此项,则待办事项按所在文件和行号排序。", "better-todo-tree.configuration.tree.sortTagsOnlyViewAlphabetically.markdownDescription": "在仅显示标签视图中,待办事项按字母排序。若不选择此项,则待办事项按所在文件和行号排序。", - "todo-tree.configuration.tree.subTagClickUrl.markdownDescription": "The URL to open when clicking on a sub tag in the tree. Can include placeholders as defined in `better-todo-tree.tree.labelFormat`.", - "better-todo-tree.configuration.tree.subTagClickUrl.markdownDescription": "The URL to open when clicking on a sub tag in the tree. Can include placeholders as defined in `better-todo-tree.tree.labelFormat`.", + "todo-tree.configuration.tree.subTagClickUrl.markdownDescription": "点击树中的子标签时打开的 URL。可包含 `better-todo-tree.tree.labelFormat` 定义的占位符。", + "better-todo-tree.configuration.tree.subTagClickUrl.markdownDescription": "点击树中的子标签时打开的 URL。可包含 `better-todo-tree.tree.labelFormat` 定义的占位符。", "todo-tree.configuration.tree.tagsOnly.markdownDescription": "当打开新的工作区时,默认使用仅显示标签视图。", "better-todo-tree.configuration.tree.tagsOnly.markdownDescription": "当打开新的工作区时,默认使用仅显示标签视图。", "todo-tree.configuration.tree.tooltipFormat.markdownDescription": "待办事项的鼠标悬浮提示的格式。允许的占位符包括 `${line}`、`${column}`、`${tag}`、`${before}`(标签以前的文本)、`${after}`(标签以后的文本)、`${filename}`、`${filepath}`和`${afterOrBefore}`(如果标签以后非空,则为标签以后的文本;如果标签以后为空,则为标签以前的文本)。", diff --git a/scripts/ci/sync-pr-vsix-comment.mjs b/scripts/ci/sync-pr-vsix-comment.mjs index 03a3f035..a26287bd 100644 --- a/scripts/ci/sync-pr-vsix-comment.mjs +++ b/scripts/ci/sync-pr-vsix-comment.mjs @@ -41,8 +41,7 @@ const previewTargetLabels = Object.freeze({ 'darwin-x64': 'macOS Intel', 'darwin-arm64': 'macOS Apple silicon', 'alpine-x64': 'Alpine Linux x64', - 'alpine-arm64': 'Alpine Linux Arm64', - web: 'Web' + 'alpine-arm64': 'Alpine Linux Arm64' }); class PrVsixInvariantError extends Error { diff --git a/scripts/ci/verify-pr-vsix.mjs b/scripts/ci/verify-pr-vsix.mjs index def4a722..f77e5a78 100644 --- a/scripts/ci/verify-pr-vsix.mjs +++ b/scripts/ci/verify-pr-vsix.mjs @@ -41,9 +41,6 @@ function expectedBundleFileNames(packageMetadata, targets, baseName = `${package function expectedRipgrepEntries(target) { const platform = requireTarget(target); - if (platform === undefined) { - return []; - } return [ `extension/dist/ripgrep/${platformDirectory(platform)}/${executableName(platform)}` ]; @@ -72,8 +69,7 @@ function verifyEntrySet(entries, target) { } const actualMetadata = ripgrepMetadataEntries.filter((entry) => entries.includes(entry)); - const expectedMetadata = expectedExecutables.length === 0 ? [] : ripgrepMetadataEntries; - if (JSON.stringify(actualMetadata) !== JSON.stringify(expectedMetadata)) { + if (JSON.stringify(actualMetadata) !== JSON.stringify(ripgrepMetadataEntries)) { throw new PrVsixVerificationError(`${target} VSIX ripgrep metadata entries mismatch`); } return expectedExecutables; @@ -124,9 +120,6 @@ function verifyTargetManifest(vsixPath, target) { } function verifyRipgrepManifest(vsixPath, target, executableEntries) { - if (executableEntries.length === 0) { - return; - } const raw = runUnzip(['-p', vsixPath, 'extension/dist/ripgrep/manifest.json']); let manifest; try { diff --git a/scripts/perf/extensionScenarios.js b/scripts/perf/extensionScenarios.js index f6ec4cbb..aedea861 100644 --- a/scripts/perf/extensionScenarios.js +++ b/scripts/perf/extensionScenarios.js @@ -589,6 +589,7 @@ module.exports.buildExtensionScenarioDefinitions = function( deps ) { this.event = function() {}; this.fire = function() {}; + this.dispose = function() {}; } function TreeItem( label ) @@ -714,7 +715,6 @@ module.exports.buildExtensionScenarioDefinitions = function( deps ) }, ripgrep: { ripgrepArgs: '', - ripgrepMaxBuffer: 200, usePatternFile: false } }, undefined, configurationUpdates ); @@ -768,7 +768,6 @@ module.exports.buildExtensionScenarioDefinitions = function( deps ) sections[ 'better-todo-tree.regex' ] = sections[ 'todo-tree.regex' ]; sections[ 'todo-tree.ripgrep' ] = createConfigurationSection( { ripgrepArgs: '', - ripgrepMaxBuffer: 200, usePatternFile: false }, undefined, configurationUpdates ); sections[ 'better-todo-tree.ripgrep' ] = sections[ 'todo-tree.ripgrep' ]; @@ -914,6 +913,8 @@ module.exports.buildExtensionScenarioDefinitions = function( deps ) showQuickPick: function() { return Promise.resolve(); }, showTextDocument: function() { return Promise.resolve(); }, onDidChangeActiveTextEditor: function( listener ) { return registerListener( workspaceListeners, 'activeEditor', listener ); }, + onDidChangeVisibleTextEditors: function( listener ) { return registerListener( windowListeners, 'visibleTextEditors', listener ); }, + onDidChangeWindowState: function( listener ) { return registerListener( windowListeners, 'windowState', listener ); }, onDidChangeVisibleNotebookEditors: function( listener ) { return registerListener( windowListeners, 'visibleNotebookEditors', listener ); } }, warningMessages: warningMessages, diff --git a/scripts/perf/run-all.js b/scripts/perf/run-all.js index 1fc252fb..5039fa93 100644 --- a/scripts/perf/run-all.js +++ b/scripts/perf/run-all.js @@ -971,6 +971,7 @@ function createTreeVscodeStub() this._listener( value ); } }.bind( this ); + this.dispose = function() {}; } function TreeItem( label ) @@ -1326,6 +1327,11 @@ function createHighlightModule(relativePath) this.end = end; }, window: { + visibleTextEditors: [], + onDidChangeVisibleTextEditors: function() + { + return { dispose: function() {} }; + }, createTextEditorDecorationType: function( options ) { creationCount.value++; diff --git a/scripts/release/build-vsix.mjs b/scripts/release/build-vsix.mjs index 2ba9effa..4f0a3098 100644 --- a/scripts/release/build-vsix.mjs +++ b/scripts/release/build-vsix.mjs @@ -149,18 +149,13 @@ function copyRipgrepMetadata() { } function stageRipgrepForTarget(target) { - const platform = ripgrepTargetPlatforms.get(target); - if (!ripgrepTargetPlatforms.has(target)) { throw new Error(`Unsupported ripgrep target "${target}".`); } + const platform = ripgrepTargetPlatforms.get(target); resetRipgrepStage(); - if (platform === undefined) { - return; - } - const packageJson = readJson(path.join(ripgrepPackageRoot, 'package.json')); const stagedPlatform = copyRipgrepPlatform(platform); copyRipgrepMetadata(); diff --git a/scripts/release/ripgrep-targets.mjs b/scripts/release/ripgrep-targets.mjs index 78e1f777..1b05c13e 100644 --- a/scripts/release/ripgrep-targets.mjs +++ b/scripts/release/ripgrep-targets.mjs @@ -7,8 +7,7 @@ const ripgrepTargetPlatforms = new Map([ ['darwin-x64', Object.freeze({ os: 'darwin', arch: 'x64' })], ['darwin-arm64', Object.freeze({ os: 'darwin', arch: 'arm64' })], ['alpine-x64', Object.freeze({ os: 'linux', arch: 'x64' })], - ['alpine-arm64', Object.freeze({ os: 'linux', arch: 'arm64' })], - ['web', undefined] + ['alpine-arm64', Object.freeze({ os: 'linux', arch: 'arm64' })] ]); function platformDirectory(platform) { diff --git a/scripts/release/targets.json b/scripts/release/targets.json index 402df1c1..fb5c5f64 100644 --- a/scripts/release/targets.json +++ b/scripts/release/targets.json @@ -7,6 +7,5 @@ "darwin-x64", "darwin-arm64", "alpine-x64", - "alpine-arm64", - "web" + "alpine-arm64" ] diff --git a/src/colours.js b/src/colours.js index b23b7b99..b89b5246 100644 --- a/src/colours.js +++ b/src/colours.js @@ -1,8 +1,7 @@ -var vscode = require( 'vscode' ); var utils = require( './utils.js' ); var identity = require( './extensionIdentity.js' ); -function validateColours( workspace ) +function validateColours() { function check( setting ) { @@ -35,7 +34,7 @@ function validateColours( workspace ) return result; } -function validateIconColours( workspace ) +function validateIconColours() { var hasInvalidCodiconColour = false; var hasInvalidOcticonColour = false; diff --git a/src/config.js b/src/config.js index 0a1754a3..778bfa7a 100644 --- a/src/config.js +++ b/src/config.js @@ -377,7 +377,7 @@ function shouldShowScanModeInTree() function shouldUseColourScheme() { - return identity.getSetting( 'highlights.useColourScheme', true ); + return identity.getSetting( 'highlights.useColourScheme', false ); } function foregroundColourScheme() diff --git a/src/detection.js b/src/detection.js index f10ed2ea..56c46ebd 100644 --- a/src/detection.js +++ b/src/detection.js @@ -947,7 +947,7 @@ function resolveMarkdownCommentPattern() return markdownCommentPattern; } -function scanMarkdownText( uri, text, pattern, lineOffsets, resourceConfig ) +function scanMarkdownText( uri, text, lineOffsets, resourceConfig ) { var markdownCommentPattern = resolveMarkdownCommentPattern(); var context = createScanContext( uri, text, undefined, { regexSource: resourceConfig.regex } ); @@ -1286,7 +1286,7 @@ function createShiftedNestedResult( result, charOffset, lineOffsets ) return shifted; } -function scanEmbeddedDocumentRegions( uri, text, resourceConfig, lineOffsets, embeddedDocument ) +function scanEmbeddedDocumentRegions( uri, resourceConfig, lineOffsets, embeddedDocument ) { if( !embeddedDocument || !Array.isArray( embeddedDocument.regions ) ) { @@ -1347,7 +1347,7 @@ function scanCommentPatternText( uri, text, resourceConfig, patternFileName, opt { if( path.extname( patternLookupName ).toLowerCase() === '.md' || pattern.name === 'Markdown' ) { - results = scanMarkdownText( uri, scanText, pattern, lineOffsets, resourceConfig ); + results = scanMarkdownText( uri, scanText, lineOffsets, resourceConfig ); } else { @@ -1366,7 +1366,7 @@ function scanCommentPatternText( uri, text, resourceConfig, patternFileName, opt } ); } - return sortResultsByLocation( results.concat( scanEmbeddedDocumentRegions( uri, text, resourceConfig, lineOffsets, embeddedDocument ) ) ); + return sortResultsByLocation( results.concat( scanEmbeddedDocumentRegions( uri, resourceConfig, lineOffsets, embeddedDocument ) ) ); } function normalizeRegexExecMatchWithContext( context, match ) diff --git a/src/extension.js b/src/extension.js index 4dce142f..b458ca2e 100644 --- a/src/extension.js +++ b/src/extension.js @@ -24,6 +24,7 @@ var regexRegistry = require( './regexRegistry.js' ); var settingsSnapshotModule = require( './runtime/settingsSnapshot.js' ); var documentScanCacheModule = require( './runtime/documentScanCache.js' ); var streamScanner = require( './runtime/streamScanner.js' ); +var activationSchedulerModule = require( './runtime/activationScheduler.js' ); var regexEngine = require( './regexEngine.js' ); var packageJson = require( '../package.json' ); var packageNls = require( '../package.nls.json' ); @@ -31,7 +32,6 @@ var packageNls = require( '../package.nls.json' ); var searchList = []; var currentFilter; var interrupted = false; -var selectedDocument; var treeRefreshTimeout; var rescanTimeout; var hideTimeout; @@ -39,7 +39,6 @@ var autoGitRefreshTimer; var periodicRefreshTimer; var lastGitHead = {}; var openDocuments = {}; -var provider; var ignoreMarkdownUpdate = false; var markdownUpdatePopupOpen = false; var scanGeneration = 0; @@ -51,6 +50,7 @@ var documentRefreshTimers = new Map(); var documentVersions = new Map(); var pendingDocumentRefreshes = new Map(); var gitHeadCheckInFlight = new Set(); +var activeRuntimeDisposer; var nlsTokenRegex = regexRegistry.createRegExp( 'nlsToken' ); var workspaceFolderPlaceholderRegex = regexRegistry.createRegExp( 'workspaceFolderPlaceholder', 'g' ); var diagnosticPathBackslashRegex = regexRegistry.createRegExp( 'pathBackslash', 'g' ); @@ -80,6 +80,11 @@ var EXPORT_DIAGNOSTICS_BUTTON = "Export Diagnostics"; function activate( context ) { + if( activeRuntimeDisposer ) + { + activeRuntimeDisposer(); + } + var outputChannel; var legacySettingImportMarker = 'importedLegacyNamespaceVersion'; var currentManifestSettingSuffixes = identity.getManifestSettingSuffixes( packageJson ); @@ -108,6 +113,18 @@ function activate( context ) var scanStatusBarSpinnerVisible = false; var startupOpenScanRetryTimer; var startupOpenScanRetryIndex = 0; + var exportDocumentGeneration = 0; + var activeEditorTrackTimer; + var lineFlashStyles = new Set(); + var pendingTreeUiTurnResolvers = new Set(); + var runtimeScheduler = activationSchedulerModule.createActivationScheduler( { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval, + setImmediate: setImmediate, + clearImmediate: clearImmediate + } ); var SCAN_PROGRESS_MIN_FILE_UNITS = 1; var SCAN_DIAGNOSTIC_EVENT_LIMIT = 500; @@ -120,6 +137,89 @@ function activate( context ) 'all': Object.freeze( { notification: true, statusBar: true, tree: true } ) } ); + function disposeRuntime() + { + if( runtimeScheduler.isDisposed() === true ) + { + return; + } + + pendingTreeUiTurnResolvers.forEach( function( resolve ) + { + resolve( false ); + } ); + pendingTreeUiTurnResolvers.clear(); + runtimeScheduler.dispose(); + + lineFlashStyles.forEach( function( decoration ) + { + if( decoration && decoration.dispose ) + { + decoration.dispose(); + } + } ); + lineFlashStyles.clear(); + + if( scanProgressSession && scanProgressSession.resolve ) + { + scanProgressSession.resolve(); + scanProgressSession = undefined; + } + if( outputChannel ) + { + outputChannel.dispose(); + outputChannel = undefined; + } + + if( activeScanGeneration !== 0 ) + { + cancelledScanGenerations.add( activeScanGeneration ); + } + activeScanGeneration = 0; + scanInFlight = false; + pendingRescan = false; + interrupted = false; + searchList = []; + currentFilter = undefined; + markdownUpdatePopupOpen = false; + documentRefreshTimers.clear(); + documentVersions.clear(); + pendingDocumentRefreshes.clear(); + gitHeadCheckInFlight.clear(); + openDocuments = {}; + lastGitHead = {}; + + treeRefreshTimeout = undefined; + rescanTimeout = undefined; + hideTimeout = undefined; + autoGitRefreshTimer = undefined; + periodicRefreshTimer = undefined; + streamingTreeApplyTimer = undefined; + startupOpenScanRetryTimer = undefined; + activeEditorTrackTimer = undefined; + nextSearchResults = undefined; + workspaceScanIssues = []; + scanProgressState = undefined; + scanStatusBarSpinnerVisible = false; + + ripgrep.kill(); + highlights.resetCaches(); + if( provider ) + { + provider.clear( [] ); + provider.dispose(); + provider = undefined; + } + + if( activeRuntimeDisposer === disposeRuntime ) + { + activeRuntimeDisposer = undefined; + } + } + + activeRuntimeDisposer = disposeRuntime; + context.subscriptions.push( { dispose: disposeRuntime } ); + function settingLocation( setting, uri ) { return identity.getSettingTarget( setting, uri ); @@ -201,6 +301,11 @@ function activate( context ) return undefined; } ).then( function() { + if( runtimeScheduler.isDisposed() === true ) + { + return undefined; + } + var changedEntries = Array.from( latestEntries.entries() ).reduce( function( filtered, entry ) { if( extensionContextValues[ entry[ 0 ] ] !== entry[ 1 ] ) @@ -241,7 +346,10 @@ function activate( context ) extensionContextUpdateQueue = scheduled.catch( function( error ) { - vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": failed to update command contexts (" + formatErrorMessage( error ) + ")" ); + if( runtimeScheduler.isDisposed() !== true ) + { + vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": failed to update command contexts (" + formatErrorMessage( error ) + ")" ); + } } ); return scheduled; @@ -260,6 +368,11 @@ function activate( context ) context.subscriptions.push( vscode.workspace.registerTextDocumentContentProvider( scheme, { provideTextDocumentContent( uri ) { + if( runtimeScheduler.isDisposed() === true || !provider ) + { + return ''; + } + if( path.extname( uri.path ) === '.json' ) { return JSON.stringify( provider.exportTree(), null, 2 ); @@ -920,6 +1033,11 @@ function activate( context ) var commandArguments = arguments; var commandThis = this; + if( runtimeScheduler.isDisposed() === true ) + { + return Promise.resolve(); + } + return runCommandOperation( boundaryOperationName, function() { return handler.apply( commandThis, commandArguments ); @@ -962,14 +1080,12 @@ function activate( context ) var resolveCommentPatternFileNameForLanguage = commentPatternLanguageResolver.createCommentPatternLanguageResolver( vscode, utils ); - provider = new tree.TreeNodeProvider( context, debug, setButtonsAndContext ); + var provider = new tree.TreeNodeProvider( context, debug, setButtonsAndContext ); var statusBarIndicator = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, 0 ); var scanStatusBarSpinner = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, 1 ); var todoTreeView = vscode.window.createTreeView( identity.VIEW_ID, { treeDataProvider: provider } ); - var fileSystemWatcher; - context.subscriptions.push( provider ); context.subscriptions.push( statusBarIndicator ); context.subscriptions.push( scanStatusBarSpinner ); @@ -982,6 +1098,11 @@ function activate( context ) function resetOutputChannel() { + if( runtimeScheduler.isDisposed() === true ) + { + return; + } + if( outputChannel ) { outputChannel.dispose(); @@ -995,7 +1116,12 @@ function activate( context ) function refreshTree( immediate ) { - clearTimeout( treeRefreshTimeout ); + if( runtimeScheduler.isDisposed() === true || !provider ) + { + return Promise.resolve(); + } + + runtimeScheduler.cancelTimeout( treeRefreshTimeout ); if( immediate === true ) { treeRefreshTimeout = undefined; @@ -1003,8 +1129,9 @@ function activate( context ) return setButtonsAndContext(); } - treeRefreshTimeout = setTimeout( function() + treeRefreshTimeout = runtimeScheduler.scheduleTimeout( function() { + treeRefreshTimeout = undefined; provider.refresh(); setButtonsAndContext(); }, 200 ); @@ -1105,7 +1232,7 @@ function activate( context ) { var commandError = normalizeCommandError( error ); - if( commandError.reportedToUser !== true ) + if( commandError.reportedToUser !== true && runtimeScheduler.isDisposed() !== true ) { commandError.reportedToUser = true; vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": failed to " + operationName + " (" + formatErrorMessage( commandError ) + ")" ); @@ -1118,7 +1245,10 @@ function activate( context ) { return Promise.resolve( promise ).catch( function( error ) { - vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": failed to " + operationName + " (" + formatErrorMessage( error ) + ")" ); + if( runtimeScheduler.isDisposed() !== true ) + { + vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": failed to " + operationName + " (" + formatErrorMessage( error ) + ")" ); + } } ); } @@ -2003,15 +2133,27 @@ function activate( context ) treeBusyStateCounts[ busyContextKey ] = ( treeBusyStateCounts[ busyContextKey ] || 0 ) + 1; updateTreeBusyContexts(); - var scheduled = treeStateMutationQueue.then( mutation ); + var scheduled = treeStateMutationQueue.then( function() + { + if( runtimeScheduler.isDisposed() !== true ) + { + return mutation(); + } + } ); treeStateMutationQueue = scheduled.catch( function( error ) { - vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": failed to update " + operationName + " (" + formatErrorMessage( error ) + ")" ); + if( runtimeScheduler.isDisposed() !== true ) + { + vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": failed to update " + operationName + " (" + formatErrorMessage( error ) + ")" ); + } } ).finally( function() { treeBusyStateCounts[ busyContextKey ] = Math.max( ( treeBusyStateCounts[ busyContextKey ] || 0 ) - 1, 0 ); - updateTreeBusyContexts(); + if( runtimeScheduler.isDisposed() !== true ) + { + updateTreeBusyContexts(); + } } ); return scheduled; @@ -2087,7 +2229,7 @@ function activate( context ) { if( streamingTreeApplyTimer ) { - clearTimeout( streamingTreeApplyTimer ); + runtimeScheduler.cancelTimeout( streamingTreeApplyTimer ); streamingTreeApplyTimer = undefined; } @@ -2114,7 +2256,7 @@ function activate( context ) return; } - streamingTreeApplyTimer = setTimeout( function() + streamingTreeApplyTimer = runtimeScheduler.scheduleTimeout( function() { streamingTreeApplyTimer = undefined; @@ -2135,6 +2277,11 @@ function activate( context ) function updateInformation() { + if( runtimeScheduler.isDisposed() === true || !provider ) + { + return; + } + var statusBar = getSetting( 'general.statusBar', 'none' ); var activityBarCounts = provider.getTagCountsForActivityBar(); @@ -2759,7 +2906,6 @@ function activate( context ) options.outputChannel = outputChannel; options.additional = getSetting( 'ripgrep.ripgrepArgs', '' ); - options.maxBuffer = getSetting( 'ripgrep.ripgrepMaxBuffer', 200 ); options.multiline = regexSource.indexOf( "\\n" ) > -1 || resourceConfig.enableMultiLine === true; if( context.storageUri && context.storageUri.fsPath && getSetting( 'ripgrep.usePatternFile', true ) === true ) @@ -2949,7 +3095,7 @@ function activate( context ) if( documentRefreshTimers.has( key ) ) { - clearTimeout( documentRefreshTimers.get( key ) ); + runtimeScheduler.cancelTimeout( documentRefreshTimers.get( key ) ); documentRefreshTimers.delete( key ); } } @@ -3325,6 +3471,11 @@ function activate( context ) function applyDirtyResultsToTree( options, store ) { + if( runtimeScheduler.isDisposed() === true || !provider ) + { + return; + } + options = options || {}; var resultsStore = getSearchResultsStore( store ); @@ -3750,7 +3901,7 @@ function activate( context ) streamingTreeApplyGeneration = generation; if( streamingTreeApplyTimer ) { - clearTimeout( streamingTreeApplyTimer ); + runtimeScheduler.cancelTimeout( streamingTreeApplyTimer ); streamingTreeApplyTimer = undefined; } @@ -3775,6 +3926,11 @@ function activate( context ) { nextSearchResults = undefined; + if( runtimeScheduler.isDisposed() === true ) + { + return; + } + if( isCancelledError( error ) !== true ) { if( streamingTreePreparedGeneration === generation ) @@ -3792,9 +3948,14 @@ function activate( context ) } } ).finally( function() { + if( runtimeScheduler.isDisposed() === true ) + { + return; + } + if( streamingTreeApplyTimer ) { - clearTimeout( streamingTreeApplyTimer ); + runtimeScheduler.cancelTimeout( streamingTreeApplyTimer ); streamingTreeApplyTimer = undefined; } if( streamingTreePreparedGeneration === generation ) @@ -3810,7 +3971,7 @@ function activate( context ) function rebuild() { - clearTimeout( rescanTimeout ); + runtimeScheduler.cancelTimeout( rescanTimeout ); if( scanInFlight === true ) { @@ -3823,9 +3984,10 @@ function activate( context ) function triggerRescan( delay ) { - clearTimeout( rescanTimeout ); - rescanTimeout = setTimeout( function() + runtimeScheduler.cancelTimeout( rescanTimeout ); + rescanTimeout = runtimeScheduler.scheduleTimeout( function() { + rescanTimeout = undefined; if( scanInFlight === true ) { pendingRescan = true; @@ -3840,7 +4002,7 @@ function activate( context ) { if( startupOpenScanRetryTimer ) { - clearTimeout( startupOpenScanRetryTimer ); + runtimeScheduler.cancelTimeout( startupOpenScanRetryTimer ); startupOpenScanRetryTimer = undefined; } } @@ -3867,7 +4029,7 @@ function activate( context ) var retryDelay = STARTUP_OPEN_SCAN_RETRY_DELAYS[ startupOpenScanRetryIndex ]; startupOpenScanRetryIndex++; - startupOpenScanRetryTimer = setTimeout( function() + startupOpenScanRetryTimer = runtimeScheduler.scheduleTimeout( function() { startupOpenScanRetryTimer = undefined; rememberVisibleTextEditors(); @@ -3906,6 +4068,11 @@ function activate( context ) { gitHeadCheckInFlight.delete( folder.uri.fsPath ); + if( runtimeScheduler.isDisposed() === true ) + { + return; + } + if( err ) { debug( "git rev-parse HEAD failed for " + folder.uri.fsPath + ": " + stderr.toString().trim() ); @@ -3928,13 +4095,14 @@ function activate( context ) if( autoGitRefreshTimer ) { - clearInterval( autoGitRefreshTimer ); + runtimeScheduler.cancelInterval( autoGitRefreshTimer ); + autoGitRefreshTimer = undefined; } if( timerInterval > 0 ) { debug( 'Setting automatic Git refresh interval to ' + timerInterval + ' seconds' ); - autoGitRefreshTimer = setInterval( checkGitHead, timerInterval * 1000 ); + autoGitRefreshTimer = runtimeScheduler.scheduleInterval( checkGitHead, timerInterval * 1000 ); } else { @@ -3948,13 +4116,14 @@ function activate( context ) if( periodicRefreshTimer ) { - clearInterval( periodicRefreshTimer ); + runtimeScheduler.cancelInterval( periodicRefreshTimer ); + periodicRefreshTimer = undefined; } if( timerInterval > 0 ) { debug( 'Setting periodic refresh interval to ' + timerInterval + ' minutes' ); - periodicRefreshTimer = setInterval( triggerRescan, timerInterval * 1000 * 60 ); + periodicRefreshTimer = runtimeScheduler.scheduleInterval( triggerRescan, timerInterval * 1000 * 60 ); } else { @@ -3964,6 +4133,11 @@ function activate( context ) function setButtonsAndContext() { + if( runtimeScheduler.isDisposed() === true || !provider ) + { + return Promise.resolve(); + } + var isFlat = config.shouldFlatten(); var isTagsOnly = config.shouldShowTagsOnly(); var isGroupedByTag = config.shouldGroupByTag(); @@ -3988,8 +4162,12 @@ function activate( context ) return total + treeBusyStateCounts[ key ]; }, 0 ); - clearTimeout( hideTimeout ); - hideTimeout = setTimeout( hideTreeIfEmpty, 1000 ); + runtimeScheduler.cancelTimeout( hideTimeout ); + hideTimeout = runtimeScheduler.scheduleTimeout( function() + { + hideTimeout = undefined; + hideTreeIfEmpty(); + }, 1000 ); return queueExtensionContextUpdates( [ { suffix: 'show-reveal-button', value: showRevealButton && !getSetting( 'tree.trackFile', false ) }, @@ -4023,6 +4201,11 @@ function activate( context ) function hideTreeIfEmpty() { + if( runtimeScheduler.isDisposed() === true || !provider ) + { + return; + } + var children = provider.getChildren(); children = children.filter( function( child ) { @@ -4094,10 +4277,10 @@ function activate( context ) if( documentRefreshTimers.has( key ) ) { - clearTimeout( documentRefreshTimers.get( key ) ); + runtimeScheduler.cancelTimeout( documentRefreshTimers.get( key ) ); } - documentRefreshTimers.set( key, setTimeout( function() + documentRefreshTimers.set( key, runtimeScheduler.scheduleTimeout( function() { documentRefreshTimers.delete( key ); @@ -4220,6 +4403,11 @@ function activate( context ) function refresh( options ) { + if( runtimeScheduler.isDisposed() === true || !provider ) + { + return; + } + options = options || {}; var resultsStore = getDisplayedSearchResultsStore(); @@ -4241,14 +4429,42 @@ function activate( context ) function waitForTreeUiTurn() { + if( runtimeScheduler.isDisposed() === true ) + { + return Promise.resolve( false ); + } + return new Promise( function( resolve ) { - setImmediate( resolve ); + var settled = false; + + function settle( ready ) + { + if( settled === true ) + { + return; + } + + settled = true; + pendingTreeUiTurnResolvers.delete( settle ); + resolve( ready ); + } + + pendingTreeUiTurnResolvers.add( settle ); + runtimeScheduler.scheduleImmediate( function() + { + settle( true ); + } ); } ); } function getVisibleTreeChildren( node ) { + if( !provider ) + { + return []; + } + var children = provider.getChildren( node ); return Array.isArray( children ) ? children : []; } @@ -4298,9 +4514,12 @@ function activate( context ) provider.clearExpansionState(); return Promise.resolve( refresh( { immediateRefresh: true, forceFullRefresh: true } ) ) .then( waitForTreeUiTurn ) - .then( function() + .then( function( treeUiReady ) { - return syncRenderedTreeExpansion( expanded ); + if( treeUiReady === true ) + { + return syncRenderedTreeExpansion( expanded ); + } } ); } @@ -4476,7 +4695,7 @@ function activate( context ) if( getSetting( 'regex.regex', '' ).indexOf( utils.LEGACY_MARKDOWN_TASK_FRAGMENT ) > -1 ) { markdownUpdatePopupOpen = true; - setTimeout( function() + runtimeScheduler.scheduleTimeout( function() { markdownUpdatePopupOpen = false; }, 15000 ); @@ -4633,7 +4852,6 @@ function activate( context ) migrateLegacyFlatSettingIfRequired( legacyRootConfiguration, updates, 'revealBehaviour', 'string', 'general' ); migrateLegacyFlatSettingIfRequired( legacyRootConfiguration, updates, 'ripgrep', 'string', 'ripgrep' ); migrateLegacyFlatSettingIfRequired( legacyRootConfiguration, updates, 'ripgrepArgs', 'string', 'ripgrep' ); - migrateLegacyFlatSettingIfRequired( legacyRootConfiguration, updates, 'ripgrepMaxBuffer', 'number', 'ripgrep' ); migrateLegacyFlatSettingIfRequired( legacyRootConfiguration, updates, 'rootFolder', 'string', 'general' ); migrateLegacyFlatSettingIfRequired( legacyRootConfiguration, updates, 'showBadges', 'boolean', 'tree' ); migrateLegacyFlatSettingIfRequired( legacyRootConfiguration, updates, 'showCountsInTree', 'boolean', 'tree' ); @@ -4731,20 +4949,23 @@ function activate( context ) function showInTree( uri, options ) { options = options || {}; - var revealPromise = Promise.resolve(); - provider.getElement( uri.fsPath, function( element ) + if( runtimeScheduler.isDisposed() === true || !provider ) { - if( todoTreeView.visible === true ) - { - revealPromise = todoTreeView.reveal( element, { focus: false, select: options.select !== false } ); - } - } ); - return revealPromise; + return Promise.resolve(); + } + + var element = provider.getElement( uri.fsPath ); + if( todoTreeView.visible === true && element ) + { + return todoTreeView.reveal( element, { focus: false, select: options.select !== false } ); + } + + return Promise.resolve(); } function scheduleStartupScan() { - var startupScanImmediate = setImmediate( function() + var startupScanImmediate = runtimeScheduler.scheduleImmediate( function() { startupOpenScanRetryIndex = 0; rememberVisibleTextEditors(); @@ -4755,7 +4976,7 @@ function activate( context ) context.subscriptions.push( { dispose: function() { - clearImmediate( startupScanImmediate ); + runtimeScheduler.cancelImmediate( startupScanImmediate ); clearStartupOpenScanRetry(); } } ); @@ -4763,6 +4984,9 @@ function activate( context ) function activeEditorChanged( editor ) { + runtimeScheduler.cancelTimeout( activeEditorTrackTimer ); + activeEditorTrackTimer = undefined; + if( !editor || !editor.document ) { return; @@ -4772,7 +4996,6 @@ function activate( context ) var activeDocumentIsNotebookCell = notebooks.isNotebookCellDocument( document ) === true; var activeNotebook = activeDocumentIsNotebookCell === true ? getNotebookForDocument( document ) : undefined; var ownerUri = activeNotebook ? activeNotebook.uri : getOwnerUriForDocument( document ); - var ownerFileFilter = ownerUri && ownerUri.fsPath !== undefined ? ownerUri.fsPath : document.fileName; triggerHighlightsForVisibleEditors( document ); @@ -4787,14 +5010,15 @@ function activate( context ) { if( ownerUri && config.isValidScheme( ownerUri ) ) { - if( selectedDocument !== ownerFileFilter ) + activeEditorTrackTimer = runtimeScheduler.scheduleTimeout( function() { - setTimeout( function() + activeEditorTrackTimer = undefined; + if( vscode.window.activeTextEditor === editor && editor.document.isClosed !== true && + getSetting( 'tree.autoRefresh', true ) === true && getSetting( 'tree.trackFile', true ) === true ) { observeOperationFailure( 'track active file in tree', showInTree( ownerUri, { select: false } ) ); - }, 500 ); - } - selectedDocument = undefined; + } + }, 500 ); } } @@ -4832,12 +5056,12 @@ function activate( context ) function validateColours() { - var invalidColourMessage = colours.validateColours( vscode.workspace ); + var invalidColourMessage = colours.validateColours(); if( invalidColourMessage ) { vscode.window.showWarningMessage( identity.DISPLAY_NAME + ": " + invalidColourMessage ); } - var invalidIconColourMessage = colours.validateIconColours( vscode.workspace ); + var invalidIconColourMessage = colours.validateIconColours(); if( invalidIconColourMessage ) { vscode.window.showWarningMessage( identity.DISPLAY_NAME + ": " + invalidIconColourMessage ); @@ -4846,7 +5070,7 @@ function activate( context ) function validateIcons() { - var invalidIconMessage = icons.validateIcons( vscode.workspace ); + var invalidIconMessage = icons.validateIcons(); if( invalidIconMessage ) { vscode.window.showWarningMessage( identity.DISPLAY_NAME + ": " + invalidIconMessage ); @@ -4908,7 +5132,10 @@ function activate( context ) exportPath = utils.replaceEnvironmentVariables( exportPath ); exportPath = utils.formatExportPath( exportPath ); - var uri = vscode.Uri.parse( identity.EXPORT_SCHEME + ':' + exportPath ); + exportDocumentGeneration++; + var uri = vscode.Uri.parse( identity.EXPORT_SCHEME + ':' + exportPath ).with( { + query: 'generation=' + exportDocumentGeneration + } ); return vscode.workspace.openTextDocument( uri ).then( function( document ) { return vscode.window.showTextDocument( document, { preview: true } ); @@ -5359,7 +5586,7 @@ function activate( context ) function flashLine() { var editor = vscode.window.activeTextEditor; - if( !editor ) + if( runtimeScheduler.isDisposed() === true || !editor || !editor.document || editor.document.isClosed === true ) { return; } @@ -5379,11 +5606,24 @@ function activate( context ) var lineRangeHighlight = { range: currentLineRange }; - editor.setDecorations( lineFlashStyle, [ lineRangeHighlight ] ); + lineFlashStyles.add( lineFlashStyle ); + try + { + editor.setDecorations( lineFlashStyle, [ lineRangeHighlight ] ); + } + catch( error ) + { + lineFlashStyles.delete( lineFlashStyle ); + if( lineFlashStyle.dispose ) + { + lineFlashStyle.dispose(); + } + throw error; + } - setTimeout( function() + runtimeScheduler.scheduleTimeout( function() { - editor.setDecorations( lineFlashStyle, [] ); + lineFlashStyles.delete( lineFlashStyle ); if( lineFlashStyle.dispose ) { lineFlashStyle.dispose(); @@ -5395,8 +5635,20 @@ function activate( context ) } ); } ); - context.subscriptions.push( todoTreeView.onDidExpandElement( function( e ) { provider.setExpanded( e.element.fsPath, true ); } ) ); - context.subscriptions.push( todoTreeView.onDidCollapseElement( function( e ) { provider.setExpanded( e.element.fsPath, false ); } ) ); + context.subscriptions.push( todoTreeView.onDidExpandElement( function( e ) + { + if( runtimeScheduler.isDisposed() !== true && provider ) + { + provider.setExpanded( e.element.fsPath, true ); + } + } ) ); + context.subscriptions.push( todoTreeView.onDidCollapseElement( function( e ) + { + if( runtimeScheduler.isDisposed() !== true && provider ) + { + provider.setExpanded( e.element.fsPath, false ); + } + } ) ); registerCommandPair( 'filterClear', function() { @@ -5515,7 +5767,7 @@ function activate( context ) if( typeof ( vscode.workspace.onDidOpenNotebookDocument ) === 'function' ) { - context.subscriptions.push( vscode.workspace.onDidOpenNotebookDocument( function( notebook ) + context.subscriptions.push( vscode.workspace.onDidOpenNotebookDocument( function() { handleVisibleNotebookEditorsChanged( 'open' ); } ) ); @@ -5544,7 +5796,7 @@ function activate( context ) if( typeof ( vscode.workspace.onDidCloseNotebookDocument ) === 'function' ) { - context.subscriptions.push( vscode.workspace.onDidCloseNotebookDocument( function( notebook ) + context.subscriptions.push( vscode.workspace.onDidCloseNotebookDocument( function() { handleVisibleNotebookEditorsChanged( 'open' ); } ) ); @@ -5667,14 +5919,15 @@ function activate( context ) documentChanged( e.document ); } ) ); - context.subscriptions.push( outputChannel ); - resetOutputChannel(); migrateSettings().catch( function( error ) { - vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": Failed to migrate legacy settings (" + formatErrorMessage( error ) + ")" ); + if( runtimeScheduler.isDisposed() !== true ) + { + vscode.window.showErrorMessage( identity.DISPLAY_NAME + ": Failed to migrate legacy settings (" + formatErrorMessage( error ) + ")" ); + } } ); validateColours(); validateIcons(); @@ -5700,10 +5953,13 @@ function activate( context ) function deactivate() { - ripgrep.kill(); - if( provider ) + if( activeRuntimeDisposer ) { - provider.clear( [] ); + activeRuntimeDisposer(); + } + else + { + ripgrep.kill(); } } diff --git a/src/highlights.js b/src/highlights.js index 060b6774..974b120f 100644 --- a/src/highlights.js +++ b/src/highlights.js @@ -24,7 +24,6 @@ var lanes = var decorations = {}; var decorationCache = new Map(); var highlightTimer = {}; -var highlightVersions = {}; var context; var debug; var scanResultsProvider = function( document ) { return detection.scanDocument( document ); }; @@ -36,6 +35,40 @@ function init( context_, debug_ ) context.subscriptions.push( { dispose: resetCaches } ); + context.subscriptions.push( vscode.window.onDidChangeVisibleTextEditors( pruneEditorState ) ); +} + +function visibleEditorIds( editors ) +{ + return new Set( editors.map( editorId ) ); +} + +function pruneEditorState( editors ) +{ + var retainedIds = visibleEditorIds( editors ); + + Object.keys( highlightTimer ).forEach( function( id ) + { + if( retainedIds.has( id ) !== true ) + { + clearTimeout( highlightTimer[ id ] ); + delete highlightTimer[ id ]; + } + } ); + + Object.keys( decorations ).forEach( function( id ) + { + if( retainedIds.has( id ) !== true ) + { + delete decorations[ id ]; + } + } ); +} + +function isEditorVisible( editor ) +{ + return editor && editor.document && editor.document.isClosed !== true && + vscode.window.visibleTextEditors.indexOf( editor ) !== -1; } function applyOpacity( colour, opacity ) @@ -420,9 +453,15 @@ function triggerHighlight( editor ) clearTimeout( highlightTimer[ id ] ); } - highlightVersions[ id ] = version; highlightTimer[ id ] = setTimeout( function( scheduledEditor, scheduledVersion ) { + delete highlightTimer[ id ]; + if( isEditorVisible( scheduledEditor ) !== true ) + { + delete decorations[ id ]; + return; + } + if( scheduledEditor.document && scheduledVersion !== undefined && scheduledEditor.document.version !== scheduledVersion ) { return; diff --git a/src/icons.js b/src/icons.js index fd0ae52d..aed767ce 100644 --- a/src/icons.js +++ b/src/icons.js @@ -197,7 +197,7 @@ function getGutterIcon( context, tag, debug ) return getFileBackedIcon( context, tag, debug ); } -function validateIcons( workspace ) +function validateIcons() { function checkIcon( setting ) { diff --git a/src/ripgrep.js b/src/ripgrep.js index 774e7b59..26975240 100644 --- a/src/ripgrep.js +++ b/src/ripgrep.js @@ -8,8 +8,7 @@ var fs = require( 'fs' ); var regexEngine = require( './regexEngine.js' ); var regexRegistry = require( './regexRegistry.js' ); -var currentProcess; -var currentProcessState; +var activeProcesses = new Map(); var whitespaceCharacterRegex = regexRegistry.createRegExp( 'whitespaceCharacter' ); var MAX_DEBUG_PREVIEW_LINES = 10; @@ -313,15 +312,25 @@ function normalizeSearchOptions( options ) return normalizedOptions; } -function clearCurrentProcess( processRef ) +function clearActiveProcess( processRef ) { - if( currentProcess && ( processRef === undefined || currentProcess === processRef ) ) + if( processRef !== undefined ) { - currentProcess = undefined; - currentProcessState = undefined; + activeProcesses.delete( processRef ); } } +function beginProcessCompletion( state ) +{ + if( state.completionStarted === true ) + { + return false; + } + + state.completionStarted = true; + return true; +} + function rejectAfterPatternCleanup( options, state, reject, error, beforeCleanup, processRef ) { if( typeof ( beforeCleanup ) === 'function' ) @@ -331,11 +340,11 @@ function rejectAfterPatternCleanup( options, state, reject, error, beforeCleanup return cleanupPatternFile( options.patternFilePath ).then( function() { - clearCurrentProcess( processRef ); + clearActiveProcess( processRef ); reject( toRipgrepError( error, state.stderr, false, createErrorDetails( state ) ) ); }, function( cleanupError ) { - clearCurrentProcess( processRef ); + clearActiveProcess( processRef ); reject( toRipgrepError( cleanupError, state.stderr, false, createErrorDetails( state ) ) ); } ); } @@ -373,13 +382,14 @@ module.exports.search = function ripGrep( cwd, options, onEvent ) stderr: "", stdoutTail: "", matchCount: 0, - summary: undefined + summary: undefined, + completionStarted: false }; var eventHandler = typeof ( onEvent ) === 'function' ? onEvent : function() {}; try { - currentProcess = child_process.spawn( options.rgPath, args, { cwd: cwd, windowsHide: true } ); + var searchProcess = child_process.spawn( options.rgPath, args, { cwd: cwd, windowsHide: true } ); } catch( error ) { @@ -387,20 +397,28 @@ module.exports.search = function ripGrep( cwd, options, onEvent ) return; } - var searchProcess = currentProcess; - currentProcessState = state; + activeProcesses.set( searchProcess, state ); searchProcess.stdout.on( 'data', function( data ) { + if( state.completionStarted === true ) + { + return; + } + try { processStdoutChunk( data, state, eventHandler ); } catch( error ) { + if( beginProcessCompletion( state ) !== true ) + { + return; + } rejectAfterPatternCleanup( options, state, reject, error, function() { - if( currentProcess === searchProcess ) + if( activeProcesses.has( searchProcess ) ) { searchProcess.kill( 'SIGINT' ); } @@ -410,16 +428,31 @@ module.exports.search = function ripGrep( cwd, options, onEvent ) searchProcess.stderr.on( 'data', function( data ) { + if( state.completionStarted === true ) + { + return; + } state.stderr = appendBoundedText( state.stderr, data.toString(), MAX_DEBUG_TEXT_LENGTH ); } ); searchProcess.on( 'error', function( error ) { + if( beginProcessCompletion( state ) !== true ) + { + return; + } rejectAfterPatternCleanup( options, state, reject, error, undefined, searchProcess ); } ); searchProcess.on( 'close', function( code, signal ) { + if( beginProcessCompletion( state ) !== true ) + { + clearActiveProcess( searchProcess ); + return; + } + + clearActiveProcess( searchProcess ); var completion = Promise.resolve(); if( state.stdoutTail.length > 0 ) @@ -446,7 +479,7 @@ module.exports.search = function ripGrep( cwd, options, onEvent ) return cleanupPatternFile( options.patternFilePath ); } ).then( function() { - clearCurrentProcess( searchProcess ); + clearActiveProcess( searchProcess ); if( state.previewLines.length > 0 ) { @@ -475,7 +508,7 @@ module.exports.search = function ripGrep( cwd, options, onEvent ) } ) ) ); } ).catch( function( error ) { - clearCurrentProcess( searchProcess ); + clearActiveProcess( searchProcess ); reject( toRipgrepError( error, state.stderr, false, createErrorDetails( state ) ) ); } ); } ); @@ -501,14 +534,14 @@ module.exports.search = function ripGrep( cwd, options, onEvent ) module.exports.kill = function() { - if( currentProcess !== undefined ) + Array.from( activeProcesses.entries() ).forEach( function( entry ) { - if( currentProcessState ) - { - currentProcessState.cancellationRequested = true; - } - currentProcess.kill( 'SIGINT' ); - } + var processRef = entry[ 0 ]; + var state = entry[ 1 ]; + + state.cancellationRequested = true; + processRef.kill( 'SIGINT' ); + } ); }; module.exports.decodeJsonValue = decodeJsonValue; diff --git a/src/runtime/activationScheduler.js b/src/runtime/activationScheduler.js new file mode 100644 index 00000000..dd5bb6c9 --- /dev/null +++ b/src/runtime/activationScheduler.js @@ -0,0 +1,120 @@ +function createHandleRegistry( schedule, cancel, repeats, isDisposed ) +{ + var handles = new Set(); + + function add( callback, delay ) + { + var completedSynchronously = false; + var handle; + + if( isDisposed() === true ) + { + return undefined; + } + + function invoke() + { + completedSynchronously = true; + if( repeats !== true && handle !== undefined ) + { + handles.delete( handle ); + } + if( isDisposed() !== true ) + { + callback(); + } + } + + handle = delay === undefined ? schedule( invoke ) : schedule( invoke, delay ); + + if( isDisposed() === true ) + { + if( handle !== undefined ) + { + cancel( handle ); + } + } + else if( handle !== undefined && ( repeats === true || completedSynchronously !== true ) ) + { + handles.add( handle ); + } + + return handle; + } + + function remove( handle ) + { + if( handle !== undefined ) + { + handles.delete( handle ); + cancel( handle ); + } + } + + function dispose() + { + handles.forEach( cancel ); + handles.clear(); + } + + return { + add: add, + remove: remove, + dispose: dispose + }; +} + +function createActivationScheduler( clock ) +{ + var disposed = false; + + [ + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'setImmediate', + 'clearImmediate' + ].forEach( function( method ) + { + if( !clock || typeof ( clock[ method ] ) !== 'function' ) + { + throw new TypeError( 'activation scheduler requires clock.' + method ); + } + } ); + + function isDisposed() + { + return disposed; + } + + var timeouts = createHandleRegistry( clock.setTimeout, clock.clearTimeout, false, isDisposed ); + var intervals = createHandleRegistry( clock.setInterval, clock.clearInterval, true, isDisposed ); + var immediates = createHandleRegistry( clock.setImmediate, clock.clearImmediate, false, isDisposed ); + + function dispose() + { + if( disposed === true ) + { + return; + } + + disposed = true; + timeouts.dispose(); + intervals.dispose(); + immediates.dispose(); + } + + return { + scheduleTimeout: timeouts.add, + cancelTimeout: timeouts.remove, + scheduleInterval: intervals.add, + cancelInterval: intervals.remove, + scheduleImmediate: immediates.add, + cancelImmediate: immediates.remove, + isDisposed: isDisposed, + dispose: dispose + }; +} + +module.exports.createActivationScheduler = createActivationScheduler; diff --git a/src/runtime/settingsSnapshot.js b/src/runtime/settingsSnapshot.js index e0f46dad..b0c56d45 100644 --- a/src/runtime/settingsSnapshot.js +++ b/src/runtime/settingsSnapshot.js @@ -60,7 +60,7 @@ function buildSettingsSnapshot( context, identity, config, vscode ) highlightDelay: identity.getSetting( 'highlights.highlightDelay', 500 ), customHighlight: identity.getSetting( 'highlights.customHighlight', {} ), defaultHighlight: identity.getSetting( 'highlights.defaultHighlight', {} ), - useColourScheme: identity.getSetting( 'highlights.useColourScheme', true ), + useColourScheme: config.shouldUseColourScheme(), foregroundColourScheme: identity.getSetting( 'highlights.foregroundColourScheme', [] ), backgroundColourScheme: identity.getSetting( 'highlights.backgroundColourScheme', [] ), customCommentPatterns: identity.getSetting( 'languages.customPatterns', [] ), diff --git a/src/searchResults.js b/src/searchResults.js index c2783c39..cf1c6aa9 100644 --- a/src/searchResults.js +++ b/src/searchResults.js @@ -48,11 +48,6 @@ function toResultArray( resultMap ) return Array.from( resultMap.values() ); } -function cloneCounts( counts ) -{ - return Object.assign( {}, counts ); -} - function createEntry( uri ) { return { diff --git a/src/tree.js b/src/tree.js index 7668920f..6f56b8b2 100644 --- a/src/tree.js +++ b/src/tree.js @@ -531,6 +531,7 @@ class TreeNodeProvider this._rootListDirty = false; this._pendingRefreshRoots = undefined; this._expandedStateWriteHandle = undefined; + this._disposed = false; } getChildren( node ) @@ -1455,16 +1456,16 @@ class TreeNodeProvider return children === undefined ? nodes : children; } - getElement( filename, found, children ) + getElement( filename ) { var indexedNodes = Array.from( this._nodesByFsPath.get( filename ) || [] ).filter( isVisible ); if( indexedNodes.length > 0 ) { - var pathNode = indexedNodes.find( isPathNode ) || indexedNodes[ 0 ]; - found( pathNode ); - return; + return indexedNodes.find( isPathNode ) || indexedNodes[ 0 ]; } + + return undefined; } setExpanded( path, expanded ) @@ -1583,7 +1584,15 @@ class TreeNodeProvider dispose() { + if( this._disposed === true ) + { + return; + } + + this._disposed = true; clearTimeout( this._expandedStateWriteHandle ); + this._expandedStateWriteHandle = undefined; + this._onDidChangeTreeData.dispose(); } } diff --git a/src/utils.js b/src/utils.js index 0ddce3b1..bbb1f056 100644 --- a/src/utils.js +++ b/src/utils.js @@ -301,11 +301,6 @@ function removeLineComments( text, fileName ) return result; } -function getTagRegex() -{ - return getTagRegexSource(); -} - function escapeRegexLiteral( value ) { return regexRegistry.escapeRegexLiteral( value ); diff --git a/test/activationScheduler.behavior.test.js b/test/activationScheduler.behavior.test.js new file mode 100644 index 00000000..239e1240 --- /dev/null +++ b/test/activationScheduler.behavior.test.js @@ -0,0 +1,126 @@ +var activationScheduler = require( '../src/runtime/activationScheduler.js' ); + +function createClock( synchronousMethod ) +{ + var handles = []; + + function schedule( method, callback, delay ) + { + var handle = { + method: method, + callback: callback, + delay: delay, + cancellations: 0 + }; + handles.push( handle ); + if( synchronousMethod === method ) + { + callback(); + } + return handle; + } + + function cancel( handle ) + { + handle.cancellations++; + } + + return { + handles: handles, + setTimeout: function( callback, delay ) { return schedule( 'timeout', callback, delay ); }, + clearTimeout: cancel, + setInterval: function( callback, delay ) { return schedule( 'interval', callback, delay ); }, + clearInterval: cancel, + setImmediate: function( callback ) { return schedule( 'immediate', callback ); }, + clearImmediate: cancel + }; +} + +QUnit.module( 'activation scheduler' ); + +QUnit.test( 'completed one-shot work leaves no disposable handle', function( assert ) +{ + var clock = createClock(); + var scheduler = activationScheduler.createActivationScheduler( clock ); + var calls = 0; + var handle = scheduler.scheduleTimeout( function() { calls++; }, 25 ); + + handle.callback(); + scheduler.dispose(); + + assert.equal( calls, 1 ); + assert.equal( handle.cancellations, 0 ); +} ); + +QUnit.test( 'disposal cancels pending work once and suppresses retained callbacks', function( assert ) +{ + var clock = createClock(); + var scheduler = activationScheduler.createActivationScheduler( clock ); + var calls = 0; + var timeout = scheduler.scheduleTimeout( function() { calls++; }, 25 ); + var interval = scheduler.scheduleInterval( function() { calls++; }, 50 ); + var immediate = scheduler.scheduleImmediate( function() { calls++; } ); + + scheduler.dispose(); + scheduler.dispose(); + timeout.callback(); + interval.callback(); + immediate.callback(); + + assert.equal( calls, 0 ); + assert.equal( timeout.cancellations, 1 ); + assert.equal( interval.cancellations, 1 ); + assert.equal( immediate.cancellations, 1 ); + assert.strictEqual( scheduler.scheduleTimeout( function() {}, 1 ), undefined ); +} ); + +QUnit.test( 'explicit cancellation removes handles from activation disposal', function( assert ) +{ + var clock = createClock(); + var scheduler = activationScheduler.createActivationScheduler( clock ); + var timeout = scheduler.scheduleTimeout( function() {}, 25 ); + var interval = scheduler.scheduleInterval( function() {}, 50 ); + var immediate = scheduler.scheduleImmediate( function() {} ); + + scheduler.cancelTimeout( timeout ); + scheduler.cancelInterval( interval ); + scheduler.cancelImmediate( immediate ); + scheduler.dispose(); + + assert.deepEqual( clock.handles.map( function( handle ) { return handle.cancellations; } ), [ 1, 1, 1 ] ); +} ); + +QUnit.test( 'synchronous one-shot clocks preserve callback execution and cleanup', function( assert ) +{ + [ 'timeout', 'immediate' ].forEach( function( method ) + { + var clock = createClock( method ); + var scheduler = activationScheduler.createActivationScheduler( clock ); + var calls = 0; + var handle = method === 'timeout' ? + scheduler.scheduleTimeout( function() { calls++; }, 0 ) : + scheduler.scheduleImmediate( function() { calls++; } ); + + scheduler.dispose(); + + assert.equal( calls, 1, method + ' callback count' ); + assert.equal( handle.cancellations, 0, method + ' cancellation count' ); + } ); +} ); + +QUnit.test( 'clock contract rejects missing scheduling methods', function( assert ) +{ + var thrown; + + try + { + activationScheduler.createActivationScheduler( {} ); + } + catch( error ) + { + thrown = error; + } + + assert.ok( thrown instanceof TypeError ); + assert.equal( thrown.message, 'activation scheduler requires clock.setTimeout' ); +} ); diff --git a/test/config.behavior.test.js b/test/config.behavior.test.js index 7444b022..92c2a5ea 100644 --- a/test/config.behavior.test.js +++ b/test/config.behavior.test.js @@ -137,6 +137,19 @@ function createDirectoryEntry( name ) QUnit.module( 'behavioral config' ); +QUnit.test( 'issue #110 colour schemes require explicit opt-in', function( assert ) +{ + var defaultConfig = loadConfigModule(); + var optedInConfig = loadConfigModule( { + settingValues: { + 'highlights.useColourScheme': true + } + } ); + + assert.strictEqual( defaultConfig.shouldUseColourScheme(), false ); + assert.strictEqual( optedInConfig.shouldUseColourScheme(), true ); +} ); + QUnit.test( 'regex reads only language-overridable regex source with a resource URI', function( assert ) { var uri = { toString: function() { return '/workspace/source.vue'; } }; diff --git a/test/extension.scan-parity.test.js b/test/extension.scan-parity.test.js index 5c35eed3..9fb57d5e 100644 --- a/test/extension.scan-parity.test.js +++ b/test/extension.scan-parity.test.js @@ -329,6 +329,14 @@ function createProviderStub( options ) exportTree: function() { return {}; }, hasSubTags: function() { return false; }, getChildren: function() { return []; }, + getElement: function( filename ) + { + if( typeof ( options.getElementImpl ) === 'function' ) + { + return options.getElementImpl( filename ); + } + return options.treeElement; + }, clearExpansionState: function() {}, setExpanded: function() {}, dispose: function() {} @@ -394,6 +402,8 @@ function createVscodeStub( options ) var progressSessions = []; var statusBarItems = []; var treeViews = []; + var decorationTypes = []; + var outputChannels = []; var configurationUpdates = []; var automaticGitRefreshInterval = options.automaticGitRefreshInterval !== undefined ? options.automaticGitRefreshInterval : 0; var periodicRefreshInterval = options.periodicRefreshInterval !== undefined ? options.periodicRefreshInterval : 0; @@ -428,7 +438,7 @@ function createVscodeStub( options ) expand: false, export: false }, - trackFile: false, + trackFile: options.trackFile === true, expanded: false, flat: false, tagsOnly: false, @@ -441,7 +451,7 @@ function createVscodeStub( options ) }, filtering: filteringDefaults, general: { - debug: false, + debug: options.debug === true, automaticGitRefreshInterval: automaticGitRefreshInterval, periodicRefreshInterval: periodicRefreshInterval, rootFolder: "", @@ -451,7 +461,6 @@ function createVscodeStub( options ) }, ripgrep: { ripgrepArgs: '', - ripgrepMaxBuffer: 200, usePatternFile: false }, regex: { @@ -460,7 +469,7 @@ function createVscodeStub( options ) }, undefined, configurationUpdates, options.configurationUpdateImpl ); var generalSection = createConfigurationSection( { - debug: false, + debug: options.debug === true, automaticGitRefreshInterval: automaticGitRefreshInterval, periodicRefreshInterval: periodicRefreshInterval, rootFolder: "", @@ -475,7 +484,7 @@ function createVscodeStub( options ) }, undefined, configurationUpdates ); var treeSection = createConfigurationSection( { autoRefresh: true, - trackFile: false, + trackFile: options.trackFile === true, showCountsInTree: false, showBadges: false, scanMode: options.scanMode, @@ -502,7 +511,6 @@ function createVscodeStub( options ) }, undefined, configurationUpdates ); var ripgrepSection = createConfigurationSection( { ripgrepArgs: '', - ripgrepMaxBuffer: 200, usePatternFile: false }, undefined, configurationUpdates ); @@ -530,14 +538,14 @@ function createVscodeStub( options ) return { dispose: function() {} }; } - function createTreeView( id, options ) + function createTreeView( id, viewOptions ) { var view = { badge: undefined, title: 'Tree', message: '', - visible: false, - treeDataProvider: options && options.treeDataProvider, + visible: options.treeViewVisible === true, + treeDataProvider: viewOptions && viewOptions.treeDataProvider, revealCalls: [], onDidExpandElement: function( listener ) { return registerListener( windowListeners, 'expand', listener ); }, onDidCollapseElement: function( listener ) { return registerListener( windowListeners, 'collapse', listener ); }, @@ -547,6 +555,13 @@ function createVscodeStub( options ) element: element, options: revealOptions } ); + if( typeof ( options.revealImpl ) === 'function' ) + { + return Promise.resolve().then( function() + { + return options.revealImpl( element, revealOptions ); + } ); + } return Promise.resolve(); } }; @@ -599,6 +614,7 @@ function createVscodeStub( options ) { this.event = function() {}; this.fire = function() {}; + this.dispose = function() {}; } function TreeItem( label ) @@ -622,6 +638,8 @@ function createVscodeStub( options ) progressSessions: progressSessions, statusBarItems: statusBarItems, treeViews: treeViews, + decorationTypes: decorationTypes, + outputChannels: outputChannels, configurationUpdates: configurationUpdates, extensions: { all: options.extensions || [ { @@ -659,7 +677,22 @@ function createVscodeStub( options ) }, parse: function( value ) { - return { path: value, fsPath: value, toString: function() { return value; } }; + return { + path: value, + fsPath: value, + query: '', + with: function( changes ) + { + var query = changes.query; + return { + path: value, + fsPath: value, + query: query, + toString: function() { return value + '?' + query; } + }; + }, + toString: function() { return value; } + }; } }, env: { @@ -738,10 +771,29 @@ function createVscodeStub( options ) }, createOutputChannel: function() { - return { + var outputChannel = { appendLine: function() {}, - dispose: function() {} + disposeCalls: 0, + dispose: function() + { + this.disposeCalls++; + } + }; + outputChannels.push( outputChannel ); + return outputChannel; + }, + createTextEditorDecorationType: function( decorationOptions ) + { + var decoration = { + options: decorationOptions, + disposeCalls: 0, + dispose: function() + { + this.disposeCalls++; + } }; + decorationTypes.push( decoration ); + return decoration; }, showInformationMessage: function( message ) { @@ -881,7 +933,7 @@ function createExtensionHarness( options ) } } ); var context = { - subscriptions: { push: function() {} }, + subscriptions: [], workspaceState: options.workspaceState || matrixHelpers.createWorkspaceState(), globalState: options.globalState || matrixHelpers.createWorkspaceState(), storageUri: matrixHelpers.createUri( '/tmp/storage' ), @@ -1484,6 +1536,53 @@ function createDeferred() }; } +function createControlledTimers() +{ + var timeouts = []; + var intervals = []; + + function createHandle( callback, delay ) + { + return { + callback: callback, + delay: delay, + cancelled: false, + unref: function() {} + }; + } + + return { + timeouts: timeouts, + intervals: intervals, + setTimeout: function( callback, delay ) + { + var handle = createHandle( callback, delay ); + timeouts.push( handle ); + return handle; + }, + clearTimeout: function( handle ) + { + if( handle ) + { + handle.cancelled = true; + } + }, + setInterval: function( callback, delay ) + { + var handle = createHandle( callback, delay ); + intervals.push( handle ); + return handle; + }, + clearInterval: function( handle ) + { + if( handle ) + { + handle.cancelled = true; + } + } + }; +} + function summarizeNotebookResults( results ) { return results.map( function( result ) @@ -4932,6 +5031,39 @@ QUnit.test( "exportTree reports document open failures through the command bound } ); } ); +QUnit.test( "exportTree uses a fresh virtual document generation for repeated paths", function( assert ) +{ + var openedUris = []; + var harness = createExtensionHarness( { + scanMode: 'open files', + scanAtStartup: false, + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + openTextDocumentImpl: function( uri ) + { + openedUris.push( uri ); + return {}; + }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + + return matrixHelpers.flushAsyncWork().then( function() + { + return harness.vscode.commandHandlers[ 'better-todo-tree.exportTree' ](); + } ).then( function() + { + return harness.vscode.commandHandlers[ 'better-todo-tree.exportTree' ](); + } ).then( function() + { + assert.equal( openedUris.length, 2 ); + assert.equal( openedUris[ 0 ].path, openedUris[ 1 ].path ); + assert.equal( openedUris[ 0 ].query, 'generation=1' ); + assert.equal( openedUris[ 1 ].query, 'generation=2' ); + assert.notEqual( openedUris[ 0 ].toString(), openedUris[ 1 ].toString() ); + } ); +} ); + QUnit.test( "revealInFile reports VS Code open failures through the command boundary", function( assert ) { var harness = createExtensionHarness( { @@ -4967,6 +5099,203 @@ QUnit.test( "revealInFile reports VS Code open failures through the command boun } ); } ); +QUnit.test( "revealInFile disposes line flashes without writing to a closed editor", function( assert ) +{ + var timers = createControlledTimers(); + var decorationWrites = []; + var document = matrixHelpers.createDocument( '/workspace/file.js', '// TODO item' ); + var editor = { + document: document, + selection: { active: { line: 0, character: 0 } }, + setDecorations: function( decoration, ranges ) + { + if( document.isClosed === true ) + { + throw new Error( 'closed editor write' ); + } + decorationWrites.push( { decoration: decoration, ranges: ranges } ); + } + }; + var harness = createExtensionHarness( { + scanMode: 'open files', + scanAtStartup: false, + activeTextEditor: editor, + timerStubs: timers, + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + + return matrixHelpers.flushAsyncWork().then( function() + { + return harness.vscode.commandHandlers[ 'better-todo-tree.revealInFile' ]( document.uri, undefined ); + } ).then( function() + { + var flashTimer = timers.timeouts.find( function( timer ) { return timer.delay === 150; } ); + + assert.ok( flashTimer ); + assert.equal( decorationWrites.length, 1 ); + assert.equal( decorationWrites[ 0 ].ranges.length, 1 ); + + document.isClosed = true; + harness.vscode.window.activeTextEditor = undefined; + flashTimer.callback(); + + assert.equal( decorationWrites.length, 1 ); + assert.equal( harness.vscode.decorationTypes[ 0 ].disposeCalls, 1 ); + harness.extension.deactivate(); + } ); +} ); + +QUnit.test( "active editor tracking cancels stale delayed reveals", function( assert ) +{ + var timers = createControlledTimers(); + var requestedFiles = []; + var firstDocument = matrixHelpers.createDocument( '/workspace/first.js', '// TODO first' ); + var secondDocument = matrixHelpers.createDocument( '/workspace/second.js', '// TODO second' ); + var firstEditor = { document: firstDocument }; + var secondEditor = { document: secondDocument }; + var treeElement = { fsPath: '/workspace/second.js' }; + var harness = createExtensionHarness( { + scanMode: 'open files', + scanAtStartup: false, + trackFile: true, + treeViewVisible: true, + treeElement: treeElement, + activeTextEditor: firstEditor, + visibleTextEditors: [ firstEditor, secondEditor ], + timerStubs: timers, + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + getElementImpl: function( filename ) + { + requestedFiles.push( filename ); + return treeElement; + }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + harness.vscode.workspaceListeners.activeEditor( firstEditor ); + harness.vscode.window.activeTextEditor = secondEditor; + harness.vscode.workspaceListeners.activeEditor( secondEditor ); + + var revealTimers = timers.timeouts.filter( function( timer ) { return timer.delay === 500; } ); + + assert.equal( revealTimers.length, 2 ); + assert.true( revealTimers[ 0 ].cancelled ); + assert.false( revealTimers[ 1 ].cancelled ); + revealTimers[ 1 ].callback(); + + return matrixHelpers.flushAsyncWork().then( function() + { + assert.deepEqual( requestedFiles, [ '/workspace/second.js' ] ); + assert.equal( harness.vscode.treeViews[ 0 ].revealCalls.length, 1 ); + assert.strictEqual( harness.vscode.treeViews[ 0 ].revealCalls[ 0 ].element, treeElement ); + harness.extension.deactivate(); + } ); +} ); + +QUnit.test( "reveal command propagates tree view failures through the command boundary", function( assert ) +{ + var document = matrixHelpers.createDocument( '/workspace/file.js', '// TODO item' ); + var editor = { document: document }; + var harness = createExtensionHarness( { + scanMode: 'open files', + scanAtStartup: false, + treeViewVisible: true, + treeElement: { fsPath: document.fileName }, + activeTextEditor: editor, + visibleTextEditors: [ editor ], + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + revealImpl: function() + { + return Promise.reject( new Error( 'tree reveal failed' ) ); + }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + + return matrixHelpers.flushAsyncWork().then( function() + { + return harness.vscode.commandHandlers[ 'better-todo-tree.reveal' ]().then( function() + { + assert.ok( false, 'reveal rejects tree view failures' ); + }, function( error ) + { + assert.equal( error.message, 'tree reveal failed' ); + assert.deepEqual( harness.vscode.errorMessages, [ + 'Better Todo Tree: failed to reveal active editor in tree (tree reveal failed)' + ] ); + harness.extension.deactivate(); + } ); + } ); +} ); + +QUnit.test( "deactivation cancels scheduled work and disposes activation-owned resources", function( assert ) +{ + var timers = createControlledTimers(); + var document = matrixHelpers.createDocument( '/workspace/file.js', '// TODO item' ); + var editor = { + document: document, + selection: { active: { line: 0, character: 0 } }, + setDecorations: function() {} + }; + var harness = createExtensionHarness( { + scanMode: 'open files', + scanAtStartup: false, + debug: true, + automaticGitRefreshInterval: 1, + periodicRefreshInterval: 1, + activeTextEditor: editor, + timerStubs: timers, + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + + return matrixHelpers.flushAsyncWork().then( function() + { + return harness.vscode.commandHandlers[ 'better-todo-tree.revealInFile' ]( document.uri, undefined ); + } ).then( function() + { + assert.equal( harness.vscode.outputChannels.length, 1 ); + assert.equal( harness.vscode.decorationTypes.length, 1 ); + assert.equal( timers.intervals.length, 2 ); + + harness.extension.deactivate(); + harness.extension.deactivate(); + + assert.true( timers.timeouts.every( function( timer ) { return timer.cancelled; } ) ); + assert.true( timers.intervals.every( function( timer ) { return timer.cancelled; } ) ); + assert.equal( harness.vscode.outputChannels[ 0 ].disposeCalls, 1 ); + assert.equal( harness.vscode.decorationTypes[ 0 ].disposeCalls, 1 ); + assert.equal( harness.provider.clearCalls, 1 ); + } ); +} ); + +QUnit.test( "deactivation settles pending tree UI work without touching the disposed view", function( assert ) +{ + var harness = createExtensionHarness( { + scanMode: 'open files', + scanAtStartup: false, + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + + var expansionPromise = harness.vscode.commandHandlers[ 'better-todo-tree.expand' ](); + harness.extension.deactivate(); + + return expansionPromise.then( function() + { + assert.equal( harness.vscode.treeViews[ 0 ].revealCalls.length, 0 ); + } ); +} ); + QUnit.test( "markdown migration writes the shared default regex source", function( assert ) { var harness = createExtensionHarness( { diff --git a/test/highlights.behavior.test.js b/test/highlights.behavior.test.js index 8772374a..2052aa1e 100644 --- a/test/highlights.behavior.test.js +++ b/test/highlights.behavior.test.js @@ -7,6 +7,8 @@ var issue888Helpers = require( './issue888Helpers.js' ); function createVscodeStub( highlightConfiguration, decorationLog ) { + var visibleTextEditorsListener; + function Position( line, character ) { this.line = line; @@ -29,10 +31,16 @@ function createVscodeStub( highlightConfiguration, decorationLog ) Position: Position, Range: Range, window: { + visibleTextEditors: [], createTextEditorDecorationType: function( options ) { decorationLog.push( options ); return options; + }, + onDidChangeVisibleTextEditors: function( listener ) + { + visibleTextEditorsListener = listener; + return { dispose: function() {} }; } }, workspace: { @@ -50,6 +58,11 @@ function createVscodeStub( highlightConfiguration, decorationLog ) } }; } + }, + setVisibleTextEditors: function( editors ) + { + this.window.visibleTextEditors = editors; + visibleTextEditorsListener( editors ); } }; } @@ -412,7 +425,7 @@ QUnit.module( "behavioral highlights", function( hooks ) assert.strictEqual( decoration.dark.color, undefined ); } ); - QUnit.test( "issue #58 default colour scheme emits visible editor highlights", function( assert ) + QUnit.test( "issue #58 explicit colour scheme emits visible editor highlights", function( assert ) { var decorationLog = []; var config = createAttributeConfig( { @@ -473,6 +486,74 @@ QUnit.module( "behavioral highlights", function( hooks ) assert.equal( decoration.dark.color, 'white' ); } ); + QUnit.test( "issue #110 foreground-only custom highlights receive no implicit background", function( assert ) + { + var decorationLog = []; + var config = createAttributeConfig( { + tagList: [ 'TODO' ], + foregroundColourScheme: function() + { + return [ 'white' ]; + }, + backgroundColourScheme: function() + { + return [ 'red' ]; + }, + customHighlight: function() + { + return { + TODO: { + foreground: '#008888', + type: 'tag', + textDecoration: 'underline' + } + }; + } + } ); + + actualUtils.init( config ); + actualAttributes.init( config ); + + var highlights = helpers.loadWithStubs( '../src/highlights.js', { + vscode: createVscodeStub( { enabled: true }, decorationLog ), + './config.js': { + customHighlight: config.customHighlight.bind( config ), + subTagRegex: function() { return regexRegistry.pattern( 'subTagPrefixCapture' ); }, + tagGroup: function() { return undefined; } + }, + './utils.js': actualUtils, + './attributes.js': actualAttributes, + './icons.js': { + getGutterIcon: function() + { + return { dark: '/tmp/gutter-dark.svg', light: '/tmp/gutter-light.svg' }; + } + }, + './detection.js': { + scanDocument: function() + { + return []; + } + }, + './extensionIdentity.js': { + getSetting: function( setting, defaultValue ) + { + return defaultValue; + } + } + } ); + + highlights.init( { subscriptions: { push: function() {} } }, function() {} ); + + var decoration = highlights.getDecoration( 'TODO' ); + + assert.equal( decoration.light.color, '#008888' ); + assert.equal( decoration.dark.color, '#008888' ); + assert.strictEqual( decoration.light.backgroundColor, undefined ); + assert.strictEqual( decoration.dark.backgroundColor, undefined ); + assert.equal( decoration.textDecoration, 'underline' ); + } ); + QUnit.test( "issue #75 identical yellow backgrounds use identical black foregrounds", function( assert ) { var decorationLog = []; @@ -957,6 +1038,92 @@ QUnit.module( "behavioral highlights", function( hooks ) assert.equal( recorded[ 0 ][ 0 ].range.end.character, 2 ); } ); + QUnit.test( "delayed highlights skip editors removed without a visibility event", function( assert ) + { + var done = assert.async(); + var scanCount = 0; + var vscodeStub = createVscodeStub( { enabled: true }, [] ); + var highlights = helpers.loadWithStubs( '../src/highlights.js', { + vscode: vscodeStub, + './config.js': { + tagGroup: function() { return undefined; } + }, + './detection.js': { + scanDocument: function() + { + scanCount++; + return []; + } + }, + './extensionIdentity.js': { + getSetting: function( setting, defaultValue ) + { + return setting === 'highlights.highlightDelay' ? 5 : defaultValue; + } + } + } ); + var editor = { + viewColumn: 1, + document: createDocument( '// TODO body' ), + setDecorations: function() {} + }; + + highlights.init( { subscriptions: { push: function() {} } }, function() {} ); + vscodeStub.window.visibleTextEditors = [ editor ]; + highlights.triggerHighlight( editor ); + vscodeStub.window.visibleTextEditors = []; + + setTimeout( function() + { + assert.equal( scanCount, 0 ); + highlights.resetCaches(); + done(); + }, 25 ); + } ); + + QUnit.test( "visibility changes cancel pending highlights for removed editors", function( assert ) + { + var done = assert.async(); + var scanCount = 0; + var vscodeStub = createVscodeStub( { enabled: true }, [] ); + var highlights = helpers.loadWithStubs( '../src/highlights.js', { + vscode: vscodeStub, + './config.js': { + tagGroup: function() { return undefined; } + }, + './detection.js': { + scanDocument: function() + { + scanCount++; + return []; + } + }, + './extensionIdentity.js': { + getSetting: function( setting, defaultValue ) + { + return setting === 'highlights.highlightDelay' ? 5 : defaultValue; + } + } + } ); + var editor = { + viewColumn: 1, + document: createDocument( '// TODO body' ), + setDecorations: function() {} + }; + + highlights.init( { subscriptions: { push: function() {} } }, function() {} ); + vscodeStub.window.visibleTextEditors = [ editor ]; + highlights.triggerHighlight( editor ); + vscodeStub.setVisibleTextEditors( [] ); + + setTimeout( function() + { + assert.equal( scanCount, 0 ); + highlights.resetCaches(); + done(); + }, 25 ); + } ); + QUnit.test( "repeated highlights reuse cached decoration types for unchanged content", function( assert ) { var creationCount = 0; @@ -975,10 +1142,15 @@ QUnit.module( "behavioral highlights", function( hooks ) this.end = end; }, window: { + visibleTextEditors: [], createTextEditorDecorationType: function( options ) { creationCount++; return options; + }, + onDidChangeVisibleTextEditors: function() + { + return { dispose: function() {} }; } } }, diff --git a/test/highlights.matrix.test.js b/test/highlights.matrix.test.js index 9dfe2458..a435b919 100644 --- a/test/highlights.matrix.test.js +++ b/test/highlights.matrix.test.js @@ -27,6 +27,10 @@ function createVscodeStub( highlightConfiguration, decorationLog ) Position: Position, Range: Range, window: { + onDidChangeVisibleTextEditors: function() + { + return { dispose: function() {} }; + }, createTextEditorDecorationType: function( options ) { decorationLog.push( options ); diff --git a/test/package.manifest.test.js b/test/package.manifest.test.js index 7abba781..7695b65b 100644 --- a/test/package.manifest.test.js +++ b/test/package.manifest.test.js @@ -50,8 +50,58 @@ function getConfigurationProperty( propertyName ) return languageMatrix.findConfigurationProperty( propertyName ); } +function collectLocalizationKeys( value, keys ) +{ + keys = keys || new Set(); + + if( typeof ( value ) === 'string' && value[ 0 ] === '%' && value[ value.length - 1 ] === '%' ) + { + keys.add( value.slice( 1, -1 ) ); + } + else if( Array.isArray( value ) ) + { + value.forEach( function( entry ) { collectLocalizationKeys( entry, keys ); } ); + } + else if( value && typeof ( value ) === 'object' ) + { + Object.keys( value ).forEach( function( key ) { collectLocalizationKeys( value[ key ], keys ); } ); + } + + return keys; +} + QUnit.module( 'package manifest' ); +QUnit.test( 'every manifest localization key exists in both language bundles', function( assert ) +{ + var keys = Array.from( collectLocalizationKeys( readPackageJson() ) ).sort(); + var english = readPackageNls( 'package.nls.json' ); + var chinese = readPackageNls( 'package.nls.zh-cn.json' ); + + assert.ok( keys.length > 0 ); + keys.forEach( function( key ) + { + assert.equal( typeof ( english[ key ] ), 'string', 'English: ' + key ); + assert.equal( typeof ( chinese[ key ] ), 'string', 'Simplified Chinese: ' + key ); + } ); +} ); + +QUnit.test( 'simplified Chinese localizes every manifest string except product names', function( assert ) +{ + var keys = Array.from( collectLocalizationKeys( readPackageJson() ) ).sort(); + var english = readPackageNls( 'package.nls.json' ); + var chinese = readPackageNls( 'package.nls.zh-cn.json' ); + var productNameKeys = new Set( [ + 'better-todo-tree.configuration.ripgrep' + ] ); + var untranslated = keys.filter( function( key ) + { + return productNameKeys.has( key ) !== true && english[ key ] === chinese[ key ]; + } ); + + assert.deepEqual( untranslated, [] ); +} ); + QUnit.test( 'stable hidden view ids are preserved while the public namespace is rebranded', function( assert ) { var packageJson = readPackageJson(); @@ -196,13 +246,16 @@ QUnit.test( 'regex defaults are generated by the shared registry', function( ass assert.equal( legacyRegex.default, regexRegistry.DEFAULT_REGEX_SOURCE ); } ); -QUnit.test( 'issue #58 highlight colour scheme is enabled by default', function( assert ) +QUnit.test( 'issue #110 highlight colour scheme is opt-in in both namespaces', function( assert ) { var currentUseColourScheme = getConfigurationProperty( 'better-todo-tree.highlights.useColourScheme' ); var legacyUseColourScheme = getConfigurationProperty( 'todo-tree.highlights.useColourScheme' ); + var readme = readRepositoryFile( 'README.md' ).toString( 'utf8' ); - assert.strictEqual( currentUseColourScheme.default, true ); - assert.strictEqual( legacyUseColourScheme.default, true ); + assert.strictEqual( currentUseColourScheme.default, false ); + assert.strictEqual( legacyUseColourScheme.default, false ); + assert.ok( readme.indexOf( '**better-todo-tree.highlights.foregroundColourScheme**' ) !== -1 ); + assert.strictEqual( readme.indexOf( 'highlights.foreroundColourScheme' ), -1 ); } ); QUnit.test( 'ripgrep executable setting documents packaged binary behavior', function( assert ) @@ -219,6 +272,38 @@ QUnit.test( 'ripgrep executable setting documents packaged binary behavior', fun ); } ); +QUnit.test( 'release targets match the Node and native-ripgrep runtime contract', function( assert ) +{ + var packageJson = readPackageJson(); + var targets = require( '../scripts/release/targets.json' ); + var readme = readRepositoryFile( 'README.md' ).toString( 'utf8' ); + + assert.equal( packageJson.main, './dist/extension' ); + assert.strictEqual( packageJson.browser, undefined ); + assert.equal( targets.length, 9 ); + assert.strictEqual( targets.includes( 'web' ), false ); + assert.ok( readme.indexOf( 'Browser-hosted VS Code is excluded' ) !== -1 ); +} ); + +QUnit.test( 'streaming ripgrep output exposes no inert buffer-size setting', function( assert ) +{ + var configurationProperties = readPackageJson().contributes.configuration.reduce( function( properties, group ) + { + return Object.assign( properties, group.properties || {} ); + }, {} ); + var readme = readRepositoryFile( 'README.md' ).toString( 'utf8' ); + var packageNls = readPackageNls( 'package.nls.json' ); + var packageNlsZhCn = readPackageNls( 'package.nls.zh-cn.json' ); + var extensionSource = readRepositoryFile( 'src/extension.js' ).toString( 'utf8' ); + + assert.false( Object.prototype.hasOwnProperty.call( configurationProperties, 'better-todo-tree.ripgrep.ripgrepMaxBuffer' ) ); + assert.false( Object.prototype.hasOwnProperty.call( configurationProperties, 'todo-tree.ripgrep.ripgrepMaxBuffer' ) ); + assert.strictEqual( packageNls[ 'better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription' ], undefined ); + assert.strictEqual( packageNlsZhCn[ 'better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription' ], undefined ); + assert.strictEqual( readme.indexOf( 'ripgrepMaxBuffer' ), -1 ); + assert.strictEqual( extensionSource.indexOf( 'ripgrepMaxBuffer' ), -1 ); +} ); + QUnit.test( 'legacy settings remain present and deprecated', function( assert ) { var currentSetting = getConfigurationProperty( 'better-todo-tree.general.tags' ); diff --git a/test/packaging.ignore.test.js b/test/packaging.ignore.test.js index 86dd3e31..af025f1a 100644 --- a/test/packaging.ignore.test.js +++ b/test/packaging.ignore.test.js @@ -14,6 +14,8 @@ QUnit.test( '.vscodeignore excludes local workflow tooling and release-only docu 'TODOS_LISTS/', '.tools/', '.act-artifacts/', + '.forge/', + '.npmignore', 'MIGRATION.md', 'OPEN_VSX_CERTIFICATE_REPORT.md', 'CHANGELOG.upstream.md', @@ -38,6 +40,9 @@ QUnit.test( '.npmignore excludes local workflow tooling and keeps dist package r 'TODOS_LISTS/', '.tools/', '.act-artifacts/', + '.forge/', + '.devcontainer/', + '.vscodeignore', 'webpack.config.js', 'buildCodiconNames.js', 'Better.TODO.Tree.settings.txt' diff --git a/test/pr-vsix-bundle.test.js b/test/pr-vsix-bundle.test.js index ab2157be..6c714b10 100644 --- a/test/pr-vsix-bundle.test.js +++ b/test/pr-vsix-bundle.test.js @@ -34,7 +34,7 @@ QUnit.test( 'bundle filenames cover the complete canonical target matrix', async return packageMetadata.name + '-' + packageMetadata.version + '-' + target + '.vsix'; } ).sort(); - assert.equal( targets.length, 10 ); + assert.equal( targets.length, 9 ); assert.deepEqual( verifier.expectedBundleFileNames( packageMetadata, targets ), expected ); verifier.verifyTargetMap( targets ); } ); @@ -57,16 +57,23 @@ QUnit.test( 'native targets contain one matching executable and metadata set', a }, messageContains( 'metadata entries mismatch' ) ); } ); -QUnit.test( 'web target rejects native executables and metadata', async function( assert ) +QUnit.test( 'every published target requires one native executable and metadata set', async function( assert ) { var verifier = await verifierPromise; - assert.deepEqual( verifier.expectedRipgrepEntries( 'web' ), [] ); - assert.deepEqual( verifier.verifyEntrySet( [], 'web' ), [] ); + targets.forEach( function( target ) + { + var expected = verifier.expectedRipgrepEntries( target ); + + assert.equal( expected.length, 1, target + ' has one executable' ); + assert.deepEqual( verifier.verifyEntrySet( expected.concat( metadataEntries() ), target ), expected ); + } ); + + assert.notOk( targets.includes( 'web' ) ); assert.throws( function() { - verifier.verifyEntrySet( metadataEntries(), 'web' ); - }, messageContains( 'metadata entries mismatch' ) ); + verifier.expectedRipgrepEntries( 'web' ); + }, messageContains( 'Unsupported VSIX target' ) ); } ); QUnit.test( 'target map verifier rejects order and membership drift', async function( assert ) diff --git a/test/pr-vsix-comment.test.js b/test/pr-vsix-comment.test.js index 4bbc41f0..714df432 100644 --- a/test/pr-vsix-comment.test.js +++ b/test/pr-vsix-comment.test.js @@ -384,11 +384,14 @@ QUnit.test( 'successful current run publishes one platform bundle and removes du assert.deepEqual( results, [ { pullRequestNumber: 19, applied: true, removedArtifacts: 1 } ] ); assert.ok( body.indexOf( 'actions/runs/200/artifacts/300' ) !== -1 ); - assert.equal( body.split( '[Download VSIX]' ).length - 1, 10 ); + assert.equal( body.split( '[Download VSIX]' ).length - 1, TARGETS.length ); assert.ok( body.indexOf( 'Windows x64' ) !== -1 ); assert.ok( body.indexOf( 'macOS Apple silicon' ) !== -1 ); assert.ok( body.indexOf( '`win32-x64`' ) !== -1 ); - assert.ok( body.indexOf( '`web`' ) !== -1 ); + TARGETS.forEach( function( target ) + { + assert.ok( body.indexOf( '`' + target + '`' ) !== -1, target ); + } ); assert.ok( body.indexOf( 'Download the VSIX matching the test platform' ) !== -1 ); assert.ok( body.indexOf( 'unreviewed code from this pull request' ) !== -1 ); assert.deepEqual( fixture.calls.deletedArtifacts, [ 299 ] ); @@ -515,7 +518,7 @@ QUnit.test( 'successful rerun rejects an artifact created before the current att await assert.rejects( synchronize( module, fixture, rerun ), function( error ) { return error instanceof module.PrVsixInvariantError && - error.message.indexOf( 'expected 10 target artifacts, found 0' ) !== -1; + error.message.indexOf( 'expected ' + TARGETS.length + ' target artifacts, found 0' ) !== -1; } ); assert.deepEqual( fixture.calls.deletedArtifacts, [ 300 ] ); } ); diff --git a/test/release.workflow-scripts.test.js b/test/release.workflow-scripts.test.js index ac93e9a6..82ba441c 100644 --- a/test/release.workflow-scripts.test.js +++ b/test/release.workflow-scripts.test.js @@ -136,7 +136,7 @@ QUnit.test( 'release-artifacts enumerates release VSIX files in a stable order', var workspace = createWorkspace(); var result; - fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-web.vsix' ), 'web' ); + fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-arm64.vsix' ), 'arm64' ); fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-x64.vsix' ), 'linux' ); result = childProcess.spawnSync( @@ -156,8 +156,8 @@ QUnit.test( 'release-artifacts enumerates release VSIX files in a stable order', assert.deepEqual( result.stdout.trim().split( regexRegistry.createRegExp( 'optionalCarriageReturnLineBreak' ) ), [ - 'artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix', - 'artifacts/release/better-todo-tree-0.0.225-web.vsix' + 'artifacts/release/better-todo-tree-0.0.225-linux-arm64.vsix', + 'artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix' ] ); } ); @@ -172,7 +172,7 @@ QUnit.test( 'publish-vscode-marketplace publishes every VSIX with duplicate-safe } ); fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-x64.vsix' ), 'linux' ); - fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-web.vsix' ), 'web' ); + fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-arm64.vsix' ), 'arm64' ); makeExecutable( path.join( workspace.bin, 'npx' ), @@ -188,7 +188,7 @@ QUnit.test( 'publish-vscode-marketplace publishes every VSIX with duplicate-safe assert.strictEqual( result.status, 0, result.stderr ); assert.ok( callLog.indexOf( '@vscode/vsce publish --packagePath artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix -p vsce-test-token --skip-duplicate' ) !== -1 ); - assert.ok( callLog.indexOf( '@vscode/vsce publish --packagePath artifacts/release/better-todo-tree-0.0.225-web.vsix -p vsce-test-token --skip-duplicate' ) !== -1 ); + assert.ok( callLog.indexOf( '@vscode/vsce publish --packagePath artifacts/release/better-todo-tree-0.0.225-linux-arm64.vsix -p vsce-test-token --skip-duplicate' ) !== -1 ); assert.ok( callLog.indexOf( 'ovsx publish' ) === -1 ); } ); @@ -202,7 +202,7 @@ QUnit.test( 'publish-open-vsx publishes every VSIX with duplicate-safe flags', f } ); fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-x64.vsix' ), 'linux' ); - fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-web.vsix' ), 'web' ); + fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-arm64.vsix' ), 'arm64' ); makeExecutable( path.join( workspace.bin, 'npx' ), @@ -218,7 +218,7 @@ QUnit.test( 'publish-open-vsx publishes every VSIX with duplicate-safe flags', f assert.strictEqual( result.status, 0, result.stderr ); assert.ok( callLog.indexOf( 'ovsx publish --packagePath artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix -p ovsx-test-token --skip-duplicate' ) !== -1 ); - assert.ok( callLog.indexOf( 'ovsx publish --packagePath artifacts/release/better-todo-tree-0.0.225-web.vsix -p ovsx-test-token --skip-duplicate' ) !== -1 ); + assert.ok( callLog.indexOf( 'ovsx publish --packagePath artifacts/release/better-todo-tree-0.0.225-linux-arm64.vsix -p ovsx-test-token --skip-duplicate' ) !== -1 ); assert.ok( callLog.indexOf( '@vscode/vsce publish' ) === -1 ); } ); @@ -228,8 +228,8 @@ QUnit.test( 'publish-open-vsx waits on retryable registry failures and retries t var callLogPath = path.join( workspace.root, 'npx.log' ); var sleepLogPath = path.join( workspace.root, 'sleep.log' ); var counterPath = path.join( workspace.root, 'counter' ); - var firstPackage = path.join( 'artifacts', 'release', 'better-todo-tree-0.0.225-linux-x64.vsix' ); - var secondPackage = path.join( 'artifacts', 'release', 'better-todo-tree-0.0.225-web.vsix' ); + var firstPackage = path.join( 'artifacts', 'release', 'better-todo-tree-0.0.225-linux-arm64.vsix' ); + var secondPackage = path.join( 'artifacts', 'release', 'better-todo-tree-0.0.225-linux-x64.vsix' ); var env = Object.assign( {}, process.env, { PATH: workspace.bin + path.delimiter + process.env.PATH, OVSX_PAT: 'ovsx-test-token', @@ -237,7 +237,7 @@ QUnit.test( 'publish-open-vsx waits on retryable registry failures and retries t } ); fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-x64.vsix' ), 'linux' ); - fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-web.vsix' ), 'web' ); + fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-arm64.vsix' ), 'arm64' ); fs.writeFileSync( counterPath, '0\n' ); makeExecutable( @@ -450,7 +450,7 @@ QUnit.test( 'create-github-release uploads assets when the release already exist } ); fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-x64.vsix' ), 'linux' ); - fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-web.vsix' ), 'web' ); + fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-arm64.vsix' ), 'arm64' ); makeExecutable( path.join( workspace.bin, 'bash' ), '#!/bin/bash\n' + @@ -486,7 +486,7 @@ QUnit.test( 'create-github-release uploads assets when the release already exist assert.ok( callLog.indexOf( 'api repos/{owner}/{repo}/releases/tags/v0.0.225 --jq .id' ) !== -1 ); assert.ok( callLog.indexOf( 'api --method PATCH repos/{owner}/{repo}/releases/' ) !== -1 ); assert.ok( callLog.indexOf( '--title' ) === -1 ); - assert.ok( callLog.indexOf( 'release upload v0.0.225 artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix artifacts/release/better-todo-tree-0.0.225-web.vsix --clobber' ) !== -1 ); + assert.ok( callLog.indexOf( 'release upload v0.0.225 artifacts/release/better-todo-tree-0.0.225-linux-arm64.vsix artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix --clobber' ) !== -1 ); assert.ok( callLog.indexOf( 'release create v0.0.225' ) === -1 ); } ); @@ -504,7 +504,7 @@ QUnit.test( 'create-or-update-moving-github-release creates a moving prerelease } ); fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-x64.vsix' ), 'linux' ); - fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-web.vsix' ), 'web' ); + fs.writeFileSync( path.join( workspace.artifacts, 'better-todo-tree-0.0.225-linux-arm64.vsix' ), 'arm64' ); makeExecutable( path.join( workspace.bin, 'bash' ), '#!/bin/bash\n' + @@ -548,7 +548,7 @@ QUnit.test( 'create-or-update-moving-github-release creates a moving prerelease assert.ok( callLog.indexOf( 'git tag -f latest 0123456789abcdef0123456789abcdef01234567' ) !== -1 ); assert.ok( callLog.indexOf( 'git push --force origin refs/tags/latest' ) !== -1 ); assert.ok( callLog.indexOf( 'gh release view latest' ) !== -1 ); - assert.ok( callLog.indexOf( 'gh release create latest artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix artifacts/release/better-todo-tree-0.0.225-web.vsix --title Latest Nightly Build (abc123def456) --notes-file ' ) !== -1 ); + assert.ok( callLog.indexOf( 'gh release create latest artifacts/release/better-todo-tree-0.0.225-linux-arm64.vsix artifacts/release/better-todo-tree-0.0.225-linux-x64.vsix --title Latest Nightly Build (abc123def456) --notes-file ' ) !== -1 ); assert.ok( callLog.indexOf( '--prerelease --target 0123456789abcdef0123456789abcdef01234567' ) !== -1 ); } ); @@ -844,7 +844,7 @@ QUnit.test( 'verify-vscode-marketplace waits for public version metadata and cha fs.mkdirSync( path.join( workspace.root, 'scripts', 'release' ), { recursive: true } ); fs.writeFileSync( path.join( workspace.root, 'scripts', 'release', 'targets.json' ), - JSON.stringify( [ 'linux-x64', 'web' ], null, 4 ) + '\n' + JSON.stringify( [ 'linux-x64', 'linux-arm64' ], null, 4 ) + '\n' ); fs.writeFileSync( expectedChangelogPath, @@ -884,7 +884,7 @@ QUnit.test( 'verify-vscode-marketplace waits for public version metadata and cha }, { version: '0.0.228', - targetPlatform: state.queryCount > 1 ? 'web' : 'missing', + targetPlatform: state.queryCount > 1 ? 'linux-arm64' : 'missing', files: [ { assetType: 'Microsoft.VisualStudio.Services.Content.Changelog', @@ -959,7 +959,7 @@ QUnit.test( 'verify-vscode-marketplace waits for public version metadata and cha { assert.strictEqual( code, 0, stderr ); assert.ok( stdout.indexOf( 'Marketplace version 0.0.228 is publicly available' ) !== -1 ); - assert.ok( stdout.indexOf( 'linux-x64, web' ) !== -1 ); + assert.ok( stdout.indexOf( 'linux-arm64, linux-x64' ) !== -1 ); assert.ok( state.queryCount >= 2, String( state.queryCount ) ); done(); } ); diff --git a/test/ripgrep.test.js b/test/ripgrep.test.js index 2ccd3a58..36b55c52 100644 --- a/test/ripgrep.test.js +++ b/test/ripgrep.test.js @@ -396,10 +396,12 @@ QUnit.module( "ripgrep streaming search", function( hooks ) QUnit.test( "malformed ripgrep json reports pattern cleanup failures", function( assert ) { var originalUnlink = fs.promises.unlink; + var unlinkCalls = 0; var patternFilePath = path.join( os.tmpdir(), 'todo-tree-pattern-json-test.txt' ); fs.promises.unlink = function() { + unlinkCalls++; return Promise.reject( Object.assign( new Error( 'json cleanup failed' ), { code: 'EPERM' } ) ); }; @@ -428,6 +430,7 @@ QUnit.module( "ripgrep streaming search", function( hooks ) { assert.ok( error instanceof ripgrep.RipgrepError ); assert.equal( error.message, 'json cleanup failed' ); + assert.equal( unlinkCalls, 1 ); } ).finally( function() { fs.promises.unlink = originalUnlink; @@ -533,7 +536,7 @@ QUnit.module( "ripgrep streaming search", function( hooks ) } ); } ); - QUnit.test( "kill cancels only the active search state", function( assert ) + QUnit.test( "kill cancels every concurrent search", function( assert ) { var firstProcess = createFakeProcess(); var secondProcess = createFakeProcess(); @@ -581,21 +584,17 @@ QUnit.module( "ripgrep streaming search", function( hooks ) { ripgrep.kill(); - return secondSearch.then( function() - { - assert.ok( false, 'expected second search cancellation' ); - }, function( error ) + return Promise.all( [ firstSearch, secondSearch ].map( function( searchPromise ) { - assert.ok( error instanceof ripgrep.RipgrepError ); - assert.equal( error.cancelled, true ); - } ); - } ).then( function() - { - firstProcess.emit( 'close', 0, null ); - return firstSearch; - } ).then( function( summary ) - { - assert.deepEqual( summary, { stats: { matches: 0 } } ); + return searchPromise.then( function() + { + assert.ok( false, 'expected concurrent search cancellation' ); + }, function( error ) + { + assert.ok( error instanceof ripgrep.RipgrepError ); + assert.equal( error.cancelled, true ); + } ); + } ) ); } ); } ); diff --git a/test/settings.compatibility.test.js b/test/settings.compatibility.test.js index 8ff2eae4..36db5dcb 100644 --- a/test/settings.compatibility.test.js +++ b/test/settings.compatibility.test.js @@ -190,8 +190,8 @@ QUnit.test( 'manifest exposes current and legacy settings namespaces together', return key.indexOf( 'todo-tree.' ) === 0; } ); - assert.equal( currentSettings.length, 73 ); - assert.equal( legacySettings.length, 73 ); + assert.equal( currentSettings.length, 72 ); + assert.equal( legacySettings.length, 72 ); assert.ok( currentSettings.indexOf( 'general.tags' ) !== -1 ); assert.ok( currentSettings.indexOf( 'general.showScanningProgress' ) !== -1 ); assert.ok( currentSettings.indexOf( 'languages.customPatterns' ) !== -1 ); diff --git a/test/settingsSnapshot.behavior.test.js b/test/settingsSnapshot.behavior.test.js new file mode 100644 index 00000000..54b3dbc6 --- /dev/null +++ b/test/settingsSnapshot.behavior.test.js @@ -0,0 +1,60 @@ +var settingsSnapshot = require( '../src/runtime/settingsSnapshot.js' ); + +function createIdentity() +{ + var calls = []; + + return { + calls: calls, + getSetting: function( setting, defaultValue ) + { + calls.push( setting ); + return defaultValue; + } + }; +} + +function createConfig( useColourScheme ) +{ + return { + tags: function() { return [ 'TODO' ]; }, + scanMode: function() { return 'workspace'; }, + shouldUseColourScheme: function() { return useColourScheme; } + }; +} + +function createVscode() +{ + return { + workspace: { + getConfiguration: function() + { + return { compactFolders: false }; + } + } + }; +} + +QUnit.module( 'settings snapshot behavior', function() +{ + QUnit.test( 'issue #110 snapshot reuses the authoritative colour-scheme policy', function( assert ) + { + var identity = createIdentity(); + var disabled = settingsSnapshot.buildSettingsSnapshot( + undefined, + identity, + createConfig( false ), + createVscode() + ); + var enabled = settingsSnapshot.buildSettingsSnapshot( + undefined, + identity, + createConfig( true ), + createVscode() + ); + + assert.strictEqual( disabled.useColourScheme, false ); + assert.strictEqual( enabled.useColourScheme, true ); + assert.strictEqual( identity.calls.indexOf( 'highlights.useColourScheme' ), -1 ); + } ); +} ); diff --git a/test/tree.behavior.test.js b/test/tree.behavior.test.js index b4f67ac5..61d0d915 100644 --- a/test/tree.behavior.test.js +++ b/test/tree.behavior.test.js @@ -26,6 +26,8 @@ function createVscodeStub() { this.event = function() {}; this.fire = function( value ) { eventFires.push( value ); }; + this.disposeCalls = 0; + this.dispose = function() { this.disposeCalls++; }; } function TreeItem( label ) @@ -220,6 +222,11 @@ QUnit.module( "behavioral tree", function() assert.equal( provider.getChildren( todoNode ).length, 0 ); assert.equal( todoNode.label, 'TODO first line' ); assert.equal( treeItem.tooltip, 'first line\nsecond line' ); + assert.strictEqual( provider.getElement( '/tmp/a.js' ), fileNode ); + assert.strictEqual( provider.getElement( '/tmp/missing.js' ), undefined ); + provider.dispose(); + provider.dispose(); + assert.equal( provider._onDidChangeTreeData.disposeCalls, 1 ); } ); QUnit.test( "issue #888 renders the multiline banner match as a single tree label", function( assert ) diff --git a/test/tree.matrix.test.js b/test/tree.matrix.test.js index 17564b4d..16c886cc 100644 --- a/test/tree.matrix.test.js +++ b/test/tree.matrix.test.js @@ -11,6 +11,7 @@ function createVscodeStub() { this.event = function() {}; this.fire = function() {}; + this.dispose = function() {}; } function TreeItem( label ) diff --git a/test/workflows.github.test.js b/test/workflows.github.test.js index 6f172bbe..5656049b 100644 --- a/test/workflows.github.test.js +++ b/test/workflows.github.test.js @@ -439,6 +439,7 @@ QUnit.test( 'latest workflow publishes a moving prerelease from master', functio QUnit.test( 'release workflows build and publish from the resolved release ref', function( assert ) { var releaseWorkflow = readWorkflow( 'release.yml' ); + var latestWorkflow = readWorkflow( 'latest.yml' ); var reusableBuildWorkflow = readWorkflow( 'reusable-build-vsix.yml' ); var publishVsCodeScript = fs.readFileSync( path.join( __dirname, '..', 'scripts', 'release', 'publish-vscode-marketplace.sh' ), 'utf8' ); var publishOpenVsxScript = fs.readFileSync( path.join( __dirname, '..', 'scripts', 'release', 'publish-open-vsx.sh' ), 'utf8' ); @@ -450,6 +451,12 @@ QUnit.test( 'release workflows build and publish from the resolved release ref', assert.ok( releaseWorkflow.indexOf( 'release_ref: ${{ steps.meta.outputs.release_ref }}' ) !== -1 ); assert.ok( releaseWorkflow.indexOf( 'ref: ${{ needs.preflight.outputs.release_ref }}' ) !== -1 ); assert.ok( releaseWorkflow.indexOf( 'run: bash scripts/release/resolve-release-metadata.sh' ) !== -1 ); + [ releaseWorkflow, latestWorkflow ].forEach( function( workflow ) + { + assert.ok( workflow.indexOf( "JSON.stringify(require('./scripts/release/targets.json'))" ) !== -1 ); + assert.ok( workflow.indexOf( 'targets: ${{ needs.preflight.outputs.targets }}' ) !== -1 ); + assert.equal( workflow.indexOf( '"web"' ), -1 ); + } ); assert.ok( releaseWorkflow.indexOf( 'verify-marketplace:' ) !== -1 ); assert.ok( releaseWorkflow.indexOf( 'publish:' ) !== -1 ); assert.ok( releaseWorkflow.indexOf( 'run: bash scripts/release/publish-vscode-marketplace.sh' ) !== -1 ); From 42527b2637f6207b0f81375c35d7bf535d50fd79 Mon Sep 17 00:00:00 2001 From: Nathan Trudeau Date: Sun, 13 Sep 2026 11:09:52 -0400 Subject: [PATCH 2/4] fix: preserve Todo Tree settings and harden editor behavior Settings compatibility: - centralize grouped and historical flat Todo Tree setting mappings - apply VS Code user, workspace, folder, and language precedence; resolve same-scope ties as current, grouped Todo Tree, then flat keys - merge object settings without mutating source values and preserve explicit false, zero, empty arrays, and empty objects - generate 108 Todo Tree settings entries from current schemas and retained historical metadata; check alias consistency before packaging - map historical scan-mode, toolbar-button, and scheme settings; retain Todo Tree view-placement and output-buffer setting metadata - remove obsolete current-namespace tree preferences and Todo Tree deprecation notices; share schemas and localized setting descriptions - accept capture-group highlight types, theme colors, and CSS color strings in default and custom highlight schemas - resolve toolbar buttons individually and keep add/remove-tag writes at the configured scope Settings import and public identity: - replace activation-time settings rewrites and version-gated migration prompts with direct reads and explicit Import Todo Tree Settings - copy recognized settings into unset destinations at their original scopes, including exact single and combined language override blocks - preserve source, unrelated, unknown, and retained historical settings; leave existing destinations intact and make repeated import idempotent - serialize import requests, observe destination propagation, and report source changes, cancellation, and write failures with typed errors - remove Todo Tree command/context aliases and the todotree-export provider while retaining the existing view/container identifiers - replace the old compatibility suite with identity and migration tests covering mapped keys, scope precedence, empty values, object merging, retries, delayed propagation, cancellation, and added folders Runtime and scan handling: - scope scan state, timers, document queues, and Git polling guards to each activation; reset transient tree overrides during initialization - derive open-file membership from editor tabs, including hidden and diff tabs; prune closed-tab queues and refresh open-file results - refresh tag-group lookup before snapshots and scans, rescan existing editors after regex edits, and rebuild snapshots on folder changes - restore optional glob-scoped workspace file watching for external creation, edits, and deletion; invalidate affected document caches and dispose replaced or disabled watchers - wait for all cache-reset operations, aggregate failures, clear icon, highlight, document, and submodule caches, and rebuild rendered state - cover reactivation callbacks, regex/group edit ordering, hidden tabs, final-tab closure, cache-deletion failures, and watcher ownership Detection and highlight data: - reuse resource snapshots and scan contexts across default, Markdown, embedded-language, and custom-regex detection - centralize regex-source construction, cache capture metadata by regex source, and cache comment-trim patterns per scan context - collect comment matches with flatMap instead of repeated growing-array copies and find newline offsets with indexOf - use Map, Set, null-prototype records, and own-property checks for tag groups, highlight/subtag ranges, counts, exports, and glob handling - cover UTF-16/newline boundaries, 8,000 separated comment blocks, snapshot-controlled regions and regexes, trim-pattern caching, and prototype-named tags through grouping, highlights, and counts Codicons and generated assets: - replace gutter letter badges with 639 bundled SVG glyphs resolved through 934 Codicon and product-icon aliases - replace the old name-only tables and downloader with an asset compiler that checks archive integrity, version, SVGs, and alias targets - include source provenance, content/code licenses, and generation docs - derive icon filenames from SVG SHA-256 digests, share canonical assets, escape Codicon color substitutions, and reject unknown names - cover every bundled alias, distinct RGB values, inherited-property rejection, and regeneration after icon storage is removed Tree export: - disambiguate same-line matches with columns and notebook source IDs while preserving prototype-named paths and multiline todo text - replace treeify with an iterative text formatter that handles prototype-free records, shared subtrees, and multiline branches, and rejects invalid containers and cycles with typed errors - add JSON and text export regressions for reserved names, duplicate positions, notebook cells, invalid nodes, and shared references Native Methodical QA: - add isolated VSIX installation and native workbench execution through Xvfb, CDP, and X11 with owned profiles, shared data, logs, and cleanup - define a 4-theme, 9-mode, 8-profile highlight matrix for each settings namespace, totaling 576 cases, plus a focused Codicon overlay - cover palette opt-in/out, explicit backgrounds, opacity, font styles, theme colors, disabled highlighting, subtags, and Codicon gutters - compare independent expected ranges, computed styles, overlay/gutter counts, geometry within one CSS pixel, and fixture color contrast - verify requested Codicon SVG digests and native image decoding; retain screenshots, DOM, ruler canvases, settings, source fixtures, editor identity, and the installed VSIX digest - add customer probes for contributed commands, filters, navigation, scan/view controls, diagnostics, exports, cache reset, and notebook edits, same-line cells, and closed-editor cleanup - add live grouped/flat settings, schema negative controls, import, precedence, watcher, JSONC, combined-language, and added-folder flows - add X11 focus/keyboard helpers, OCR readiness, capture acknowledgments, and notification/render readiness checks - generate indexed screenshot and ruler galleries with source hashes; constrain replacement evidence to successful matching flows and VSIXs - add customer/settings/migration fixtures, a pinned upstream settings inventory, and native QA execution and evidence documentation Build, benchmark, and evidence maintenance: - remove regexp-match-indices, its regexp-tree dependency, and treeify from package metadata and the lockfile - simplify webpack to the JavaScript build and remove unused TypeScript loader rules and registry patterns - track retired regex consumers separately from missing coverage and fail the equivalence CLI on hardcoded regexes, uncovered sources, or behavior differences - isolate historical bundles and regex-index dependencies in VM contexts; resolve upstream dependencies from their installation and preserve host-native RegExp behavior - use the selected baseline identity and separate current/baseline settings fixtures in performance scenarios; model editor tab groups - isolate smoke-run editor data and remove ELECTRON_RUN_AS_NODE from editor launches - align manifest, language-matrix, parity, regex, packaging, and workflow tests with the generated settings and asset contracts - remove obsolete Codicon build exclusions and trailing ignore whitespace Documentation and localization: - document settings precedence, source-preserving import, retained historical settings, current command IDs, and export-scheme changes - correct subtag inheritance, customHighlight naming, gutter support, prototype-tag configuration, highlight wording, and regex argument documentation - rename the import command in English and Simplified Chinese and remove unused duplicate namespace and deprecation strings --- .gitignore | 1 - .npmignore | 1 - .vscodeignore | 1 - MIGRATION.md | 93 +- README.md | 19 +- buildCodiconNames.js | 48 - package-lock.json | 46 +- package.json | 2155 +++++++++++------ package.nls.json | 161 +- package.nls.zh-cn.json | 161 +- resources/codicons/LICENSE | 399 +++ resources/codicons/LICENSE-CODE | 21 + resources/codicons/README.md | 14 + resources/codicons/provenance.json | 11 + scripts/build-codicon-assets.py | 97 + .../evidence/regex-registry-equivalence.js | 12 + .../regexRegistryEquivalenceHarness.js | 24 +- scripts/evidence/retired-regexes.json | 23 + scripts/perf/extensionScenarios.js | 22 +- scripts/perf/run-all.js | 84 +- scripts/qa/README.md | 126 + scripts/qa/codicons.yaml | 10 + scripts/qa/customer-probe/extension.js | 622 +++++ scripts/qa/customer-probe/migration.js | 97 + scripts/qa/customer-probe/package.json | 13 + scripts/qa/customer-probe/send-keys.py | 57 + scripts/qa/customer-probe/settings.js | 117 + .../customer-workspace/.vscode/settings.json | 38 + .../qa/customer-workspace/nested/example.py | 1 + scripts/qa/customer-workspace/source.js | 5 + scripts/qa/customer.yaml | 44 + scripts/qa/focus-window.py | 49 + scripts/qa/highlights.yaml | 45 + scripts/qa/migration-profile.json | 10 + .../first/.vscode/settings.json | 5 + .../qa/migration-workspace/first/source.js | 1 + .../migration.code-workspace | 9 + .../second/.vscode/settings.json | 4 + .../qa/migration-workspace/second/source.js | 1 + scripts/qa/migration.yaml | 47 + scripts/qa/native-smoke.yaml | 27 + scripts/qa/observe-highlights.js | 37 + scripts/qa/plugins/highlight_contract.py | 208 ++ scripts/qa/plugins/native_vscode.py | 597 +++++ scripts/qa/prepare-ocr.py | 26 + scripts/qa/render-gallery.py | 134 + .../settings-workspace/.vscode/settings.json | 15 + scripts/qa/settings-workspace/source.js | 4 + scripts/qa/settings.yaml | 45 + scripts/qa/todo-tree-settings-inventory.json | 76 + .../repro/issue101-102-55-62-vscode-smoke.js | 34 +- scripts/sync-settings-aliases.js | 55 + src/codiconAliases.json | 936 +++++++ src/codiconGlyphs.json | 641 +++++ src/codiconNames.js | 708 ------ src/config.js | 17 +- src/detection.js | 90 +- src/extension.js | 553 ++--- src/extensionIdentity.js | 170 +- src/highlights.js | 4 +- src/icons.js | 69 +- src/productIconNames.js | 214 -- src/regexRegistry.js | 10 - src/runtime/openEditorUris.js | 26 + src/runtime/settingsMigration.js | 171 ++ src/runtime/workspaceFileWatcher.js | 34 + src/settingsCompatibility.js | 68 + src/tree.js | 44 +- src/treeExport.js | 66 + src/utils.js | 32 +- test/config.behavior.test.js | 28 + test/detection.behavior.test.js | 101 + test/extension.scan-parity.test.js | 236 +- test/highlights.behavior.test.js | 15 + test/highlights.matrix.test.js | 7 + test/icons.behavior.test.js | 100 +- test/languageMatrix.js | 4 +- test/openEditorUris.test.js | 15 + test/package.manifest.test.js | 56 +- test/packaging.ignore.test.js | 1 - test/parity/parity.test.js | 7 + test/parity/upstreamGitLoader.js | 16 +- test/regexRegistry.equivalence.test.js | 17 + test/regexRegistry.test.js | 2 - test/settings.compatibility.test.js | 203 -- test/settings.identity.test.js | 361 +++ test/settingsMigration.test.js | 247 ++ test/tree.behavior.test.js | 86 + test/treeExport.test.js | 47 + test/workflows.github.test.js | 2 +- test/workspaceFileWatcher.test.js | 60 + webpack.config.js | 25 +- 92 files changed, 8404 insertions(+), 3037 deletions(-) delete mode 100755 buildCodiconNames.js create mode 100644 resources/codicons/LICENSE create mode 100644 resources/codicons/LICENSE-CODE create mode 100644 resources/codicons/README.md create mode 100644 resources/codicons/provenance.json create mode 100644 scripts/build-codicon-assets.py create mode 100644 scripts/evidence/retired-regexes.json create mode 100644 scripts/qa/README.md create mode 100644 scripts/qa/codicons.yaml create mode 100644 scripts/qa/customer-probe/extension.js create mode 100644 scripts/qa/customer-probe/migration.js create mode 100644 scripts/qa/customer-probe/package.json create mode 100644 scripts/qa/customer-probe/send-keys.py create mode 100644 scripts/qa/customer-probe/settings.js create mode 100644 scripts/qa/customer-workspace/.vscode/settings.json create mode 100644 scripts/qa/customer-workspace/nested/example.py create mode 100644 scripts/qa/customer-workspace/source.js create mode 100644 scripts/qa/customer.yaml create mode 100644 scripts/qa/focus-window.py create mode 100644 scripts/qa/highlights.yaml create mode 100644 scripts/qa/migration-profile.json create mode 100644 scripts/qa/migration-workspace/first/.vscode/settings.json create mode 100644 scripts/qa/migration-workspace/first/source.js create mode 100644 scripts/qa/migration-workspace/migration.code-workspace create mode 100644 scripts/qa/migration-workspace/second/.vscode/settings.json create mode 100644 scripts/qa/migration-workspace/second/source.js create mode 100644 scripts/qa/migration.yaml create mode 100644 scripts/qa/native-smoke.yaml create mode 100644 scripts/qa/observe-highlights.js create mode 100644 scripts/qa/plugins/highlight_contract.py create mode 100644 scripts/qa/plugins/native_vscode.py create mode 100644 scripts/qa/prepare-ocr.py create mode 100644 scripts/qa/render-gallery.py create mode 100644 scripts/qa/settings-workspace/.vscode/settings.json create mode 100644 scripts/qa/settings-workspace/source.js create mode 100644 scripts/qa/settings.yaml create mode 100644 scripts/qa/todo-tree-settings-inventory.json create mode 100644 scripts/sync-settings-aliases.js create mode 100644 src/codiconAliases.json create mode 100644 src/codiconGlyphs.json delete mode 100644 src/codiconNames.js delete mode 100644 src/productIconNames.js create mode 100644 src/runtime/openEditorUris.js create mode 100644 src/runtime/settingsMigration.js create mode 100644 src/runtime/workspaceFileWatcher.js create mode 100644 src/settingsCompatibility.js create mode 100644 src/treeExport.js create mode 100644 test/openEditorUris.test.js delete mode 100644 test/settings.compatibility.test.js create mode 100644 test/settings.identity.test.js create mode 100644 test/settingsMigration.test.js create mode 100644 test/treeExport.test.js create mode 100644 test/workspaceFileWatcher.test.js diff --git a/.gitignore b/.gitignore index 79f5818f..4f98bbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,3 @@ artifacts/perf/runtime-benchmarks.json artifacts/**/*.json Better.TODO.Tree.settings.txt .forge/ - diff --git a/.npmignore b/.npmignore index 5b9c1bcb..cc3a5f28 100644 --- a/.npmignore +++ b/.npmignore @@ -23,7 +23,6 @@ MIGRATION.md OPEN_VSX_CERTIFICATE_REPORT.md CHANGELOG.upstream.md Better.TODO.Tree.settings.txt -buildCodiconNames.js old-*.js *.bak *~ diff --git a/.vscodeignore b/.vscodeignore index e4c79d70..705bea9a 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -22,7 +22,6 @@ MIGRATION.md OPEN_VSX_CERTIFICATE_REPORT.md CHANGELOG.upstream.md Better.TODO.Tree.settings.txt -buildCodiconNames.js old-*.js *.bak *~ diff --git a/MIGRATION.md b/MIGRATION.md index 79c3667d..6db08be7 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,56 +3,55 @@ ## Extension Identity - New public extension ID: `FanaticPythoner.better-todo-tree` -- Legacy public extension ID: `Gruntfuggly.todo-tree` -- Stable hidden view IDs kept for layout compatibility: +- Source extension ID: `Gruntfuggly.todo-tree` +- View IDs: - `todo-tree-container` - `todo-tree-view` -## Settings Compatibility - -Better Todo Tree keeps both configuration namespaces in the manifest during the `0.x` line: - -- current namespace: `better-todo-tree.*` -- legacy namespace: `todo-tree.*` - -Runtime reads resolve in this order: - -1. explicit `better-todo-tree.*` value -2. explicit `todo-tree.*` value -3. current default - -All writes go to `better-todo-tree.*`. - -On activation, the extension copies legacy `todo-tree.*` values into the matching `better-todo-tree.*` key at the same scope when the new key is unset. Supported scopes: - -- Global -- Workspace -- Workspace Folder - -Manual re-import command: - -- `Better Todo Tree: Import Legacy Settings` - -## Command Compatibility - -All legacy `todo-tree.*` command IDs remain registered as hidden aliases to the rebranded command handlers. Existing keybindings, macros, and scripts keep working while public menus and the command palette expose the `better-todo-tree.*` IDs. - -## Export Compatibility - -Both export schemes are registered: - -- current: `better-todo-tree-export:` -- legacy: `todotree-export:` - -## Non-Portable State - -Extension storage keyed by the old extension identifier is not migrated. The compatibility guarantee covers: - -- settings -- public command IDs -- stable hidden view/container IDs - -It does not cover old extension-local cache state or global/workspace mementos stored under the previous publisher/name. +## Todo Tree settings compatibility + +Todo Tree settings are supported directly. Activation reads configuration without rewriting settings files. + +| Contract | Behavior | +| --- | --- | +| Recognized settings | `better-todo-tree.*`, grouped `todo-tree.*`, historical flat Todo Tree keys | +| Live edits | Changes under either namespace refresh the affected scanners, highlights and tree controls | +| Scope | User, workspace, workspace folder and language overrides | +| Precedence | VS Code scope order; at the same scope: explicit current setting, grouped Todo Tree setting, historical flat key | +| Empty values | Explicit `false`, `0`, empty arrays and empty objects remain configured values | +| Object inheritance | Properties merge across scopes using VS Code object-setting semantics | +| Palette | `highlights.useColourScheme` defaults to `false` under both namespaces | +| Import command | `Better Todo Tree: Import Todo Tree Settings` copies recognized source values into unset destinations at the original scope | +| Source preservation | Import preserves every source key, unknown setting, retained historical setting and unrelated setting | +| Language import | Exact single-language and combined-language selector blocks remain intact | +| Failed write | Typed error; source retained; destination propagation verified before completion | +| Retry | Existing destination values remain intact; repeated import performs no writes | +| New workspace folder | Todo Tree settings work immediately; manual import includes the added folder | + +Explicit import creates `better-todo-tree.*` overrides. Subsequent Todo Tree edits follow the same +scope and namespace precedence rules. + +| Historical setting | Current behavior | +| --- | --- | +| `todo-tree.tree.showTagsFromOpenFilesOnly` | Maps `true` to `open files`, `false` to `workspace` | +| `todo-tree.tree.showScanOpenFilesOrWorkspaceButton` | Maps to `tree.buttons.scanMode` | +| `todo-tree.highlights.schemes` | Maps to `general.schemes` | +| `todo-tree.general.enableFileWatcher` | Enables external-file refresh in workspace scan modes | +| `todo-tree.general.fileWatcherGlob` | Selects file events that trigger the optional watcher | +| `todo-tree.ripgrep.ripgrepMaxBuffer`, `todo-tree.ripgrepMaxBuffer` | Preserved; streaming output has no aggregate output buffer | +| `todo-tree.tree.showInExplorer` | Preserved; VS Code owns view placement through Move View | + +Extension storage owned by `Gruntfuggly.todo-tree` is separate from settings and is not read or copied. +Setting aliases are generated from current schemas by `npm run settings:sync` and checked during packaging. + +## Commands and exports + +- Command IDs: `better-todo-tree.*`. +- Export scheme: `better-todo-tree-export:`. +- Removed command aliases: `todo-tree.*`. +- Removed export alias: `todotree-export:`. +- Existing keybindings and macros must reference the current command IDs. +- View identifiers remain `todo-tree-container` and `todo-tree-view`. ## GitHub Release Environment diff --git a/README.md b/README.md index dfc3ea89..9884d3d1 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Highlighting tags is configurable. Use `defaultHighlight` to set up highlights f Custom highlights can also be specified for sub tags (if used). -*Note: `defaultHighlight` is not applied to sub tags.* +Subtags require a matching `customHighlight` entry. Omitted attributes inherit `defaultHighlight`. Both `defaultHighlight` and `customHighlight` allow for the following settings: @@ -114,7 +114,7 @@ Both `defaultHighlight` and `customHighlight` allow for the following settings: `gutterIcon` - set to true to show the icon in the editor gutter. -*Note: Unfortunately, only octicons and the better-todo-tree icon can be displayed in the gutter.* +Octicons, codicons, and the Better Todo Tree icon can be displayed in the gutter. Gutter codicons use bundled default SVG shapes. Product icon themes control the tree icons. `rulerColour` - used to set the colour of the marker in the overview ruler. If not specified, it will default to use the foreground colour. Colour can be specified as per foreground and background colours. @@ -129,12 +129,12 @@ Both `defaultHighlight` and `customHighlight` allow for the following settings: - `tag` - highlights just the tag - `text` - highlights the tag and any text after the tag - `tag-and-comment` - highlights the comment characters (or the start of the match) and the tag -- `tag-and-subTag` - as above, but allows the sub tag to be highlight too (with colours defined in custom highlights) +- `tag-and-subTag` - highlights the tag and a subtag with a matching custom highlight - `text-and-comment` - highlights the comment characters (or the start of the match), the tag and the text after the tag - `line` - highlights the entire line containing the tag - `whole-line` - highlights the entire line containing the tag to the full width of the editor - `capture-groups:n,m...` - highlights capture groups from the regex, where 'n' is the index into the regex -- `none` - disable highlightling in the document +- `none` - disables highlighting in the document `hideFromTree` - used to hide tags from the tree, but still highlight in files @@ -301,6 +301,9 @@ This treats any of `FIXME`, `FIXIT` or `FIX` as `FIXME`. When the tree is groupe *Note: all tags in the group should also appear in `better-todo-tree.general.tags`.* +VS Code omits the exact `__proto__` object key from its configuration API. +Assign that tag to a named group such as `PrototypeMarker`, then configure the group in `customHighlight`. + **better-todo-tree.general.revealBehaviour** (`start of todo`)
Change the cursor behaviour when double-clicking a todo in the tree. You can choose from: `start of todo` (moves the cursor to the beginning of the todo), `end of todo` (moves the cursor to the end of the todo) or `start of line` (moves the cursor to the start of the line). @@ -338,7 +341,7 @@ A list of workspace names to include as roots in the tree (wildcards can be used A list of workspace names to exclude as roots in the tree (wildcards can be used). **better-todo-tree.filtering.passGlobsToRipgrep** (`true`)
-Set this to false to apply the globs *after* the search (legacy behaviour). +Set this to false to apply the globs *after* the search. **better-todo-tree.filtering.useBuiltInExcludes** (`none`)
Set this to use VSCode's built in files or search excludes. Can be one of `none`, `file excludes` (uses Files:Exclude), `search excludes` (Uses Search:Exclude) or `file and search excludes` (uses both). @@ -404,7 +407,7 @@ This defines the regex used to locate TODOs. By default, it searches for tags in *Regexes use ripgrep's Rust engine unless look-around, backreferences, or explicit ripgrep engine arguments select PCRE2.* **better-todo-tree.regex.subTagRegex** -This is a regular expression for processing the text to the right of the tag, e.g. for extracting a sub tag, or removing unwanted characters. Anything that the regex matches will be removed from the text. If a capture group is included, the contents are extracted into a sub tag, which will be used in the tree to group similar tags. The sub tag can also be used as a placeholder in `better-todo-tree.tree.subTagClickUrl` and `better-todo-tree.tree.labelFormat`. Sub tags can also be highlighted by specifying a section in the `better-todo-tree.highlights.customHighlights` setting. To highlight the sub tag itself, set "type" to "tag-and-subTag" in custom highlights for the tag. +Extract a subtag or remove text following a tag with this regular expression. Matched text is removed from the displayed label. A capture group supplies the subtag for tree grouping and the `better-todo-tree.tree.subTagClickUrl` and `better-todo-tree.tree.labelFormat` placeholders. A matching `better-todo-tree.highlights.customHighlight` entry and the `tag-and-subTag` type enable subtag decoration. Examples: @@ -424,7 +427,7 @@ Pass additional arguments to ripgrep. Engine arguments such as `--pcre2`, `-P`, Ripgrep output is parsed incrementally. No output-buffer size setting is required. **better-todo-tree.ripgrep.usePatternFile** (`true`)
-A pattern file is used with ripgrep by default. If you experience issues with deleting the pattern file, set this to false to use the legacy method of providing the regex to ripgrep. +A pattern file is used with ripgrep by default. Setting this to false passes the regex as a command argument and avoids pattern-file creation and deletion. **better-todo-tree.tree.hideTreeWhenEmpty** (`true`)
Normally, the tree is removed from the explorer view if nothing is found. Set this to false to keep the view present. @@ -648,7 +651,7 @@ When there is no current workspace, default icons will be shown in the tree. ## Compatibility -Better Todo Tree keeps reading legacy `todo-tree.*` settings and imports them into the new `better-todo-tree.*` namespace, so existing user configuration keeps working after the rebrand. +Existing Todo Tree configurations work directly, including `todo-tree.*` settings and historical flat keys. Changes take effect while the extension is running. **Better Todo Tree: Import Todo Tree Settings** copies settings into `better-todo-tree.*` at their original scopes and preserves the source configuration. Scope precedence follows VS Code; explicit `better-todo-tree.*` values win ties at the same scope. See [settings compatibility and import](MIGRATION.md#todo-tree-settings-compatibility). ### Credits diff --git a/buildCodiconNames.js b/buildCodiconNames.js deleted file mode 100755 index 130e7266..00000000 --- a/buildCodiconNames.js +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env node - -var fs = require( 'fs' ); -var path = require( 'path' ); -var cp = require( "child_process" ); - -var codiconMappingUrl = "https://raw.githubusercontent.com/microsoft/vscode-codicons/main/src/template/mapping.json"; -var outputPath = path.join( __dirname, "src/codiconNames.js" ); -var raw = cp.execFileSync( "curl", [ "-fsSL", codiconMappingUrl ], { encoding: "utf8" } ); -var mappings = JSON.parse( raw ); - -if( !mappings || Array.isArray( mappings ) || typeof mappings !== "object" ) -{ - throw new Error( "codicon mapping: object required" ); -} - -var names = []; - -Object.keys( mappings ).forEach( function( codepoint ) -{ - var aliases = mappings[ codepoint ]; - - if( Array.isArray( aliases ) !== true ) - { - throw new Error( "codicon mapping " + codepoint + ": alias array required" ); - } - - aliases.forEach( function( alias ) - { - if( typeof alias !== "string" || alias.length === 0 ) - { - throw new Error( "codicon mapping " + codepoint + ": non-empty alias required" ); - } - - names.push( alias ); - } ); -} ); - -names = Array.from( new Set( names ) ).sort(); - -var output = "module.exports = " + JSON.stringify( names, null, 2 ) + ";\n"; - -if( fs.existsSync( outputPath ) && fs.readFileSync( outputPath, "utf8" ) === output ) -{ - process.exit( 0 ); -} - -fs.writeFileSync( outputPath, output ); diff --git a/package-lock.json b/package-lock.json index a398635e..41728a51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,7 @@ "comment-patterns": "^0.10.1", "fast-strftime": "^1.1.1", "find": "^0.3.0", - "micromatch": "^4.0.8", - "regexp-match-indices": "^1.0.2", - "treeify": "^1.1.0" + "micromatch": "^4.0.8" }, "devDependencies": { "@vscode/ripgrep-universal": "^1.18.0", @@ -4543,22 +4541,6 @@ "node": ">= 10.13.0" } }, - "node_modules/regexp-match-indices": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", - "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", - "dependencies": { - "regexp-tree": "^0.1.11" - } - }, - "node_modules/regexp-tree": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.23.tgz", - "integrity": "sha512-+7HWfb4Bvu8Rs2eQTUIpX9I/PlQkYOuTNbRpKLJlQpSgwSkzFYh+pUj0gtvglnOZLKB6YgnIgRuJ2/IlpL48qw==", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5333,14 +5315,6 @@ "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=" }, - "node_modules/treeify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", - "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", @@ -8878,19 +8852,6 @@ "resolve": "^1.20.0" } }, - "regexp-match-indices": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", - "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", - "requires": { - "regexp-tree": "^0.1.11" - } - }, - "regexp-tree": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.23.tgz", - "integrity": "sha512-+7HWfb4Bvu8Rs2eQTUIpX9I/PlQkYOuTNbRpKLJlQpSgwSkzFYh+pUj0gtvglnOZLKB6YgnIgRuJ2/IlpL48qw==" - }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -9385,11 +9346,6 @@ "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=" }, - "treeify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", - "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==" - }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", diff --git a/package.json b/package.json index e0ab3aad..788cb61e 100644 --- a/package.json +++ b/package.json @@ -47,10 +47,12 @@ }, "scripts": { "test": "qunit", - "codicons:update": "node ./buildCodiconNames.js", - "vscode:prepublish": "webpack --mode production", + "codicons:update": "python3 ./scripts/build-codicon-assets.py", + "vscode:prepublish": "node ./scripts/sync-settings-aliases.js --check && webpack --mode production", "webpack": "webpack --mode development", - "webpack-dev": "webpack --mode development --watch" + "webpack-dev": "webpack --mode development --watch", + "settings:sync": "node ./scripts/sync-settings-aliases.js", + "settings:check": "node ./scripts/sync-settings-aliases.js --check" }, "categories": [ "Other" @@ -599,37 +601,16 @@ "markdownDescription": "%better-todo-tree.configuration.general.debug.markdownDescription%", "type": "boolean" }, - "todo-tree.general.debug": { - "default": false, - "markdownDescription": "%better-todo-tree.configuration.general.debug.markdownDescription%", - "type": "boolean", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.automaticGitRefreshInterval": { "default": 0, "markdownDescription": "%better-todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription%", "type": "integer" }, - "todo-tree.general.automaticGitRefreshInterval": { - "default": 0, - "markdownDescription": "%better-todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription%", - "type": "integer", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.periodicRefreshInterval": { "default": 0, "markdownDescription": "%better-todo-tree.configuration.general.periodicRefreshInterval.markdownDescription%", "type": "integer" }, - "todo-tree.general.periodicRefreshInterval": { - "default": 0, - "markdownDescription": "%better-todo-tree.configuration.general.periodicRefreshInterval.markdownDescription%", - "type": "integer", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.revealBehaviour": { "default": "start of todo", "enum": [ @@ -645,47 +626,16 @@ ], "type": "string" }, - "todo-tree.general.revealBehaviour": { - "default": "start of todo", - "enum": [ - "start of line", - "start of todo", - "end of todo" - ], - "markdownDescription": "%better-todo-tree.configuration.general.revealBehaviour.markdownDescription%", - "markdownEnumDescriptions": [ - "%better-todo-tree.configuration.general.revealBehaviour.markdownEnumDescriptions.1%", - "%better-todo-tree.configuration.general.revealBehaviour.markdownEnumDescriptions.2%", - "%better-todo-tree.configuration.general.revealBehaviour.markdownEnumDescriptions.3%" - ], - "type": "string", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.exportPath": { "default": "~/better-todo-tree-%Y%m%d-%H%M.txt", "markdownDescription": "%better-todo-tree.configuration.general.exportPath.markdownDescription%", "type": "string" }, - "todo-tree.general.exportPath": { - "default": "~/better-todo-tree-%Y%m%d-%H%M.txt", - "markdownDescription": "%better-todo-tree.configuration.general.exportPath.markdownDescription%", - "type": "string", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.rootFolder": { "default": "", "markdownDescription": "%better-todo-tree.configuration.general.rootFolder.markdownDescription%", "type": "string" }, - "todo-tree.general.rootFolder": { - "default": "", - "markdownDescription": "%better-todo-tree.configuration.general.rootFolder.markdownDescription%", - "type": "string", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.schemes": { "default": [ "file", @@ -699,21 +649,6 @@ "markdownDescription": "%better-todo-tree.configuration.general.schemes.markdownDescription%", "type": "array" }, - "todo-tree.general.schemes": { - "default": [ - "file", - "ssh", - "untitled", - "vscode-notebook-cell" - ], - "items": { - "type": "string" - }, - "markdownDescription": "%better-todo-tree.configuration.general.schemes.markdownDescription%", - "type": "array", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.showScanningProgress": { "default": "status bar", "enum": [ @@ -733,27 +668,6 @@ ], "type": "string" }, - "todo-tree.general.showScanningProgress": { - "default": "status bar", - "enum": [ - "none", - "status bar", - "notification", - "tree", - "all" - ], - "markdownDescription": "%better-todo-tree.configuration.general.showScanningProgress.markdownDescription%", - "markdownEnumDescriptions": [ - "%better-todo-tree.configuration.general.showScanningProgress.markdownEnumDescriptions.1%", - "%better-todo-tree.configuration.general.showScanningProgress.markdownEnumDescriptions.2%", - "%better-todo-tree.configuration.general.showScanningProgress.markdownEnumDescriptions.3%", - "%better-todo-tree.configuration.general.showScanningProgress.markdownEnumDescriptions.4%", - "%better-todo-tree.configuration.general.showScanningProgress.markdownEnumDescriptions.5%" - ], - "type": "string", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.statusBar": { "default": "none", "enum": [ @@ -773,39 +687,11 @@ ], "type": "string" }, - "todo-tree.general.statusBar": { - "default": "none", - "enum": [ - "none", - "total", - "tags", - "top three", - "current file" - ], - "markdownDescription": "%better-todo-tree.configuration.general.statusBar.markdownDescription%", - "markdownEnumDescriptions": [ - "%better-todo-tree.configuration.general.statusBar.markdownEnumDescriptions.1%", - "%better-todo-tree.configuration.general.statusBar.markdownEnumDescriptions.2%", - "%better-todo-tree.configuration.general.statusBar.markdownEnumDescriptions.3%", - "%better-todo-tree.configuration.general.statusBar.markdownEnumDescriptions.4%", - "%better-todo-tree.configuration.general.statusBar.markdownEnumDescriptions.5%" - ], - "type": "string", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.showIconsInsteadOfTagsInStatusBar": { "default": false, "markdownDescription": "%better-todo-tree.configuration.general.showIconsInsteadOfTagsInStatusBar.markdownDescription%", "type": "boolean" }, - "todo-tree.general.showIconsInsteadOfTagsInStatusBar": { - "default": false, - "markdownDescription": "%better-todo-tree.configuration.general.showIconsInsteadOfTagsInStatusBar.markdownDescription%", - "type": "boolean", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.statusBarClickBehaviour": { "default": "reveal", "enum": [ @@ -821,23 +707,6 @@ ], "type": "string" }, - "todo-tree.general.statusBarClickBehaviour": { - "default": "reveal", - "enum": [ - "cycle", - "reveal", - "toggle highlights" - ], - "markdownDescription": "%better-todo-tree.configuration.general.statusBarClickBehaviour.markdownDescription%", - "markdownEnumDescriptions": [ - "%better-todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.1%", - "%better-todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.2%", - "%better-todo-tree.configuration.general.statusBarClickBehaviour.markdownEnumDescriptions.3%" - ], - "type": "string", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.tagGroups": { "default": {}, "markdownDescription": "%better-todo-tree.configuration.general.tagGroups.markdownDescription%", @@ -849,19 +718,6 @@ } } }, - "todo-tree.general.tagGroups": { - "default": {}, - "markdownDescription": "%better-todo-tree.configuration.general.tagGroups.markdownDescription%", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.tags": { "default": [ "BUG", @@ -878,35 +734,21 @@ "markdownDescription": "%better-todo-tree.configuration.general.tags.markdownDescription%", "type": "array" }, - "todo-tree.general.tags": { - "default": [ - "BUG", - "HACK", - "FIXME", - "TODO", - "XXX", - "[ ]", - "[x]" - ], - "items": { - "type": "string" - }, - "markdownDescription": "%better-todo-tree.configuration.general.tags.markdownDescription%", - "type": "array", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.general.showActivityBarBadge": { "default": false, "markdownDescription": "%better-todo-tree.configuration.general.showActivityBarBadge.markdownDescription%", "type": "boolean" }, - "todo-tree.general.showActivityBarBadge": { - "default": false, - "markdownDescription": "%better-todo-tree.configuration.general.showActivityBarBadge.markdownDescription%", + "better-todo-tree.general.enableFileWatcher": { "type": "boolean", - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" + "default": false, + "markdownDescription": "Refresh workspace results after external file creation, edits and deletion." + }, + "better-todo-tree.general.fileWatcherGlob": { + "type": "string", + "default": "**/*", + "minLength": 1, + "markdownDescription": "Glob matched by the optional workspace file watcher." } } }, @@ -1025,16 +867,6 @@ } } }, - "todo-tree.languages.customPatterns": { - "default": [], - "markdownDescription": "%better-todo-tree.configuration.languages.customPatterns.markdownDescription%", - "type": "array", - "items": { - "type": "object" - }, - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" - }, "better-todo-tree.languages.embeddedDocuments": { "default": [], "markdownDescription": "%better-todo-tree.configuration.languages.embeddedDocuments.markdownDescription%", @@ -1197,16 +1029,6 @@ } } } - }, - "todo-tree.languages.embeddedDocuments": { - "default": [], - "markdownDescription": "%better-todo-tree.configuration.languages.embeddedDocuments.markdownDescription%", - "type": "array", - "items": { - "type": "object" - }, - "deprecationMessage": "%todo-tree.configuration.legacyNamespace.deprecationMessage%", - "markdownDeprecationMessage": "%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%" } } }, @@ -1243,11 +1065,25 @@ "properties": { "foreground": { "type": "string", - "format": "color-hex" + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] }, "background": { "type": "string", - "format": "color-hex" + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] }, "opacity": { "type": "number" @@ -1269,14 +1105,28 @@ }, "iconColour": { "type": "string", - "format": "color-hex" + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] }, "gutterIcon": { "type": "boolean" }, "rulerColour": { "type": "string", - "format": "color-hex" + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] }, "rulerOpacity": { "type": "number" @@ -1293,15 +1143,22 @@ }, "type": { "type": "string", - "enum": [ - "tag", - "text", - "tag-and-comment", - "tag-and-subTag", - "text-and-comment", - "line", - "whole-line", - "none" + "anyOf": [ + { + "enum": [ + "tag", + "text", + "tag-and-comment", + "tag-and-subTag", + "text-and-comment", + "line", + "whole-line", + "none" + ] + }, + { + "pattern": "^capture-groups:\\s*\\d+(?:\\s*,\\s*\\d+)*\\s*$" + } ] }, "hideFromTree": { @@ -1316,25 +1173,1147 @@ } } }, - "todo-tree.highlights.customHighlight": { - "default": { - "BUG": { - "icon": "bug" + "better-todo-tree.highlights.defaultHighlight": { + "default": {}, + "markdownDescription": "%better-todo-tree.configuration.highlights.defaultHighlight.markdownDescription%", + "type": "object", + "properties": { + "foreground": { + "type": "string", + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] }, - "HACK": { - "icon": "tools" + "background": { + "type": "string", + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] }, - "FIXME": { - "icon": "flame" + "opacity": { + "type": "number" }, - "XXX": { - "icon": "x" + "fontWeight": { + "type": "string" }, - "[ ]": { - "icon": "issue-draft" + "fontStyle": { + "type": "string" }, - "[x]": { - "icon": "issue-closed" + "textDecoration": { + "type": "string" + }, + "borderRadius": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "iconColour": { + "type": "string", + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] + }, + "gutterIcon": { + "type": "boolean" + }, + "rulerColour": { + "type": "string", + "anyOf": [ + { + "format": "color-hex" + }, + { + "pattern": "^[^#].*$" + } + ] + }, + "rulerOpacity": { + "type": "number" + }, + "rulerLane": { + "type": "string", + "enum": [ + "none", + "left", + "center", + "right", + "full" + ] + }, + "type": { + "type": "string", + "anyOf": [ + { + "enum": [ + "tag", + "text", + "tag-and-comment", + "tag-and-subTag", + "text-and-comment", + "line", + "whole-line", + "none" + ] + }, + { + "pattern": "^capture-groups:\\s*\\d+(?:\\s*,\\s*\\d+)*\\s*$" + } + ] + }, + "hideFromTree": { + "type": "boolean" + }, + "hideFromStatusBar": { + "type": "boolean" + }, + "hideFromActivityBar": { + "type": "boolean" + } + } + }, + "better-todo-tree.highlights.enabled": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.highlights.enabled.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.highlights.highlightDelay": { + "default": 500, + "markdownDescription": "%better-todo-tree.configuration.highlights.highlightDelay.markdownDescription%", + "type": "integer" + }, + "better-todo-tree.highlights.useColourScheme": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.highlights.useColourScheme.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.highlights.foregroundColourScheme": { + "default": [ + "white", + "black", + "black", + "white", + "white", + "white", + "black" + ], + "items": { + "type": "string" + }, + "markdownDescription": "%better-todo-tree.configuration.highlights.foregroundColourScheme.markdownDescription%", + "type": "array" + }, + "better-todo-tree.highlights.backgroundColourScheme": { + "default": [ + "red", + "orange", + "yellow", + "green", + "blue", + "indigo", + "violet" + ], + "items": { + "type": "string" + }, + "markdownDescription": "%better-todo-tree.configuration.highlights.backgroundColourScheme.markdownDescription%", + "type": "array" + } + } + }, + { + "title": "%better-todo-tree.configuration.filtering%", + "order": 4, + "type": "object", + "properties": { + "better-todo-tree.filtering.excludedWorkspaces": { + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "%better-todo-tree.configuration.filtering.excludedWorkspaces.markdownDescription%", + "type": "array" + }, + "better-todo-tree.filtering.excludeGlobs": { + "default": [ + "**/node_modules/*/**" + ], + "items": { + "type": "string" + }, + "markdownDescription": "%better-todo-tree.configuration.filtering.excludeGlobs.markdownDescription%", + "type": "array" + }, + "better-todo-tree.filtering.ignoreGitSubmodules": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.filtering.ignoreGitSubmodules.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.filtering.includedWorkspaces": { + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "%better-todo-tree.configuration.filtering.includedWorkspaces.markdownDescription%", + "type": "array" + }, + "better-todo-tree.filtering.includeGlobs": { + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "%better-todo-tree.configuration.filtering.includeGlobs.markdownDescription%", + "type": "array" + }, + "better-todo-tree.filtering.includeHiddenFiles": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.filtering.includeHiddenFiles.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.filtering.passGlobsToRipgrep": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.filtering.passGlobsToRipgrep.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.filtering.scopes": { + "default": [], + "markdownDescription": "%better-todo-tree.configuration.filtering.scopes.markdownDescription%", + "type": "array" + }, + "better-todo-tree.filtering.useBuiltInExcludes": { + "default": "none", + "enum": [ + "none", + "file excludes", + "search excludes", + "file and search excludes" + ], + "markdownDescription": "%better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownDescription%", + "markdownEnumDescriptions": [ + "%better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownEnumDescriptions.1%", + "%better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownEnumDescriptions.2%", + "%better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownEnumDescriptions.3%", + "%better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownEnumDescriptions.4%" + ], + "type": "string" + } + } + }, + { + "title": "%better-todo-tree.configuration.tree%", + "order": 5, + "type": "object", + "properties": { + "better-todo-tree.tree.autoRefresh": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.autoRefresh.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.disableCompactFolders": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.disableCompactFolders.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.expanded": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.expanded.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.filterCaseSensitive": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.filterCaseSensitive.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.flat": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.flat.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.groupedByTag": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.groupedByTag.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.groupedBySubTag": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.groupedBySubTag.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.hideIconsWhenGroupedByTag": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.hideIconsWhenGroupedByTag.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.hideTreeWhenEmpty": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.hideTreeWhenEmpty.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.labelFormat": { + "default": "${tag} ${after}", + "markdownDescription": "%better-todo-tree.configuration.tree.labelFormat.markdownDescription%", + "type": "string" + }, + "better-todo-tree.tree.scanAtStartup": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.scanAtStartup.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.scanMode": { + "default": "workspace", + "enum": [ + "workspace", + "open files", + "current file", + "workspace only" + ], + "markdownDescription": "%better-todo-tree.configuration.tree.scanMode.markdownDescription%", + "markdownEnumDescriptions": [ + "%better-todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.1%", + "%better-todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.2%", + "%better-todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.3%", + "%better-todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.4%" + ], + "type": "string" + }, + "better-todo-tree.tree.showBadges": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.showBadges.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.showCountsInTree": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.showCountsInTree.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.showCurrentScanMode": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.showCurrentScanMode.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.subTagClickUrl": { + "default": "", + "markdownDescription": "%better-todo-tree.configuration.tree.subTagClickUrl.markdownDescription%", + "type": "string" + }, + "better-todo-tree.tree.sortTagsOnlyViewAlphabetically": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.sortTagsOnlyViewAlphabetically.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.sort": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.sort.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.tagsOnly": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.tagsOnly.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.tooltipFormat": { + "default": "${filepath}, line ${line}", + "markdownDescription": "%better-todo-tree.configuration.tree.tooltipFormat.markdownDescription%", + "type": "string" + }, + "better-todo-tree.tree.trackFile": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.trackFile.markdownDescription%", + "type": "boolean" + } + } + }, + { + "title": "%better-todo-tree.configuration.buttons%", + "order": 6, + "type": "object", + "properties": { + "better-todo-tree.tree.buttons.reveal": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.reveal.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.scanMode": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.scanMode.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.viewStyle": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.viewStyle.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.groupByTag": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.groupByTag.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.groupBySubTag": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.groupBySubTag.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.filter": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.filter.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.refresh": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.refresh.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.expand": { + "default": true, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.expand.markdownDescription%", + "type": "boolean" + }, + "better-todo-tree.tree.buttons.export": { + "default": false, + "markdownDescription": "%better-todo-tree.configuration.tree.buttons.export.markdownDescription%", + "type": "boolean" + } + } + }, + { + "title": "%better-todo-tree.configuration.regex%", + "order": 7, + "type": "object", + "properties": { + "better-todo-tree.regex.regex": { + "default": "(^|//|#|\n' ] + ].forEach( function( fixture ) + { + QUnit.test( 'resource snapshot controls every default scan region: ' + fixture[ 0 ], function( assert ) + { + var uri = createUri( '/tmp/resource-snapshot.' + fixture[ 0 ] ); + var resourceConfig = Object.assign( {}, detection.resolveResourceConfig( uri ), { tags: [ 'REVIEW' ] } ); + var reads = 0; + var context = detection.createScanContext( uri, fixture[ 1 ], { getResourceConfig: function() + { + reads++; + return resourceConfig; + } } ); + var offsets = context.lineOffsets; + var results = detection.scanTextWithContext( context ); + assert.equal( results.length, fixture[ 0 ] === 'js' ? 1 : 2 ); + assert.true( results.every( function( result ) { return result.actualTag === 'REVIEW'; } ) ); + assert.equal( context.text, fixture[ 1 ] ); + assert.strictEqual( context.lineOffsets, offsets ); + assert.equal( reads, 1 ); + } ); + } ); + + QUnit.test( 'resource snapshot supplies custom regex compilation', function( assert ) + { + var uri = createUri( '/tmp/custom-snapshot.js' ); + var resourceConfig = Object.assign( {}, detection.resolveResourceConfig( uri ), { + tags: [ 'REVIEW' ], regex: regexRegistry.captureSource( 'REVIEW' ), isDefaultRegex: false + } ); + var context = detection.createScanContext( uri, 'REVIEW snapshot\n// TODO unrelated', { + getResourceConfig: function() { return resourceConfig; } + } ); + var results = detection.scanTextWithContext( context ); + assert.equal( results.length, 1 ); + assert.equal( results[ 0 ].actualTag, 'REVIEW' ); + assert.equal( results[ 0 ].line, 1 ); + } ); + + [ 'js', 'tmpl' ].forEach( function( extension ) + { + QUnit.test( 'regex scan resolves the comment trim pattern once per context: ' + extension, function( assert ) + { + utils.init( createConfig( { regexSource: regexRegistry.TAG_CAPTURE_PLACEHOLDER } ) ); + var uri = createUri( '/tmp/trim-pattern.' + extension ); + var text = '// TODO first\n// TODO second\n// FIXME third'; + var context = detection.createScanContext( uri, text ); + var original = utils.resolveBlockCommentPattern; + var calls = 0; + utils.resolveBlockCommentPattern = function() + { + calls += 1; + return original.apply( utils, arguments ); + }; + try + { + var first = detection.scanTextWithContext( context ); + assert.equal( first.length, 3 ); + assert.equal( calls, 1 ); + assert.deepEqual( detection.scanTextWithContext( context ), first ); + assert.equal( calls, 1, 'resolved absence and resolved patterns both remain cached' ); + detection.scanTextWithContext( detection.createScanContext( uri, text ) ); + assert.equal( calls, 2, 'a new scan context resolves its own pattern' ); + } + finally + { + utils.resolveBlockCommentPattern = original; + } + } ); + } ); + QUnit.test( "default detection ignores tag-like identifiers", function( assert ) { var results = detection.scanText( createUri( "/tmp/sample.js" ), [ diff --git a/test/extension.scan-parity.test.js b/test/extension.scan-parity.test.js index 9fb57d5e..aff49d77 100644 --- a/test/extension.scan-parity.test.js +++ b/test/extension.scan-parity.test.js @@ -630,7 +630,7 @@ function createVscodeStub( options ) ThemeIcon.Folder = new ThemeIcon( 'folder' ); ThemeIcon.File = new ThemeIcon( 'file' ); - return { + var vscodeStub = { commandHandlers: commandHandlers, workspaceListeners: workspaceListeners, windowListeners: windowListeners, @@ -733,6 +733,16 @@ function createVscodeStub( options ) }, window: { visibleTextEditors: options.visibleTextEditors || [], + tabGroups: { + get all() + { + var uris = options.openTabUris || vscodeStub.workspace.textDocuments.concat( + vscodeStub.window.visibleTextEditors.map( function( editor ) { return editor.document; } ) + ).map( function( document ) { return document.uri; } ); + return [ { tabs: uris.map( function( uri ) { return { input: { uri: uri } }; } ) } ]; + }, + onDidChangeTabs: function( listener ) { return registerListener( windowListeners, 'tabs', listener ); } + }, activeTextEditor: options.activeTextEditor, activeNotebookEditor: activeNotebookEditor, visibleNotebookEditors: visibleNotebookEditors, @@ -868,7 +878,17 @@ function createVscodeStub( options ) }, notebookDocuments: options.notebookDocuments || [], onDidSaveTextDocument: function( listener ) { return registerListener( workspaceListeners, 'save', listener ); }, - onDidOpenTextDocument: function( listener ) { return registerListener( workspaceListeners, 'open', listener ); }, + onDidOpenTextDocument: function( listener ) + { + return registerListener( workspaceListeners, 'open', function( document ) + { + if( vscodeStub.workspace.textDocuments.indexOf( document ) === -1 ) + { + vscodeStub.workspace.textDocuments.push( document ); + } + return listener( document ); + } ); + }, onDidCloseTextDocument: function( listener ) { return registerListener( workspaceListeners, 'close', listener ); }, onDidOpenNotebookDocument: function( listener ) { return registerListener( workspaceListeners, 'openNotebook', listener ); }, onDidChangeNotebookDocument: function( listener ) { return registerListener( workspaceListeners, 'changeNotebook', listener ); }, @@ -887,6 +907,7 @@ function createVscodeStub( options ) } } }; + return vscodeStub; } function createExtensionHarness( options ) @@ -913,6 +934,11 @@ function createExtensionHarness( options ) }; var vscodeStub = createVscodeStub( options ); + var getHarnessConfiguration = vscodeStub.workspace.getConfiguration; + vscodeStub.workspace.getConfiguration = function( section, uri ) + { + return section === 'todo-tree' ? createConfigurationSection( {} ) : getHarnessConfiguration( section, uri ); + }; var identityStub = helpers.loadWithStubs( '../src/extensionIdentity.js', { vscode: vscodeStub } ); @@ -1267,7 +1293,11 @@ function createExtensionHarness( options ) validateIconColours: function() { return undefined; } }, './icons.js': { - validateIcons: function() { return undefined; } + validateIcons: function() { return undefined; }, + resetCaches: function() + { + if( options.resetIconCaches ) { options.resetIconCaches(); } + } }, './highlights.js': { init: function() {}, @@ -1429,14 +1459,23 @@ function createExtensionHarness( options ) } return Promise.resolve( options.fileContents[ filePath ] ); }, - readdir: function() { return Promise.resolve( [] ); }, - unlink: function() { return Promise.resolve(); } + readdir: function( folder ) + { + return options.cacheReaddir ? options.cacheReaddir( folder ) : Promise.resolve( [] ); + }, + unlink: function( file ) + { + return options.cacheUnlink ? options.cacheUnlink( file ) : Promise.resolve(); + } } }, - treeify: { asTree: function() { return ''; } }, child_process: { execFile: function( executable, args, execOptions, callback ) { + if( options.execFileImpl ) + { + return options.execFileImpl( executable, args, execOptions, callback ); + } callback( null, 'head', '' ); } } @@ -1455,6 +1494,7 @@ function createExtensionHarness( options ) return { extension: extension, + config: configStub, context: context, options: options, get provider() @@ -1642,6 +1682,51 @@ function fireVisibleNotebookEditorsChanged( harness, notebooksToShow, activeNote QUnit.module( "extension scan parity" ); +[ false, true ].forEach( function( combined ) +{ + QUnit.test( "regex configuration changes refresh existing editors" + ( combined ? " with tag groups" : "" ), async function( assert ) + { + var document = matrixHelpers.createDocument( '/workspace/regex-change.js', '// TODO first\n// FIXME second' ); + var order = []; + var changed = false; + var harness = createExtensionHarness( { + scanMode: 'open files', + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + visibleTextEditors: [ { document: document } ], + fileContents: {}, + scanDocumentImpl: function() + { + order.push( 'scan' ); + return [ { uri: document.uri, actualTag: changed ? 'FIXME' : 'TODO', + displayText: changed ? 'second' : 'first', continuationText: [] } ]; + } + } ); + harness.config.refreshTagGroupLookup = function() { order.push( 'groups' ); }; + harness.extension.activate( harness.context ); + await matrixHelpers.flushAsyncWork(); + var previousHighlights = harness.highlightCalls.length; + order.length = 0; + changed = true; + var settings = [ 'better-todo-tree', 'better-todo-tree.regex', 'better-todo-tree.regex.regex' ]; + if( combined ) + { + settings.push( 'better-todo-tree.general.tagGroups', 'better-todo-tree.highlights.customHighlight' ); + } + harness.vscode.workspaceListeners.configuration( { + affectsConfiguration: function( name ) { return settings.indexOf( name ) !== -1; } + } ); + await matrixHelpers.flushAsyncWork(); + assert.ok( order.indexOf( 'scan' ) >= 0, 'existing document rescanned' ); + assert.ok( harness.highlightCalls.length > previousHighlights, 'existing editor redecorated' ); + assert.equal( harness.provider.replaceCalls.slice( -1 )[ 0 ].results[ 0 ].actualTag, 'FIXME' ); + if( combined ) + { + assert.equal( order[ 0 ], 'groups', 'group lookup refresh precedes rescanning' ); + } + harness.extension.deactivate(); + } ); +} ); + QUnit.test( "issue #41 total status bar uses status counts and activity badge uses activity counts", function( assert ) { var harness = createExtensionHarness( { @@ -1762,6 +1847,54 @@ QUnit.test( "open-files mode stores canonical document results through the refre } ); } ); +QUnit.test( "open-files scan excludes cached documents without tabs and retains hidden tabs", function( assert ) +{ + var hidden = matrixHelpers.createDocument( '/tmp/hidden-tab.js', '// TODO hidden tab' ); + var cached = matrixHelpers.createDocument( '/tmp/closed-tab.js', '// TODO closed tab' ); + var harness = createExtensionHarness( { + scanMode: 'open files', + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + textDocuments: [ hidden, cached ], + openTabUris: [ hidden.uri ], + visibleTextEditors: [], + documentResults: [], + fileContents: {} + } ); + harness.extension.activate( harness.context ); + return matrixHelpers.flushAsyncWork().then( function() + { + assert.deepEqual( harness.scanDocumentCalls.map( function( document ) { return document.fileName; } ), + [ '/tmp/hidden-tab.js' ] ); + } ); +} ); + +QUnit.test( "closing the final tab clears open-files results before document disposal", function( assert ) +{ + var document = matrixHelpers.createDocument( '/tmp/last-tab.js', '// TODO last tab' ); + var tabs = [ document.uri ]; + var harness = createExtensionHarness( { + scanMode: 'open files', + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + textDocuments: [ document ], + openTabUris: tabs, + visibleTextEditors: [], + documentResults: [ { uri: document.uri, actualTag: 'TODO', displayText: 'last tab', continuationText: [] } ], + fileContents: {} + } ); + harness.extension.activate( harness.context ); + return matrixHelpers.flushAsyncWork().then( function() + { + assert.equal( harness.provider.latestResultsByUri.size, 1 ); + tabs.length = 0; + harness.windowListeners.tabs( { opened: [], changed: [], closed: [ { input: { uri: document.uri } } ] } ); + return waitForDelay( 0 ).then( matrixHelpers.flushAsyncWork ); + } ).then( function() + { + assert.equal( harness.provider.latestResultsByUri.size, 0 ); + assert.equal( harness.scanDocumentCalls.length, 1 ); + } ); +} ); + QUnit.test( "open-files startup scan includes visible editors restored after activation", function( assert ) { var fixture = [ { @@ -4965,6 +5098,49 @@ QUnit.test( "registered command boundary uses manifest titles for operation name } ); } ); +[ false, true ].forEach( function( failDeletion ) +{ + QUnit.test( 'cache reset waits for deletions before invalidation, failure=' + failDeletion, async function( assert ) + { + var delayed = createDeferred(); + var resets = 0; + var completed = false; + var failure = new Error( 'icon deletion denied' ); + var harness = createExtensionHarness( { + scanMode: 'open files', scanAtStartup: false, fileContents: {}, + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + resetIconCaches: function() { resets += 1; }, + cacheReaddir: function( folder ) + { + return Promise.resolve( folder === '/tmp/global-storage' ? [ 'first.svg', 'last.svg' ] : [] ); + }, + cacheUnlink: function( file ) + { + if( file.endsWith( 'last.svg' ) ) { return delayed.promise; } + return failDeletion ? Promise.reject( failure ) : Promise.resolve(); + } + } ); + await harness.extension.activate( harness.context ); + await matrixHelpers.flushAsyncWork(); + var operation = harness.vscode.commandHandlers[ 'better-todo-tree.resetCache' ]().then( function() + { + assert.notOk( failDeletion, 'successful reset resolves' ); + }, function( error ) + { + assert.ok( failDeletion, 'failed deletion rejects' ); + assert.strictEqual( error.errors[ 0 ].errors[ 0 ], failure ); + assert.strictEqual( error.reportedToUser, true ); + } ).then( function() { completed = true; } ); + await matrixHelpers.flushAsyncWork(); + assert.strictEqual( resets, 0, 'invalidation waits for every deletion' ); + assert.strictEqual( completed, false, 'command waits for every deletion' ); + delayed.resolve(); + await operation; + assert.strictEqual( resets, 1, 'completed deletions invalidate paths on success and failure' ); + harness.extension.deactivate(); + } ); +} ); + QUnit.test( "registered command boundary reports setting write failures once", function( assert ) { var harness = createExtensionHarness( { @@ -5276,6 +5452,42 @@ QUnit.test( "deactivation cancels scheduled work and disposes activation-owned r } ); } ); +QUnit.test( "Git callbacks from a disposed activation preserve the new activation guard", function( assert ) +{ + var timers = createControlledTimers(); + var callbacks = []; + var harness = createExtensionHarness( { + scanMode: 'open files', scanAtStartup: false, + automaticGitRefreshInterval: 1, timerStubs: timers, + workspaceFolders: [ { uri: matrixHelpers.createUri( '/workspace' ), name: 'workspace' } ], + resourceConfig: { isDefaultRegex: true, enableMultiLine: false, regexCaseSensitive: true }, + fileContents: {}, + execFileImpl: function( executable, args, options, callback ) { callbacks.push( callback ); } + } ); + + harness.extension.activate( harness.context ); + return matrixHelpers.flushAsyncWork().then( function() + { + timers.intervals[ 0 ].callback(); + harness.extension.deactivate(); + harness.extension.activate( harness.context ); + return matrixHelpers.flushAsyncWork(); + } ).then( function() + { + var newInterval = timers.intervals[ timers.intervals.length - 1 ]; + newInterval.callback(); + assert.equal( callbacks.length, 2 ); + callbacks[ 0 ]( null, 'old-head', '' ); + newInterval.callback(); + assert.equal( callbacks.length, 2, 'the new Git poll remains in flight' ); + callbacks[ 1 ]( null, 'new-head', '' ); + newInterval.callback(); + assert.equal( callbacks.length, 3, 'the completed new poll permits another request' ); + callbacks[ 2 ]( null, 'new-head', '' ); + harness.extension.deactivate(); + } ); +} ); + QUnit.test( "deactivation settles pending tree UI work without touching the disposed view", function( assert ) { var harness = createExtensionHarness( { @@ -5296,7 +5508,7 @@ QUnit.test( "deactivation settles pending tree UI work without touching the disp } ); } ); -QUnit.test( "markdown migration writes the shared default regex source", function( assert ) +QUnit.test( "explicit historical markdown regex remains unchanged without migration prompts", function( assert ) { var harness = createExtensionHarness( { scanMode: 'open files', @@ -5318,14 +5530,10 @@ QUnit.test( "markdown migration writes the shared default regex source", functio return matrixHelpers.flushAsyncWork(); } ).then( function() { - assert.deepEqual( harness.vscode.informationMessages, [ - 'Better Todo Tree: There is now an improved method of locating markdown TODOs. Apply settings automatically?' - ] ); - assert.ok( harness.vscode.configurationUpdates.some( function( update ) + assert.deepEqual( harness.vscode.informationMessages, [] ); + assert.notOk( harness.vscode.configurationUpdates.some( function( update ) { - return update.key === 'regex.regex' && - update.value === actualUtils.DEFAULT_REGEX_SOURCE && - update.target === harness.vscode.ConfigurationTarget.Global; + return update.key === 'regex.regex'; } ) ); } ); } ); diff --git a/test/highlights.behavior.test.js b/test/highlights.behavior.test.js index 2052aa1e..fc50dd1a 100644 --- a/test/highlights.behavior.test.js +++ b/test/highlights.behavior.test.js @@ -242,6 +242,21 @@ QUnit.module( "behavioral highlights", function( hooks ) { var originalVscode; + QUnit.test( "prototype-named custom tags retain every highlight range", function( assert ) + { + var harness = createActualDetectionHighlightHarness( { + tags: [ 'constructor', '__proto__', 'toString' ], + type: 'tag', + text: '// constructor first\n// __proto__ second\n// toString third' + } ); + + assert.equal( harness.recorded.length, 3 ); + assert.deepEqual( harness.recorded.map( function( ranges ) + { + return ranges.map( function( item ) { return item.range.start.line; } ); + } ), [ [ 0 ], [ 1 ], [ 2 ] ] ); + } ); + hooks.beforeEach( function() { originalVscode = require.cache[ require.resolve( './moduleHelpers.js' ) ]; diff --git a/test/highlights.matrix.test.js b/test/highlights.matrix.test.js index a435b919..ecc590a1 100644 --- a/test/highlights.matrix.test.js +++ b/test/highlights.matrix.test.js @@ -125,6 +125,13 @@ function createHarness( options ) return { dark: '/tmp/gutter.svg', light: '/tmp/gutter.svg' }; } }, + './extensionIdentity.js': { + getSetting: function( key, defaultValue ) + { + var values = { 'highlights.enabled': true, 'highlights.highlightDelay': 0 }; + return Object.prototype.hasOwnProperty.call( values, key ) ? values[ key ] : defaultValue; + } + }, './detection.js': { scanDocument: function() { diff --git a/test/icons.behavior.test.js b/test/icons.behavior.test.js index 568b7943..5cd937bd 100644 --- a/test/icons.behavior.test.js +++ b/test/icons.behavior.test.js @@ -156,12 +156,108 @@ QUnit.module( 'behavioral icons', function( hooks ) var treeIcon = icons.getTreeIcon( context, 'TODO:' ); var gutterIcon = icons.getGutterIcon( context, 'TODO:' ); - assert.equal( path.basename( treeIcon.dark ), 'todo-bug-ff4545.svg' ); - assert.equal( path.basename( gutterIcon.dark ), 'todo-bug-ff4545.svg' ); + assert.equal( treeIcon.dark, gutterIcon.dark ); + assert.ok( fs.readFileSync( treeIcon.dark, 'utf8' ).includes( '#ff4545' ) ); assert.ok( fs.existsSync( treeIcon.dark ) ); assert.ok( fs.existsSync( gutterIcon.dark ) ); } ); + QUnit.test( 'cache reset recreates removed icon storage and generated assets', function( assert ) + { + var storagePath = fs.mkdtempSync( path.join( os.tmpdir(), 'better-todo-tree-icons-' ) ); + tempDirectories.push( storagePath ); + var config = createAttributeConfig( { + customHighlight: function() + { + return { TODO: { icon: 'flame', iconColour: '#e85dba' } }; + } + } ); + actualUtils.init( config ); + actualAttributes.init( config ); + var icons = helpers.loadWithStubs( '../src/icons.js', { vscode: createVscodeStub() } ); + var context = createContext( storagePath ); + var initial = icons.getGutterIcon( context, 'TODO' ); + var svg = fs.readFileSync( initial.dark, 'utf8' ); + fs.rmSync( storagePath, { recursive: true } ); + icons.resetCaches(); + var regenerated = icons.getGutterIcon( context, 'TODO' ); + assert.deepEqual( regenerated, initial ); + assert.equal( fs.readFileSync( regenerated.dark, 'utf8' ), svg ); + assert.deepEqual( icons.getTreeIcon( context, 'TODO' ), regenerated ); + } ); + + QUnit.test( 'every codicon and product alias resolves an authoritative vector glyph', function( assert ) + { + var aliases = require( '../src/codiconAliases.json' ); + var storagePath = fs.mkdtempSync( path.join( os.tmpdir(), 'better-todo-tree-glyphs-' ) ); + tempDirectories.push( storagePath ); + var custom = Object.fromEntries( Object.keys( aliases ).map( function( name ) + { + return [ name, { icon: '$(' + name + ')', iconColour: '#ff4545' } ]; + } ) ); + var config = createAttributeConfig( { customHighlight: function() { return custom; } } ); + actualUtils.init( config ); + actualAttributes.init( config ); + var icons = helpers.loadWithStubs( '../src/icons.js', { vscode: createVscodeStub() } ); + var context = createContext( storagePath ); + var paths = new Map(); + Object.keys( aliases ).forEach( function( name ) + { + var icon = icons.getGutterIcon( context, name ); + var svg = fs.readFileSync( icon.dark, 'utf8' ); + assert.ok( svg.includes( '= 0 ); - assert.ok( english[ 'todo-tree.configuration.tree.showBadges.markdownDescription' ].indexOf( 'file icon theme resources' ) >= 0 ); assert.ok( chinese[ 'better-todo-tree.configuration.tree.showBadges.markdownDescription' ].indexOf( '文件图标主题资源' ) >= 0 ); - assert.ok( chinese[ 'todo-tree.configuration.tree.showBadges.markdownDescription' ].indexOf( '文件图标主题资源' ) >= 0 ); } ); diff --git a/test/packaging.ignore.test.js b/test/packaging.ignore.test.js index af025f1a..6c060855 100644 --- a/test/packaging.ignore.test.js +++ b/test/packaging.ignore.test.js @@ -44,7 +44,6 @@ QUnit.test( '.npmignore excludes local workflow tooling and keeps dist package r '.devcontainer/', '.vscodeignore', 'webpack.config.js', - 'buildCodiconNames.js', 'Better.TODO.Tree.settings.txt' ].forEach( function( entry ) { diff --git a/test/parity/parity.test.js b/test/parity/parity.test.js index 871b893a..7d1ddb71 100644 --- a/test/parity/parity.test.js +++ b/test/parity/parity.test.js @@ -406,6 +406,8 @@ QUnit.module( 'parity todo-tree vs better-todo-tree', function( hooks ) { var fs = require( 'fs' ); var upstreamGitLoader = require( './upstreamGitLoader.js' ); + var nativeExec = RegExp.prototype.exec; + var nativeIndices = Object.getOwnPropertyDescriptor( RegExp.prototype, 'hasIndices' ); var bundlePath = upstreamGitLoader.ensureUpstreamBuild(); assert.ok( fs.existsSync( bundlePath ), 'compiled bundle exists at ' + bundlePath ); @@ -458,5 +460,10 @@ QUnit.module( 'parity todo-tree vs better-todo-tree', function( hooks ) assert.equal( typeof( bundleExports.activate ), 'function', 'compiled bundle exports activate()' ); assert.equal( typeof( bundleExports.deactivate ), 'function', 'compiled bundle exports deactivate()' ); + assert.strictEqual( RegExp.prototype.exec, nativeExec, 'host RegExp.exec remains unchanged' ); + assert.deepEqual( Object.getOwnPropertyDescriptor( RegExp.prototype, 'hasIndices' ), nativeIndices ); + var source = regexRegistry.captureLiteralSource( 'a' ); + assert.deepEqual( new RegExp( source, 'd' ).exec( 'a' ).indices[ 1 ], [ 0, 1 ] ); + assert.strictEqual( new RegExp( source ).exec( 'a' ).indices, undefined ); } ); } ); diff --git a/test/parity/upstreamGitLoader.js b/test/parity/upstreamGitLoader.js index cfc47d84..aa2678dc 100644 --- a/test/parity/upstreamGitLoader.js +++ b/test/parity/upstreamGitLoader.js @@ -273,7 +273,7 @@ function loadModule( relativePath, stubs ) return loadModule( path.posix.join( path.posix.dirname( normalized ), request ) ); } - return require( request ); + return upstreamModule.require( request ); } var compiled = compileWrapper( source, syntheticPath ); @@ -310,10 +310,20 @@ function loadCompiledBundle( stubs ) { return stubs[ request ]; } - return require( request ); + return bundleModule.require( request ); } - var compiled = vm.runInThisContext( Module.wrap( source ), { filename: bundlePath } ); + var compiled = vm.runInNewContext( Module.wrap( source ), { + Buffer: Buffer, + console: console, + process: process, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval, + setImmediate: setImmediate, + clearImmediate: clearImmediate + }, { filename: bundlePath } ); compiled.call( bundleModule.exports, bundleModule.exports, bundleRequire, bundleModule, bundlePath, path.dirname( bundlePath ) ); if( hasStubs !== true ) diff --git a/test/regexRegistry.equivalence.test.js b/test/regexRegistry.equivalence.test.js index c16a39b9..15ed70e7 100644 --- a/test/regexRegistry.equivalence.test.js +++ b/test/regexRegistry.equivalence.test.js @@ -119,7 +119,24 @@ QUnit.test( 'registry refactor preserves extracted baseline regex behavior', fun assert.equal( audit.currentHardcodedRegexEntries.length, 0 ); assert.equal( audit.sourceCoverage.missing.length, 0 ); + assert.equal( audit.sourceCoverage.covered + audit.sourceCoverage.retired.length, + audit.sourceCoverage.total ); assert.equal( audit.behaviorParity.failures.length, 0 ); assert.ok( audit.metrics.baselineRegexEntries > 0 ); assert.equal( audit.metrics.behaviorParityPassed, audit.metrics.behaviorParityTotal ); } ); + +QUnit.test( 'retirement cannot conceal a missing runtime regex', function( assert ) +{ + var harness = require( '../scripts/evidence/regexRegistryEquivalenceHarness.js' ); + var retired = require( '../scripts/evidence/retired-regexes.json' )[ 0 ]; + var result = harness.compareSourceCoverage( [ { + source: retired.source, + flags: retired.flags, + count: 1, + refs: [ 'src/extension.js:1' ] + } ] ); + + assert.equal( result.missing.length, 1 ); + assert.equal( result.retired.length, 0 ); +} ); diff --git a/test/regexRegistry.test.js b/test/regexRegistry.test.js index d4365a72..944626e5 100644 --- a/test/regexRegistry.test.js +++ b/test/regexRegistry.test.js @@ -10,10 +10,8 @@ QUnit.module( 'regex registry' ); QUnit.test( 'manifest and runtime default regex sources share one registry output', function( assert ) { var currentRegex = languageMatrix.findConfigurationProperty( 'better-todo-tree.regex.regex' ); - var legacyRegex = languageMatrix.findConfigurationProperty( 'todo-tree.regex.regex' ); assert.equal( regexRegistry.DEFAULT_REGEX_SOURCE, currentRegex.default ); - assert.equal( regexRegistry.DEFAULT_REGEX_SOURCE, legacyRegex.default ); assert.equal( utils.DEFAULT_REGEX_SOURCE, regexRegistry.DEFAULT_REGEX_SOURCE ); } ); diff --git a/test/settings.compatibility.test.js b/test/settings.compatibility.test.js deleted file mode 100644 index 36db5dcb..00000000 --- a/test/settings.compatibility.test.js +++ /dev/null @@ -1,203 +0,0 @@ -var helpers = require( './moduleHelpers.js' ); -var packageJson = require( '../package.json' ); - -function createConfigurationSection( values, defaults ) -{ - function getNestedValue( source, key ) - { - return key.split( '.' ).reduce( function( current, part ) - { - return current && current[ part ] !== undefined ? current[ part ] : undefined; - }, source ); - } - - return { - get: function( key, defaultValue ) - { - var explicitValue = getNestedValue( values, key ); - if( explicitValue !== undefined ) - { - return explicitValue; - } - - var defaultSetting = getNestedValue( defaults, key ); - return defaultSetting === undefined ? defaultValue : defaultSetting; - }, - inspect: function( key ) - { - var explicitValue = getNestedValue( values, key ); - var defaultSetting = getNestedValue( defaults, key ); - - return { - defaultValue: defaultSetting, - globalValue: explicitValue, - workspaceValue: undefined, - workspaceFolderValue: undefined - }; - }, - update: function() - { - return Promise.resolve(); - } - }; -} - -function createIdentity( currentValues, legacyValues, defaults ) -{ - return helpers.loadWithStubs( '../src/extensionIdentity.js', { - vscode: { - ConfigurationTarget: { - Global: 1, - Workspace: 2, - WorkspaceFolder: 3 - }, - workspace: { - getConfiguration: function( section ) - { - if( section === 'better-todo-tree' ) - { - return createConfigurationSection( currentValues, defaults ); - } - - if( section === 'todo-tree' ) - { - return createConfigurationSection( legacyValues, defaults ); - } - - return createConfigurationSection( {}, {} ); - } - } - } - } ); -} - -QUnit.module( 'settings compatibility' ); - -QUnit.test( 'current namespace values override legacy namespace values', function( assert ) -{ - var identity = createIdentity( - { general: { tags: [ 'BETTER' ] } }, - { general: { tags: [ 'LEGACY' ] } }, - { general: { tags: [ 'DEFAULT' ] } } - ); - - assert.deepEqual( identity.getSetting( 'general.tags', [] ), [ 'BETTER' ] ); -} ); - -QUnit.test( 'legacy namespace values remain active when current namespace is unset', function( assert ) -{ - var identity = createIdentity( - {}, - { general: { tags: [ 'LEGACY' ] } }, - { general: { tags: [ 'DEFAULT' ] } } - ); - - assert.deepEqual( identity.getSetting( 'general.tags', [] ), [ 'LEGACY' ] ); -} ); - -QUnit.test( 'current namespace highlight settings override legacy highlight settings', function( assert ) -{ - var identity = createIdentity( - { - highlights: { - customHighlight: { - TODO: { - foreground: '#ffffff' - } - }, - useColourScheme: true, - backgroundColourScheme: [ '#d61' ] - } - }, - { - highlights: { - customHighlight: { - TODO: { - foreground: '#000000' - } - }, - useColourScheme: false, - backgroundColourScheme: [ '#000000' ] - } - }, - { - highlights: { - customHighlight: {}, - useColourScheme: false, - backgroundColourScheme: [] - } - } - ); - - assert.deepEqual( identity.getSetting( 'highlights.customHighlight', {} ), { - TODO: { - foreground: '#ffffff' - } - } ); - assert.strictEqual( identity.getSetting( 'highlights.useColourScheme', false ), true ); - assert.deepEqual( identity.getSetting( 'highlights.backgroundColourScheme', [] ), [ '#d61' ] ); -} ); - -QUnit.test( 'legacy highlight settings remain active when current namespace is unset', function( assert ) -{ - var identity = createIdentity( - {}, - { - highlights: { - defaultHighlight: { - gutterIcon: true, - type: 'text' - }, - customHighlight: { - FIXME: { - foreground: '#8F1BDC' - } - }, - enabled: true - } - }, - { - highlights: { - defaultHighlight: {}, - customHighlight: {}, - enabled: false - } - } - ); - - assert.deepEqual( identity.getSetting( 'highlights.defaultHighlight', {} ), { - gutterIcon: true, - type: 'text' - } ); - assert.deepEqual( identity.getSetting( 'highlights.customHighlight', {} ), { - FIXME: { - foreground: '#8F1BDC' - } - } ); - assert.strictEqual( identity.getSetting( 'highlights.enabled', false ), true ); -} ); - -QUnit.test( 'manifest exposes current and legacy settings namespaces together', function( assert ) -{ - var identity = createIdentity( {}, {}, {} ); - var currentSettings = identity.getManifestSettingSuffixes( packageJson ); - var allProperties = packageJson.contributes.configuration.reduce( function( properties, group ) - { - return properties.concat( Object.keys( group.properties || {} ) ); - }, [] ); - var legacySettings = allProperties.filter( function( key ) - { - return key.indexOf( 'todo-tree.' ) === 0; - } ); - - assert.equal( currentSettings.length, 72 ); - assert.equal( legacySettings.length, 72 ); - assert.ok( currentSettings.indexOf( 'general.tags' ) !== -1 ); - assert.ok( currentSettings.indexOf( 'general.showScanningProgress' ) !== -1 ); - assert.ok( currentSettings.indexOf( 'languages.customPatterns' ) !== -1 ); - assert.ok( currentSettings.indexOf( 'languages.embeddedDocuments' ) !== -1 ); - assert.ok( legacySettings.indexOf( 'todo-tree.general.tags' ) !== -1 ); - assert.ok( legacySettings.indexOf( 'todo-tree.general.showScanningProgress' ) !== -1 ); - assert.ok( legacySettings.indexOf( 'todo-tree.languages.customPatterns' ) !== -1 ); - assert.ok( legacySettings.indexOf( 'todo-tree.languages.embeddedDocuments' ) !== -1 ); -} ); diff --git a/test/settings.identity.test.js b/test/settings.identity.test.js new file mode 100644 index 00000000..59201e39 --- /dev/null +++ b/test/settings.identity.test.js @@ -0,0 +1,361 @@ +var helpers = require( './moduleHelpers.js' ); +var packageJson = require( '../package.json' ); + +function createConfigurationSection( values, defaults ) +{ + function getNestedValue( source, key ) + { + return key.split( '.' ).reduce( function( current, part ) + { + return current && current[ part ] !== undefined ? current[ part ] : undefined; + }, source ); + } + + return { + get: function( key, defaultValue ) + { + var explicitValue = getNestedValue( values, key ); + if( explicitValue !== undefined ) + { + return explicitValue; + } + + var defaultSetting = getNestedValue( defaults, key ); + return defaultSetting === undefined ? defaultValue : defaultSetting; + }, + inspect: function( key ) + { + var explicitValue = getNestedValue( values, key ); + var defaultSetting = getNestedValue( defaults, key ); + + return { + defaultValue: defaultSetting, + globalValue: explicitValue, + workspaceValue: undefined, + workspaceFolderValue: undefined + }; + }, + update: function() + { + return Promise.resolve(); + } + }; +} + +function createIdentity( currentValues, legacyValues, defaults ) +{ + return helpers.loadWithStubs( '../src/extensionIdentity.js', { + vscode: { + ConfigurationTarget: { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3 + }, + workspace: { + getConfiguration: function( section ) + { + if( section === 'better-todo-tree' ) + { + return createConfigurationSection( currentValues, defaults ); + } + + if( section === 'todo-tree' ) + { + return createConfigurationSection( legacyValues, defaults ); + } + + return createConfigurationSection( {}, {} ); + } + } + } + } ); +} + +QUnit.module( 'settings identity' ); + +var compatibility = require( '../src/settingsCompatibility.js' ); +compatibility.getSchemas( packageJson ).forEach( function( schema, setting ) +{ + compatibility.getSources( setting ).forEach( function( source ) + { + QUnit.test( 'read explicit Todo Tree setting: ' + source, function( assert ) + { + var value = source === 'tree.showTagsFromOpenFilesOnly' ? true : schema.default; + var input = {}; + input[ 'better-todo-tree.' + setting ] = { defaultValue: 'unconfigured-sentinel' }; + input[ 'todo-tree.' + source ] = { globalValue: value }; + var expected = source === 'tree.showTagsFromOpenFilesOnly' ? 'open files' : value; + assert.deepEqual( createScopedIdentity( input ).identity.getSetting( setting ), expected ); + } ); + } ); +} ); + +QUnit.test( 'every pinned upstream setting remains recognized by the manifest', function( assert ) +{ + var inventory = require( '../scripts/qa/todo-tree-settings-inventory.json' ); + var declared = new Set( packageJson.contributes.configuration.flatMap( function( group ) + { + return Object.keys( group.properties ); + } ) ); + inventory.settings.forEach( function( key ) { assert.true( declared.has( key ), key ); } ); +} ); + +QUnit.test( 'language defaults apply after non-language settings and before explicit language values', function( assert ) +{ + var input = { + 'better-todo-tree.regex.regex': { defaultValue: '(DEFAULT)', defaultLanguageValue: '(LANGUAGE_DEFAULT)' }, + 'todo-tree.regex.regex': { workspaceFolderValue: '(FOLDER)' } + }; + var identity = createScopedIdentity( input ).identity; + assert.equal( identity.getSetting( 'regex.regex' ), '(LANGUAGE_DEFAULT)' ); + input[ 'todo-tree.regex.regex' ].globalLanguageValue = '(LANGUAGE_USER)'; + assert.equal( identity.getSetting( 'regex.regex' ), '(LANGUAGE_USER)' ); +} ); + +QUnit.test( 'current namespace values override legacy namespace values', function( assert ) +{ + var identity = createIdentity( + { general: { tags: [ 'BETTER' ] } }, + { general: { tags: [ 'LEGACY' ] } }, + { general: { tags: [ 'DEFAULT' ] } } + ); + + assert.deepEqual( identity.getSetting( 'general.tags', [] ), [ 'BETTER' ] ); +} ); + +function createScopedIdentity( values ) +{ + var scopes = []; + var updates = []; + var identity = helpers.loadWithStubs( '../src/extensionIdentity.js', { + vscode: { + ConfigurationTarget: { Global: 1, Workspace: 2, WorkspaceFolder: 3 }, + workspace: { + getConfiguration: function( namespace, scope ) + { + scopes.push( scope ); + return { + inspect: function( setting ) { return values[ namespace + '.' + setting ] || {}; }, + get: function( setting, defaultValue ) + { + var inspection = values[ namespace + '.' + setting ] || {}; + return inspection.defaultValue === undefined ? defaultValue : inspection.defaultValue; + }, + update: function( setting, value, target ) + { + updates.push( { namespace, setting, value, target, scope } ); + return Promise.resolve(); + } + }; + } + } + } + } ); + return { identity, scopes, updates }; +} + +QUnit.test( 'scope precedence applies across both namespaces with current values winning ties', function( assert ) +{ + var fields = [ 'globalValue', 'workspaceValue', 'workspaceFolderValue', + 'globalLanguageValue', 'workspaceLanguageValue', 'workspaceFolderLanguageValue' ]; + fields.forEach( function( currentField, currentIndex ) + { + fields.forEach( function( sourceField, sourceIndex ) + { + var current = { defaultValue: '(DEFAULT)' }; + var source = {}; + current[ currentField ] = '(CURRENT)'; + source[ sourceField ] = '(SOURCE)'; + var harness = createScopedIdentity( { + 'better-todo-tree.regex.regex': current, 'todo-tree.regex.regex': source + } ); + var scope = { uri: 'file:/customer/source.py', languageId: 'python' }; + assert.equal( harness.identity.getSetting( 'regex.regex', undefined, scope ), + sourceIndex > currentIndex ? '(SOURCE)' : '(CURRENT)', currentField + '/' + sourceField ); + assert.equal( harness.identity.getSettingTarget( 'regex.regex', scope ), + Math.max( currentIndex, sourceIndex ) % 3 + 1 ); + assert.true( harness.scopes.every( function( value ) { return value === scope; } ) ); + } ); + } ); +} ); + +QUnit.test( 'explicit false, zero and empty collections survive setting aliases', function( assert ) +{ + [ [ 'highlights.useColourScheme', false ], [ 'highlights.highlightDelay', 0 ], + [ 'general.tags', [] ], [ 'highlights.defaultHighlight', {} ] ].forEach( function( entry ) + { + var values = {}; + values[ 'todo-tree.' + entry[ 0 ] ] = { workspaceValue: entry[ 1 ] }; + assert.deepEqual( createScopedIdentity( values ).identity.getSetting( entry[ 0 ], 'sentinel' ), entry[ 1 ] ); + } ); +} ); + +QUnit.test( 'historical flat keys support live reads and grouped keys win within one scope', function( assert ) +{ + var values = { + 'todo-tree.tags': { globalValue: [ 'GLOBAL' ], workspaceValue: [] }, + 'todo-tree.general.tags': { globalValue: [ 'GROUPED' ] }, + 'todo-tree.highlightDelay': { workspaceValue: 0 }, + 'todo-tree.regex': { globalValue: { regex: '(NESTED)' } }, + 'todo-tree.tree.showTagsFromOpenFilesOnly': { workspaceValue: true }, + 'todo-tree.tree.showScanOpenFilesOrWorkspaceButton': { workspaceValue: true } + }; + var identity = createScopedIdentity( values ).identity; + assert.deepEqual( identity.getSetting( 'general.tags' ), [] ); + delete values[ 'todo-tree.tags' ].workspaceValue; + assert.deepEqual( identity.getSetting( 'general.tags' ), [ 'GROUPED' ] ); + assert.strictEqual( identity.getSetting( 'highlights.highlightDelay' ), 0 ); + assert.strictEqual( identity.getSetting( 'regex.regex', '(DEFAULT)' ), '(DEFAULT)' ); + assert.equal( identity.getSetting( 'tree.scanMode' ), 'open files' ); + assert.true( identity.getSetting( 'tree.buttons.scanMode' ) ); + values[ 'todo-tree.tree.showTagsFromOpenFilesOnly' ].workspaceValue = false; + assert.equal( identity.getSetting( 'tree.scanMode' ), 'workspace' ); +} ); + +QUnit.test( 'object inheritance retains unrelated keys without mutating source settings', function( assert ) +{ + var values = { + 'better-todo-tree.highlights.customHighlight': { + defaultValue: { FIXME: { icon: 'flame' } }, + globalValue: { TODO: { foreground: '#ffffff', fontWeight: 'bold' } } + }, + 'todo-tree.highlights.customHighlight': { + workspaceValue: JSON.parse( '{"TODO":{"background":"#000000"},"__proto__":{"icon":"bug"},"constructor":{"icon":"beaker"}}' ) + } + }; + var before = JSON.stringify( values ); + var result = createScopedIdentity( values ).identity.getSetting( 'highlights.customHighlight' ); + assert.deepEqual( result.TODO, { foreground: '#ffffff', fontWeight: 'bold', background: '#000000' } ); + assert.equal( result.FIXME.icon, 'flame' ); + assert.equal( result.__proto__.icon, 'bug' ); + assert.equal( result.constructor.icon, 'beaker' ); + assert.equal( JSON.stringify( values ), before ); + assert.strictEqual( {}.icon, undefined ); +} ); + +QUnit.test( 'configuration changes cover grouped, flat and renamed settings without command aliases', function( assert ) +{ + var identity = createIdentity( {}, {}, {} ); + var pairs = [ [ 'todo-tree.general.tags', 'general.tags', 'general' ], + [ 'todo-tree.tags', 'general.tags', 'general' ], + [ 'todo-tree.customHighlight', 'highlights.customHighlight', 'highlights' ], + [ 'todo-tree.regex', 'regex.regex', 'regex' ], + [ 'todo-tree.tree.showTagsFromOpenFilesOnly', 'tree.scanMode', 'tree' ], + [ 'todo-tree.languages.customPatterns', 'languages.customPatterns', 'languages' ] ]; + pairs.forEach( function( entry ) + { + var event = { affectsConfiguration: function( key ) { return entry[ 0 ] === key || entry[ 0 ].startsWith( key + '.' ); } }; + assert.true( identity.affectsSetting( event, entry[ 1 ] ), entry[ 0 ] ); + assert.true( identity.affectsNamespace( event, 'better-todo-tree.' + entry[ 2 ] ), entry[ 0 ] ); + assert.true( identity.affectsNamespace( event, 'better-todo-tree' ) ); + } ); + assert.true( Object.values( identity.COMMANDS ).every( function( command ) { return command.startsWith( 'better-todo-tree.' ); } ) ); +} ); + +QUnit.test( 'Todo Tree values remain active without a current namespace value', function( assert ) +{ + var identity = createIdentity( + {}, + { general: { tags: [ 'LEGACY' ] } }, + { general: { tags: [ 'DEFAULT' ] } } + ); + + assert.deepEqual( identity.getSetting( 'general.tags', [] ), [ 'LEGACY' ] ); +} ); + +QUnit.test( 'current namespace highlight settings override legacy highlight settings', function( assert ) +{ + var identity = createIdentity( + { + highlights: { + customHighlight: { + TODO: { + foreground: '#ffffff' + } + }, + useColourScheme: true, + backgroundColourScheme: [ '#d61' ] + } + }, + { + highlights: { + customHighlight: { + TODO: { + foreground: '#000000' + } + }, + useColourScheme: false, + backgroundColourScheme: [ '#000000' ] + } + }, + { + highlights: { + customHighlight: {}, + useColourScheme: false, + backgroundColourScheme: [] + } + } + ); + + assert.deepEqual( identity.getSetting( 'highlights.customHighlight', {} ), { + TODO: { + foreground: '#ffffff' + } + } ); + assert.strictEqual( identity.getSetting( 'highlights.useColourScheme', false ), true ); + assert.deepEqual( identity.getSetting( 'highlights.backgroundColourScheme', [] ), [ '#d61' ] ); +} ); + +QUnit.test( 'Todo Tree highlights remain active without a current namespace value', function( assert ) +{ + var identity = createIdentity( + {}, + { + highlights: { + defaultHighlight: { + gutterIcon: true, + type: 'text' + }, + customHighlight: { + FIXME: { + foreground: '#8F1BDC' + } + }, + enabled: true + } + }, + { + highlights: { + defaultHighlight: {}, + customHighlight: {}, + enabled: false + } + } + ); + + assert.deepEqual( identity.getSetting( 'highlights.defaultHighlight', {} ), { gutterIcon: true, type: 'text' } ); + assert.deepEqual( identity.getSetting( 'highlights.customHighlight', {} ), { FIXME: { foreground: '#8F1BDC' } } ); + assert.strictEqual( identity.getSetting( 'highlights.enabled', false ), true ); +} ); + +QUnit.test( 'manifest recognizes both setting namespaces', function( assert ) +{ + var identity = createIdentity( {}, {}, {} ); + var currentSettings = identity.getManifestSettingSuffixes( packageJson ); + var allProperties = packageJson.contributes.configuration.reduce( function( properties, group ) + { + return properties.concat( Object.keys( group.properties || {} ) ); + }, [] ); + var legacySettings = allProperties.filter( function( key ) + { + return key.indexOf( 'todo-tree.' ) === 0; + } ); + + assert.equal( currentSettings.length, 71 ); + currentSettings.forEach( function( suffix ) { assert.true( legacySettings.includes( 'todo-tree.' + suffix ), suffix ); } ); + assert.true( legacySettings.includes( 'todo-tree.tags' ) ); + assert.ok( currentSettings.indexOf( 'general.tags' ) !== -1 ); + assert.ok( currentSettings.indexOf( 'general.showScanningProgress' ) !== -1 ); + assert.ok( currentSettings.indexOf( 'languages.customPatterns' ) !== -1 ); + assert.ok( currentSettings.indexOf( 'languages.embeddedDocuments' ) !== -1 ); +} ); diff --git a/test/settingsMigration.test.js b/test/settingsMigration.test.js new file mode 100644 index 00000000..cc728e05 --- /dev/null +++ b/test/settingsMigration.test.js @@ -0,0 +1,247 @@ +var migration = require( '../src/runtime/settingsMigration.js' ); +var packageJson = require( '../package.json' ); + +function createEnvironment() +{ + var values = new Map(); + var writes = []; + var listeners = new Set(); + var environment = { + values: values, + writes: writes, + listeners: listeners, + ConfigurationTarget: { Global: 1, Workspace: 2, WorkspaceFolder: 3 }, + workspace: { + onDidChangeConfiguration: function( listener ) + { + listeners.add( listener ); + return { dispose: function() { listeners.delete( listener ); } }; + }, + workspaceFile: 'file:/workspace/customer.code-workspace', + workspaceFolders: [ { uri: 'file:/first' }, { uri: 'file:/second' } ], + getConfiguration: function( namespace, scope ) + { + var uri = typeof scope === 'object' ? scope.uri : scope; + var languageId = typeof scope === 'object' ? scope.languageId : undefined; + var result = { + inspect: function( setting ) + { + var result = { languageIds: [ 'python', 'javascript' ] }; + [ 'globalValue', 'workspaceValue', 'workspaceFolderValue' ].forEach( function( field ) + { + var location = field === 'workspaceFolderValue' ? uri : ''; + result[ field ] = values.get( [ namespace, setting, field, location, '' ].join( '|' ) ); + result[ field.replace( 'Value', 'LanguageValue' ) ] = values.get( + [ namespace, setting, field, location, languageId ].join( '|' ) ); + } ); + return result; + }, + update: async function( setting, value, target, override ) + { + var field = [ '', 'globalValue', 'workspaceValue', 'workspaceFolderValue' ][ target ]; + var key = [ namespace, setting, field, target === 3 ? uri : '', override ? languageId : '' ].join( '|' ); + if( environment.beforeWrite ) + { + await environment.beforeWrite( { namespace: namespace, setting: setting, value: value, key: key } ); + } + function commit() + { + if( value === undefined ) + { + values.delete( key ); + } + else + { + values.set( key, value ); + } + writes.push( { key: key, value: value } ); + listeners.forEach( function( listener ) { listener(); } ); + } + if( environment.deferPropagation ) { setTimeout( commit, 5 ); } + else { commit(); } + } + }; + if( namespace === undefined ) + { + values.forEach( function( value, key ) + { + var parts = key.split( '|' ); + if( parts[ 0 ] === '' && parts[ 1 ].startsWith( '[' ) ) + { + result[ parts[ 1 ] ] = value; + } + } ); + } + return result; + } + } + }; + environment.set = function( namespace, setting, field, value, uri, languageId ) + { + if( languageId ) + { + var overrideKey = [ '', '[' + languageId + ']', field, uri || '', '' ].join( '|' ); + var valuesAtScope = values.get( overrideKey ) || {}; + valuesAtScope[ namespace + '.' + setting ] = value; + values.set( overrideKey, valuesAtScope ); + return; + } + values.set( [ namespace, setting, field, uri || '', languageId || '' ].join( '|' ), value ); + }; + return environment; +} + +QUnit.module( 'settings migration' ); + +var compatibility = require( '../src/settingsCompatibility.js' ); +compatibility.getSchemas( packageJson ).forEach( function( schema, setting ) +{ + compatibility.getSources( setting ).forEach( function( source ) + { + QUnit.test( 'import explicit Todo Tree setting: ' + source, async function( assert ) + { + var environment = createEnvironment(); + var value = source === 'tree.showTagsFromOpenFilesOnly' ? true : schema.default; + var expected = source === 'tree.showTagsFromOpenFilesOnly' ? 'open files' : value; + environment.set( 'todo-tree', source, 'workspaceValue', value ); + var operations = migration.collect( environment, packageJson ); + assert.equal( await migration.apply( environment, operations ), 1 ); + assert.deepEqual( environment.values.get( 'better-todo-tree|' + setting + '|workspaceValue||' ), expected ); + assert.deepEqual( environment.values.get( 'todo-tree|' + source + '|workspaceValue||' ), value ); + assert.equal( migration.collect( environment, packageJson ).length, 0 ); + } ); + } ); +} ); + +QUnit.test( 'import all scopes, language overrides, empty values and flat keys without source changes', async function( assert ) +{ + var environment = createEnvironment(); + environment.set( 'todo-tree', 'general.tags', 'globalValue', [ 'GLOBAL' ] ); + environment.set( 'todo-tree', 'general.tags', 'workspaceValue', [] ); + environment.set( 'todo-tree', 'general.tags', 'workspaceFolderValue', [ 'FIRST' ], 'file:/first' ); + environment.set( 'todo-tree', 'general.tags', 'workspaceFolderValue', [ 'SECOND' ], 'file:/second' ); + environment.set( 'todo-tree', 'highlights.useColourScheme', 'globalValue', false ); + environment.set( 'todo-tree', 'highlightDelay', 'globalValue', 0 ); + environment.set( 'todo-tree', 'regex.regex', 'workspaceFolderValue', 'PYTHON', 'file:/second', 'python' ); + var operations = migration.collect( environment, packageJson ); + assert.equal( operations.length, 7 ); + assert.equal( await migration.apply( environment, operations ), 7 ); + assert.equal( migration.collect( environment, packageJson ).length, 0 ); + assert.deepEqual( environment.values.get( 'better-todo-tree|general.tags|workspaceValue||' ), [] ); + assert.strictEqual( environment.values.get( 'better-todo-tree|highlights.useColourScheme|globalValue||' ), false ); + assert.strictEqual( environment.values.get( 'better-todo-tree|highlights.highlightDelay|globalValue||' ), 0 ); + assert.equal( environment.values.get( '|[python]|workspaceFolderValue|file:/second|' )[ 'better-todo-tree.regex.regex' ], 'PYTHON' ); +} ); + +QUnit.test( 'explicit destination wins and grouped source wins over flat source', async function( assert ) +{ + var environment = createEnvironment(); + environment.set( 'better-todo-tree', 'general.tags', 'globalValue', [ 'CURRENT' ] ); + environment.set( 'todo-tree', 'general.tags', 'globalValue', [ 'SOURCE' ] ); + environment.set( 'todo-tree', 'general.tags', 'workspaceValue', [ 'GROUPED' ] ); + environment.set( 'todo-tree', 'tags', 'workspaceValue', [ 'FLAT' ] ); + environment.set( 'todo-tree', 'regex', 'globalValue', { regex: 'nested property' } ); + await migration.apply( environment, migration.collect( environment, packageJson ) ); + assert.deepEqual( environment.values.get( 'better-todo-tree|general.tags|globalValue||' ), [ 'CURRENT' ] ); + assert.deepEqual( environment.values.get( 'better-todo-tree|general.tags|workspaceValue||' ), [ 'GROUPED' ] ); + assert.deepEqual( environment.values.get( 'todo-tree|regex|globalValue||' ), { regex: 'nested property' } ); +} ); + +QUnit.test( 'failed destination write retains source and permits retry', async function( assert ) +{ + var environment = createEnvironment(); + environment.set( 'todo-tree', 'general.tags', 'workspaceValue', [ 'SOURCE' ] ); + environment.beforeWrite = function() { throw new Error( 'denied' ); }; + await assert.rejects( migration.apply( environment, migration.collect( environment, packageJson ) ), + function( error ) { return error.code === 'DESTINATION_WRITE_FAILED' && error.cause.message === 'denied'; } ); + assert.equal( environment.writes.length, 0 ); + environment.beforeWrite = undefined; + assert.equal( await migration.apply( environment, migration.collect( environment, packageJson ) ), 1 ); +} ); + +QUnit.test( 'import never writes or removes source keys', async function( assert ) +{ + var environment = createEnvironment(); + environment.set( 'todo-tree', 'general.tags', 'globalValue', [ 'SOURCE' ] ); + environment.beforeWrite = function( operation ) + { + assert.notEqual( operation.namespace, 'todo-tree' ); + }; + assert.equal( await migration.apply( environment, migration.collect( environment, packageJson ) ), 1 ); + assert.deepEqual( environment.values.get( 'todo-tree|general.tags|globalValue||' ), [ 'SOURCE' ] ); + assert.equal( await migration.apply( environment, migration.collect( environment, packageJson ) ), 0 ); +} ); + +QUnit.test( 'concurrent source edits and cancellation preserve unconsumed configuration', async function( assert ) +{ + var environment = createEnvironment(); + environment.set( 'todo-tree', 'general.tags', 'globalValue', [ 'FIRST' ] ); + var operations = migration.collect( environment, packageJson ); + environment.set( 'todo-tree', 'general.tags', 'globalValue', [ 'SECOND' ] ); + await assert.rejects( migration.apply( environment, operations ), + function( error ) { return error.code === 'SOURCE_CHANGED'; } ); + await assert.rejects( migration.apply( environment, migration.collect( environment, packageJson ), function() { return true; } ), + function( error ) { return error.code === 'MIGRATION_CANCELLED'; } ); + assert.equal( environment.writes.length, 0 ); +} ); + +QUnit.test( 'a workspace added after another migration receives its own settings', async function( assert ) +{ + var environment = createEnvironment(); + environment.set( 'todo-tree', 'general.tags', 'workspaceFolderValue', [ 'FIRST' ], 'file:/first' ); + await migration.apply( environment, migration.collect( environment, packageJson ) ); + environment.workspace.workspaceFolders.push( { uri: 'file:/third' } ); + environment.set( 'todo-tree', 'general.tags', 'workspaceFolderValue', [ 'THIRD' ], 'file:/third' ); + assert.equal( await migration.apply( environment, migration.collect( environment, packageJson ) ), 1 ); + assert.deepEqual( environment.values.get( 'better-todo-tree|general.tags|workspaceFolderValue|file:/third|' ), [ 'THIRD' ] ); +} ); + +QUnit.test( 'single-folder workspace uses the shared settings file once', async function( assert ) +{ + var environment = createEnvironment(); + environment.workspace.workspaceFile = undefined; + environment.workspace.workspaceFolders.length = 1; + environment.set( 'todo-tree', 'highlights.useColourScheme', 'workspaceValue', false ); + environment.set( 'todo-tree', 'highlights.useColourScheme', 'workspaceFolderValue', false, 'file:/first' ); + var operations = migration.collect( environment, packageJson ); + assert.equal( operations.length, 1 ); + assert.equal( operations[ 0 ].field, 'workspaceValue' ); + assert.equal( await migration.apply( environment, operations ), 1 ); +} ); + +QUnit.test( 'retained and unknown settings remain untouched', async function( assert ) +{ + var environment = createEnvironment(); + environment.set( 'todo-tree', 'tree.showInExplorer', 'workspaceValue', true ); + environment.set( 'todo-tree', 'ripgrep.ripgrepMaxBuffer', 'workspaceValue', 600 ); + environment.set( 'todo-tree', 'customerExtensionSetting', 'globalValue', { x: 1 } ); + var before = Array.from( environment.values.entries() ); + assert.equal( await migration.apply( environment, migration.collect( environment, packageJson ) ), 0 ); + assert.deepEqual( Array.from( environment.values.entries() ), before ); +} ); + +QUnit.test( 'combined language blocks preserve their identity and unrelated settings', async function( assert ) +{ + var environment = createEnvironment(); + var key = '|[python][javascript]|workspaceValue||'; + environment.values.set( key, { 'todo-tree.regex.regex': '(SOURCE)', 'editor.tabSize': 3 } ); + var operations = migration.collect( environment, packageJson ); + assert.equal( operations.length, 1 ); + assert.equal( operations[ 0 ].overrideKey, '[python][javascript]' ); + assert.equal( await migration.apply( environment, operations ), 1 ); + assert.deepEqual( environment.values.get( key ), { + 'todo-tree.regex.regex': '(SOURCE)', 'better-todo-tree.regex.regex': '(SOURCE)', 'editor.tabSize': 3 + } ); + assert.equal( migration.collect( environment, packageJson ).length, 0 ); +} ); + +QUnit.test( 'import waits for configuration propagation and disposes its observer', async function( assert ) +{ + var environment = createEnvironment(); + environment.deferPropagation = true; + environment.set( 'todo-tree', 'filtering.includeGlobs', 'workspaceValue', [] ); + assert.equal( await migration.apply( environment, migration.collect( environment, packageJson ) ), 1 ); + assert.equal( migration.collect( environment, packageJson ).length, 0 ); + assert.deepEqual( environment.values.get( 'better-todo-tree|filtering.includeGlobs|workspaceValue||' ), [] ); + assert.equal( environment.listeners.size, 0 ); +} ); diff --git a/test/tree.behavior.test.js b/test/tree.behavior.test.js index 61d0d915..250f3231 100644 --- a/test/tree.behavior.test.js +++ b/test/tree.behavior.test.js @@ -229,6 +229,92 @@ QUnit.module( "behavioral tree", function() assert.equal( provider._onDidChangeTreeData.disposeCalls, 1 ); } ); + QUnit.test( "prototype-named tags keep numeric counts through document replacement", function( assert ) + { + var tags = [ 'constructor', '__proto__', 'toString' ]; + var tree = loadTreeModule( createConfig( { tags: function() { return tags; } } ) ); + var provider = new tree.TreeNodeProvider( { workspaceState: createWorkspaceState() }, function() {}, function() {} ); + var results = tags.map( function( tag, index ) + { + var result = createResult( '/tmp/reserved.js', tag, 'item' ); + result.line = index + 1; + return result; + } ); + + provider.clear( [] ); + provider.replaceDocument( results[ 0 ].uri, results ); + provider.finalizePendingChanges( undefined, { fullSort: true } ); + + [ provider.getTagCountsForStatusBar(), provider.getTagCountsForActivityBar() ].forEach( function( counts ) + { + assert.deepEqual( Object.keys( counts ).sort(), tags.slice().sort() ); + tags.forEach( function( tag ) { assert.strictEqual( counts[ tag ], 1 ); } ); + } ); + + provider.replaceDocument( results[ 0 ].uri, [] ); + provider.finalizePendingChanges( undefined, { fullSort: true } ); + assert.deepEqual( provider.getTagCountsForStatusBar(), {} ); + assert.deepEqual( provider.getTagCountsForActivityBar(), {} ); + provider.dispose(); + } ); + + QUnit.test( "prototype-named file labels survive JSON export", function( assert ) + { + var tree = loadTreeModule( createConfig() ); + var provider = new tree.TreeNodeProvider( { workspaceState: createWorkspaceState() }, function() {}, function() {} ); + var result = createResult( '/tmp/__proto__', 'TODO', 'retained export' ); + + provider.clear( [] ); + provider.replaceDocument( result.uri, [ result ] ); + provider.finalizePendingChanges( undefined, { fullSort: true } ); + + var exported = JSON.parse( JSON.stringify( provider.exportTree() ) ); + assert.true( Object.prototype.hasOwnProperty.call( exported, '__proto__' ) ); + assert.equal( exported[ '__proto__' ][ 'line 3' ], 'TODO retained export' ); + provider.dispose(); + } ); + + QUnit.test( "JSON exports retain distinct matches on the same line", function( assert ) + { + [ false, true ].forEach( function( tagsOnly ) + { + var tree = loadTreeModule( createConfig( { shouldShowTagsOnly: function() { return tagsOnly; } } ) ); + var provider = new tree.TreeNodeProvider( { workspaceState: createWorkspaceState() }, function() {}, function() {} ); + var first = createResult( '/tmp/a.js', 'TODO', 'first' ); + var second = createResult( '/tmp/a.js', 'FIXME', 'second' ); + second.column = 17; + provider.clear( [] ); + provider.replaceDocument( first.uri, [ first, second ] ); + provider.finalizePendingChanges( undefined, { fullSort: true } ); + var exported = JSON.parse( JSON.stringify( provider.exportTree() ) ); + var items = tagsOnly ? exported : exported[ 'a.js' ]; + var prefix = tagsOnly ? '/tmp/a.js ' : ''; + + assert.equal( Object.keys( items ).length, 2 ); + assert.equal( items[ prefix + 'line 3, column 5' ], 'TODO first' ); + assert.equal( items[ prefix + 'line 3, column 17' ], 'FIXME second' ); + provider.dispose(); + } ); + } ); + + QUnit.test( "JSON exports retain notebook cells sharing line and column coordinates", function( assert ) + { + var tree = loadTreeModule( createConfig() ); + var provider = new tree.TreeNodeProvider( { workspaceState: createWorkspaceState() }, function() {}, function() {} ); + var first = createResult( '/tmp/a.ipynb', 'TODO', 'first cell' ); + var second = createResult( '/tmp/a.ipynb', 'TODO', 'second cell' ); + first.sourceId = 'cell-1'; + second.sourceId = 'cell-2'; + provider.clear( [] ); + provider.replaceDocument( first.uri, [ first, second ] ); + provider.finalizePendingChanges( undefined, { fullSort: true } ); + var items = JSON.parse( JSON.stringify( provider.exportTree() ) )[ 'a.ipynb' ]; + assert.equal( Object.keys( items ).length, 2 ); + assert.equal( items[ 'line 3, column 5, source cell-1' ], 'TODO first cell' ); + assert.equal( items[ 'line 3, column 5, source cell-2' ], 'TODO second cell' ); + provider.dispose(); + } ); + QUnit.test( "issue #888 renders the multiline banner match as a single tree label", function( assert ) { var configStub = createConfig( { diff --git a/test/treeExport.test.js b/test/treeExport.test.js new file mode 100644 index 00000000..614879bd --- /dev/null +++ b/test/treeExport.test.js @@ -0,0 +1,47 @@ +var exporter = require( '../src/treeExport.js' ); + +QUnit.module( 'text tree export' ); + +QUnit.test( 'reserved property names render from records without prototypes', function( assert ) +{ + var root = Object.create( null ); + root.__proto__ = Object.create( null ); + root.__proto__.hasOwnProperty = 'TODO retained'; + root.constructor = 'FIXME retained'; + assert.equal( exporter.formatTree( root ), + '├─ __proto__\n│ └─ hasOwnProperty: TODO retained\n└─ constructor: FIXME retained\n' ); +} ); + +QUnit.test( 'multiline values preserve branches and ignore inherited properties', function( assert ) +{ + var root = Object.create( { inherited: 'excluded' } ); + root.file = { 'line 1': 'TODO first\ncontinuation' }; + root.empty = {}; + assert.equal( exporter.formatTree( root ), + '├─ file\n│ └─ line 1: TODO first\n│ continuation\n└─ empty\n' ); + assert.equal( exporter.formatTree( {} ), '' ); +} ); + +QUnit.test( 'invalid nodes and cycles reject with typed errors', function( assert ) +{ + [ null, [], 1, undefined, { invalid: null }, { invalid: function() {} } ].forEach( function( input ) + { + assert.throws( function() { exporter.formatTree( input ); }, function( error ) + { + return error instanceof exporter.TreeExportError && error.code === 'INVALID_EXPORT_NODE'; + } ); + } ); + var root = {}; + root.cycle = root; + assert.throws( function() { exporter.formatTree( root ); }, function( error ) + { + return error instanceof exporter.TreeExportError && error.code === 'CYCLIC_EXPORT'; + } ); +} ); + +QUnit.test( 'shared subtrees render at each path without a false cycle', function( assert ) +{ + var shared = { item: 'TODO item' }; + assert.equal( exporter.formatTree( { first: shared, second: shared } ), + '├─ first\n│ └─ item: TODO item\n└─ second\n └─ item: TODO item\n' ); +} ); diff --git a/test/workflows.github.test.js b/test/workflows.github.test.js index 5656049b..d7548237 100644 --- a/test/workflows.github.test.js +++ b/test/workflows.github.test.js @@ -841,7 +841,7 @@ QUnit.test( '.vscodeignore excludes the upstream clone, test fixtures, build too assert.ok( lines.indexOf( 'TODOS_LISTS/' ) !== -1, '.vscodeignore lists `TODOS_LISTS/`' ); assert.ok( lines.indexOf( '.github/' ) !== -1, '.vscodeignore lists `.github/`' ); assert.ok( lines.indexOf( '.tools/' ) !== -1, '.vscodeignore lists `.tools/` (upstream clone cache)' ); - assert.ok( lines.indexOf( 'buildCodiconNames.js' ) !== -1, '.vscodeignore lists `buildCodiconNames.js` (build-time tool, not needed at runtime)' ); + assert.ok( lines.indexOf( 'scripts/' ) !== -1, '.vscodeignore excludes build-time scripts' ); assert.ok( lines.indexOf( 'old-*.js' ) !== -1, '.vscodeignore lists `old-*.js` (pre-refactor scratch files never ship)' ); assert.ok( lines.indexOf( '*.bak' ) !== -1, '.vscodeignore lists `*.bak` (editor backups never ship)' ); assert.ok( lines.indexOf( '*~' ) !== -1, '.vscodeignore lists `*~` (editor backups never ship)' ); diff --git a/test/workspaceFileWatcher.test.js b/test/workspaceFileWatcher.test.js new file mode 100644 index 00000000..421ecf31 --- /dev/null +++ b/test/workspaceFileWatcher.test.js @@ -0,0 +1,60 @@ +'use strict'; + +var createWorkspaceFileWatcher = require( '../src/runtime/workspaceFileWatcher.js' ).createWorkspaceFileWatcher; + +QUnit.module( 'workspace file watcher' ); + +QUnit.test( 'watcher ownership covers changes, replacement, disable and disposal', function( assert ) +{ + var created = []; + var events = []; + var owner = createWorkspaceFileWatcher( { + createFileSystemWatcher: function( glob ) + { + var watcher = { glob: glob, disposed: 0, + dispose: function() { watcher.disposed++; }, + onDidCreate: function( handler ) { watcher.create = handler; }, + onDidChange: function( handler ) { watcher.change = handler; }, + onDidDelete: function( handler ) { watcher.remove = handler; } }; + created.push( watcher ); + return watcher; + } + }, function( uri ) { events.push( uri ); } ); + owner.configure( undefined ); + assert.equal( created.length, 0 ); + owner.configure( '**/*.js' ); + owner.configure( '**/*.js' ); + assert.equal( created.length, 1 ); + created[ 0 ].create( 'first' ); + created[ 0 ].change( 'second' ); + created[ 0 ].remove( 'third' ); + assert.deepEqual( events, [ 'first', 'second', 'third' ] ); + owner.configure( '**/*.py' ); + assert.equal( created[ 0 ].disposed, 1 ); + assert.equal( created[ 1 ].glob, '**/*.py' ); + created[ 0 ].change( 'stale replaced watcher' ); + assert.deepEqual( events, [ 'first', 'second', 'third' ] ); + owner.configure( undefined ); + assert.equal( created[ 1 ].disposed, 1 ); + created[ 1 ].change( 'stale disabled watcher' ); + assert.deepEqual( events, [ 'first', 'second', 'third' ] ); + owner.dispose(); + assert.equal( created[ 1 ].disposed, 1 ); +} ); + +QUnit.test( 'watcher creation failures propagate and permit explicit retry', function( assert ) +{ + var fail = true; + var watcher = { onDidCreate: function() {}, onDidChange: function() {}, + onDidDelete: function() {}, dispose: function() {} }; + var owner = createWorkspaceFileWatcher( { createFileSystemWatcher: function() + { + if( fail ) { throw new Error( 'watch denied' ); } + return watcher; + } }, function() {} ); + assert.throws( function() { owner.configure( '**/*' ); }, + function( error ) { return error.message === 'watch denied'; } ); + fail = false; + owner.configure( '**/*' ); + owner.dispose(); +} ); diff --git a/webpack.config.js b/webpack.config.js index 10c9bd50..fa0fd911 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,15 +1,13 @@ 'use strict'; const path = require( 'path' ); -const regexRegistry = require( './src/regexRegistry.js' ); /**@type {import('webpack').Configuration}*/ const config = { - target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ + target: 'node', - entry: './src/extension.js', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + entry: './src/extension.js', output: { - // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ path: path.resolve( __dirname, 'dist' ), filename: 'extension.js', libraryTarget: 'commonjs2', @@ -17,24 +15,7 @@ const config = { }, devtool: 'source-map', externals: { - vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ - }, - resolve: { - // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader - extensions: [ '.ts', '.js' ] - }, - module: { - rules: [ - { - test: regexRegistry.createRegExp( 'typescriptFileExtension' ), - exclude: regexRegistry.createRegExp( 'nodeModulesPath' ), - use: [ - { - loader: 'ts-loader' - } - ] - } - ] + vscode: 'commonjs vscode' } }; module.exports = config; From 8fc0978a549de87e8b282619530d03cdcd397dad Mon Sep 17 00:00:00 2001 From: Nathan Trudeau Date: Sun, 13 Sep 2026 11:58:25 -0400 Subject: [PATCH 3/4] fix: bootstrap upstream parity dependencies and gate VSIX cleanup Upstream parity: - add and export ensureUpstreamDependencies for source loading and compiled-bundle preparation, including cached bundle reuse - install locked dependencies in the pinned upstream checkout once per loader instance, recording readiness only after installation succeeds - replace the build-only node_modules existence check with shared initialization that propagates failures and permits subsequent retries - condense loader documentation and clarify the OpenSSL requirement PR VSIX cleanup: - name the staging upload step and expose its artifact ID as a test-build job output - require a nonempty artifact ID for cleanup while retaining always() across publish outcomes Regression coverage: - add bootstrap tests for installation ordering, dependency reuse, cached bundles, nonzero exits, spawn errors, and successful retries - assert the staging upload output binding and exercise cleanup with and without an artifact ID across successful, failed, cancelled, and skipped publish results --- .github/workflows/pr-vsix-build.yml | 4 +- test/parity/upstreamGitLoader.js | 49 +++++---------- test/upstream-bootstrap.test.js | 98 +++++++++++++++++++++++++++++ test/workflows.github.test.js | 22 +++++++ 4 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 test/upstream-bootstrap.test.js diff --git a/.github/workflows/pr-vsix-build.yml b/.github/workflows/pr-vsix-build.yml index 0dbba010..e0c26a9f 100644 --- a/.github/workflows/pr-vsix-build.yml +++ b/.github/workflows/pr-vsix-build.yml @@ -21,6 +21,7 @@ jobs: build: ${{ steps.context.outputs.build }} pull-request-number: ${{ steps.context.outputs.pull-request-number }} targets: ${{ steps.targets.outputs.targets }} + staging-artifact-id: ${{ steps.staging.outputs.artifact-id }} runs-on: ubuntu-24.04 timeout-minutes: 30 steps: @@ -82,6 +83,7 @@ jobs: node scripts/ci/verify-pr-vsix.mjs artifacts/vsix - name: Stage platform VSIX files + id: staging if: steps.context.outputs.build == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: @@ -122,7 +124,7 @@ jobs: needs: - test-build - publish - if: always() && needs.test-build.outputs.build == 'true' + if: always() && needs.test-build.outputs.staging-artifact-id != '' runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: diff --git a/test/parity/upstreamGitLoader.js b/test/parity/upstreamGitLoader.js index aa2678dc..40a38003 100644 --- a/test/parity/upstreamGitLoader.js +++ b/test/parity/upstreamGitLoader.js @@ -1,26 +1,4 @@ -/** - * Clones the upstream Gruntfuggly/todo-tree repository at the pinned commit - * into a workspace-local cache directory, builds its webpack bundle (the - * same artefact that ships in the published VSIX), and exposes module and - * compiled-bundle loaders for the parity harness. - * - * Exports: - * loadModule(relativePath, stubs): - * Compile a single upstream source file as a CommonJS module with the - * supplied stub map. The parity harness uses it to swap stubs for - * vscode, ./tree.js, ./ripgrep.js, etc. while still executing real - * upstream detection logic. - * loadCompiledBundle(stubs): - * Load the compiled dist/extension.js bundle (the artefact inside the - * VSIX) as a CommonJS module with vscode stubbed. Used by smoke tests - * that verify the upstream produces a usable build artefact. - * ensureUpstreamCheckout() / ensureUpstreamBuild(): - * Idempotent bootstrappers callable from test code. - * - * No upstream source or attribution is committed to this repository. All - * upstream artefacts live under .tools/upstream-todo-tree/, which is listed - * in both .gitignore and .vscodeignore. - */ +/** Load pinned upstream source and bundles for parity comparisons. */ var Module = require( 'module' ); var child_process = require( 'child_process' ); @@ -37,6 +15,7 @@ var BUILD_HASH_FILE = path.join( UPSTREAM_DIR, '.upstream-build-hash' ); var COMPILED_BUNDLE_RELATIVE = path.join( 'dist', 'extension.js' ); var checkoutEnsured = false; +var dependenciesEnsured = false; var buildEnsured = false; var moduleCache = new Map(); var sourceCache = new Map(); @@ -65,9 +44,7 @@ function runGit( args, options ) function runCommand( command, args, cwd, extraEnv ) { - // Webpack 5.74 (pinned by upstream) ships pre-OpenSSL-3 hashing helpers - // that throw ERR_OSSL_EVP_UNSUPPORTED on Node 17+; --openssl-legacy-provider - // is applied locally so it never leaks into the better-todo-tree build. + // Upstream webpack requires legacy OpenSSL hashing on Node 17 and newer. var baseEnv = Object.assign( {}, process.env, { npm_config_audit: 'false', npm_config_fund: 'false', @@ -154,6 +131,17 @@ function buildHashMatches() return recorded === UPSTREAM_COMMIT; } +function ensureUpstreamDependencies() +{ + if( dependenciesEnsured !== true ) + { + ensureUpstreamCheckout(); + runNpm( [ 'ci' ], UPSTREAM_DIR ); + dependenciesEnsured = true; + } + return UPSTREAM_DIR; +} + function bundleExists() { return fs.existsSync( path.join( UPSTREAM_DIR, COMPILED_BUNDLE_RELATIVE ) ); @@ -166,7 +154,7 @@ function ensureUpstreamBuild() return path.join( UPSTREAM_DIR, COMPILED_BUNDLE_RELATIVE ); } - ensureUpstreamCheckout(); + ensureUpstreamDependencies(); if( buildHashMatches() === true && bundleExists() === true ) { @@ -174,11 +162,6 @@ function ensureUpstreamBuild() return path.join( UPSTREAM_DIR, COMPILED_BUNDLE_RELATIVE ); } - if( !fs.existsSync( path.join( UPSTREAM_DIR, 'node_modules' ) ) ) - { - runNpm( [ 'ci' ], UPSTREAM_DIR ); - } - // Upstream vscode:prepublish invokes buildCodiconNames.js via a // /usr/local/bin/node shebang absent on Linux CI runners; invoke the two // build steps explicitly with process.execPath and the local webpack binary. @@ -246,6 +229,7 @@ function compileWrapper( source, syntheticPath ) function loadModule( relativePath, stubs ) { + ensureUpstreamDependencies(); var normalized = normalizeRelativePath( relativePath ); var hasStubs = !!stubs && Object.keys( stubs ).length > 0; @@ -345,6 +329,7 @@ module.exports.UPSTREAM_REPO_URL = UPSTREAM_REPO_URL; module.exports.UPSTREAM_DIR = UPSTREAM_DIR; module.exports.COMPILED_BUNDLE_RELATIVE = COMPILED_BUNDLE_RELATIVE; module.exports.ensureUpstreamCheckout = ensureUpstreamCheckout; +module.exports.ensureUpstreamDependencies = ensureUpstreamDependencies; module.exports.ensureUpstreamBuild = ensureUpstreamBuild; module.exports.loadModule = loadModule; module.exports.loadCompiledBundle = loadCompiledBundle; diff --git a/test/upstream-bootstrap.test.js b/test/upstream-bootstrap.test.js new file mode 100644 index 00000000..92df49ae --- /dev/null +++ b/test/upstream-bootstrap.test.js @@ -0,0 +1,98 @@ +var fs = require( 'fs' ); +var path = require( 'path' ); +var helpers = require( './moduleHelpers.js' ); +var upstreamCommit = require( './parity/upstreamGitLoader.js' ).UPSTREAM_COMMIT; + +function createHarness() +{ + var calls = []; + var state = { installStatus: 0 }; + var fixture = path.join( 'src', 'ci-bootstrap-fixture.js' ); + var loader = helpers.loadWithStubs( './parity/upstreamGitLoader.js', { + fs: Object.assign( {}, fs, { + existsSync: function() { return true; }, + readFileSync: function( filename, encoding ) + { + if( filename.endsWith( fixture ) ) + { + calls.push( 'source' ); + return 'module.exports = { loaded: true };'; + } + if( filename.endsWith( '.upstream-build-hash' ) ) + { + calls.push( 'build-hash' ); + return upstreamCommit; + } + return fs.readFileSync( filename, encoding ); + } + } ), + child_process: { + spawnSync: function( executable, args, options ) + { + if( executable === 'git' ) + { + if( args[ 0 ] === 'cat-file' ) { return { status: 0 }; } + if( args.join( ' ' ) === 'rev-parse HEAD' ) + { + return { status: 0, stdout: upstreamCommit }; + } + throw new Error( 'unexpected Git operation: ' + args.join( ' ' ) ); + } + if( executable !== 'npm' || args.join( ' ' ) !== 'ci' ) + { + throw new Error( 'unexpected bootstrap command: ' + executable + ' ' + args.join( ' ' ) ); + } + calls.push( { command: 'npm ci', cwd: options.cwd } ); + return { status: state.installStatus, error: state.installError }; + } + } + } ); + return { loader: loader, calls: calls, state: state, fixture: fixture }; +} + +QUnit.module( 'upstream dependency bootstrap' ); + +QUnit.test( 'source loading installs upstream dependencies once before execution', function( assert ) +{ + var harness = createHarness(); + assert.deepEqual( harness.loader.loadModule( harness.fixture ), { loaded: true } ); + assert.deepEqual( harness.loader.loadModule( harness.fixture ), { loaded: true } ); + assert.equal( harness.loader.ensureUpstreamDependencies(), harness.loader.UPSTREAM_DIR ); + assert.deepEqual( harness.calls, [ + { command: 'npm ci', cwd: harness.loader.UPSTREAM_DIR }, 'source' + ] ); +} ); + +QUnit.test( 'a cached bundle still prepares its own dependencies before reuse', function( assert ) +{ + var harness = createHarness(); + var expected = path.join( harness.loader.UPSTREAM_DIR, harness.loader.COMPILED_BUNDLE_RELATIVE ); + assert.equal( harness.loader.ensureUpstreamBuild(), expected ); + assert.equal( harness.loader.ensureUpstreamBuild(), expected ); + assert.deepEqual( harness.loader.loadModule( harness.fixture ), { loaded: true } ); + assert.deepEqual( harness.calls, [ + { command: 'npm ci', cwd: harness.loader.UPSTREAM_DIR }, 'build-hash', 'source' + ] ); +} ); + +[ 'exit', 'spawn' ].forEach( function( failure ) +{ + QUnit.test( 'dependency ' + failure + ' failure prevents execution and permits retry', function( assert ) + { + var harness = createHarness(); + harness.state.installStatus = 1; + if( failure === 'spawn' ) { harness.state.installError = new Error( 'npm unavailable' ); } + assert.throws( function() { harness.loader.loadModule( harness.fixture ); }, function( error ) + { + return error.message.includes( failure === 'spawn' ? 'npm unavailable' : 'exited with status 1' ); + } ); + assert.deepEqual( harness.calls, [ { command: 'npm ci', cwd: harness.loader.UPSTREAM_DIR } ] ); + harness.state.installStatus = 0; + harness.state.installError = undefined; + assert.deepEqual( harness.loader.loadModule( harness.fixture ), { loaded: true } ); + assert.deepEqual( harness.calls, [ + { command: 'npm ci', cwd: harness.loader.UPSTREAM_DIR }, + { command: 'npm ci', cwd: harness.loader.UPSTREAM_DIR }, 'source' + ] ); + } ); +} ); diff --git a/test/workflows.github.test.js b/test/workflows.github.test.js index d7548237..53058be7 100644 --- a/test/workflows.github.test.js +++ b/test/workflows.github.test.js @@ -546,6 +546,28 @@ QUnit.test( 'trusted PR workflow builds every verified platform VSIX after all g assert.ok( buildWorkflow.indexOf( 'node scripts/ci/delete-pr-vsix-staging.mjs' ) !== -1 ); } ); +QUnit.test( 'staging cleanup requires an upload receipt across publish outcomes', function( assert ) +{ + var workflow = readWorkflow( 'pr-vsix-build.yml' ); + var staging = getWorkflowStepBlock( workflow, 'Stage platform VSIX files' ); + assert.ok( staging.includes( 'id: staging' ) ); + assert.ok( workflow.includes( 'staging-artifact-id: ${{ steps.staging.outputs.artifact-id }}' ) ); + var cleanup = workflow.slice( workflow.indexOf( ' cleanup-staging:' ) ); + var condition = cleanup.split( '\n' ).find( function( line ) { return line.startsWith( ' if: ' ); } ) + .slice( ' if: '.length ); + var shouldClean = new Function( 'always', 'artifactId', 'publishResult', 'return ' + condition + .replaceAll( 'needs.test-build.outputs.staging-artifact-id', 'artifactId' ) + .replaceAll( 'needs.publish.result', 'publishResult' ) ); + [ 'success', 'failure', 'cancelled', 'skipped' ].forEach( function( publishResult ) + { + [ '', '400' ].forEach( function( artifactId ) + { + assert.strictEqual( shouldClean( function() { return true; }, artifactId, publishResult ), + artifactId !== '', publishResult + '/' + ( artifactId || 'no upload' ) ); + } ); + } ); +} ); + QUnit.test( 'trusted orchestration isolates and cancels only one PR generation', function( assert ) { var ciWorkflow = readWorkflow( 'ci.yml' ); From cf574b861b95ce8a0d854862d138cd63ec287e70 Mon Sep 17 00:00:00 2001 From: Nathan Trudeau Date: Sun, 13 Sep 2026 12:31:36 -0400 Subject: [PATCH 4/4] fix: validate PR VSIX target manifest under immutable checkout The workflow now resolves publish targets from the immutable checkout and writes `targets` output only after target-list verification. Workflow: - move platform target resolution after immutable workflow checkout - parse and verify `scripts/release/targets.json` through `verifyTargetMap` before emitting `targets=` - keep immutable targets when trusted manifest data is missing, malformed, or drifted Tests: - add `runPlatformTargetResolution` workflow-step harness with staged manifests and isolated output targets - add coverage for manifest drift, malformed trusted content, missing build manifest, invalid target lists, and output-write failures - assert workflow step ordering and resolved `targets` propagation into platform build inputs --- .github/workflows/pr-vsix-build.yml | 17 ++-- test/workflows.github.test.js | 118 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-vsix-build.yml b/.github/workflows/pr-vsix-build.yml index e0c26a9f..c482f960 100644 --- a/.github/workflows/pr-vsix-build.yml +++ b/.github/workflows/pr-vsix-build.yml @@ -48,11 +48,6 @@ jobs: PR_VSIX_MERGE_INTERVAL_MS: 5000 run: node scripts/ci/resolve-pr-vsix-context.mjs - - name: Resolve platform targets - id: targets - if: steps.context.outputs.build == 'true' - run: echo "targets=$(node -p "JSON.stringify(require('./scripts/release/targets.json'))")" >> "$GITHUB_OUTPUT" - - name: Check out immutable build context if: steps.context.outputs.build == 'true' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 @@ -61,6 +56,18 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Resolve platform targets + id: targets + if: steps.context.outputs.build == 'true' + run: | + node --input-type=module <<'NODE' + import fs from 'node:fs'; + import { verifyTargetMap } from './scripts/ci/verify-pr-vsix.mjs'; + const targets = JSON.parse(fs.readFileSync('scripts/release/targets.json', 'utf8')); + verifyTargetMap(targets); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `targets=${JSON.stringify(targets)}\n`); + NODE + - name: Install dependencies if: steps.context.outputs.build == 'true' run: npm ci diff --git a/test/workflows.github.test.js b/test/workflows.github.test.js index 53058be7..3e31dba3 100644 --- a/test/workflows.github.test.js +++ b/test/workflows.github.test.js @@ -1,5 +1,7 @@ var fs = require( 'fs' ); +var os = require( 'os' ); var path = require( 'path' ); +var spawnSync = require( 'child_process' ).spawnSync; var regexRegistry = require( '../src/regexRegistry.js' ); var ACTION_REVISIONS = Object.freeze( { @@ -266,6 +268,66 @@ function getWorkflowStepBlock( contents, stepName ) return contents.slice( start, end === -1 ? contents.length : end ); } +function runPlatformTargetResolution( trustedManifest, buildManifest, outputIsDirectory ) +{ + var workflow = readWorkflow( 'pr-vsix-build.yml' ); + var directory = fs.mkdtempSync( path.join( os.tmpdir(), 'pr-vsix-targets-' ) ); + var manifest = path.join( directory, 'scripts', 'release', 'targets.json' ); + var output = path.join( directory, 'github-output' ); + var result; + var steps = [ 'Check out trusted workflow helpers', 'Check out immutable build context', 'Resolve platform targets' ] + .map( function( name ) + { + var index = workflow.indexOf( ' - name: ' + name ); + if( index < 0 ) { throw new Error( 'missing workflow step: ' + name ); } + return { name: name, index: index }; + } ).sort( function( left, right ) { return left.index - right.index; } ); + try + { + fs.mkdirSync( path.dirname( manifest ), { recursive: true } ); + [ 'scripts/ci/verify-pr-vsix.mjs', 'scripts/release/ripgrep-targets.mjs' ].forEach( function( relativePath ) + { + var destination = path.join( directory, relativePath ); + fs.mkdirSync( path.dirname( destination ), { recursive: true } ); + fs.writeFileSync( destination, readRepositoryFile( relativePath ) ); + } ); + if( outputIsDirectory ) { fs.mkdirSync( output ); } + else { fs.writeFileSync( output, 'preserved=1\n' ); } + steps.forEach( function( step ) + { + if( step.name !== 'Resolve platform targets' ) + { + var contents = step.name === 'Check out trusted workflow helpers' ? trustedManifest : buildManifest; + if( contents === null ) { fs.rmSync( manifest, { force: true } ); } + else { fs.writeFileSync( manifest, contents ); } + return; + } + var lines = getWorkflowStepBlock( workflow, step.name ).split( '\n' ); + var runIndex = lines.findIndex( function( line ) { return line.startsWith( ' run: ' ); } ); + if( runIndex < 0 ) { throw new Error( 'target resolution command is missing' ); } + var command = lines[ runIndex ].slice( ' run: '.length ); + if( command === '|' ) + { + command = lines.slice( runIndex + 1 ).map( function( line ) { return line.slice( 10 ); } ).join( '\n' ); + } + result = spawnSync( 'bash', [ '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', command ], { + cwd: directory, + encoding: 'utf8', + env: Object.assign( {}, process.env, { + PATH: path.dirname( process.execPath ) + path.delimiter + process.env.PATH, + GITHUB_OUTPUT: output + } ) + } ); + if( result.error ) { throw result.error; } + } ); + return { status: result.status, stderr: result.stderr, output: outputIsDirectory ? '' : fs.readFileSync( output, 'utf8' ) }; + } + finally + { + fs.rmSync( directory, { recursive: true, force: true } ); + } +} + function assertSecurityWorkflowContract( assert, securityWorkflow, label ) { var references = getExternalActionReferences( securityWorkflow ); @@ -525,6 +587,7 @@ QUnit.test( 'trusted PR workflow builds every verified platform VSIX after all g assert.ok( buildWorkflow.indexOf( 'timeout-minutes: 30' ) !== -1 ); [ 'Check out immutable build context', + 'Resolve platform targets', 'Install dependencies', 'Run test suite', 'Build production bundle', @@ -546,6 +609,61 @@ QUnit.test( 'trusted PR workflow builds every verified platform VSIX after all g assert.ok( buildWorkflow.indexOf( 'node scripts/ci/delete-pr-vsix-staging.mjs' ) !== -1 ); } ); +QUnit.test( 'publish targets follow the immutable checkout when trusted manifests differ', function( assert ) +{ + var targets = require( '../scripts/release/targets.json' ); + [ targets.concat( 'web' ), targets.slice( 1 ), targets.slice().reverse() ].forEach( function( trustedTargets ) + { + var result = runPlatformTargetResolution( JSON.stringify( trustedTargets ), JSON.stringify( targets ) ); + assert.strictEqual( result.status, 0, result.stderr ); + assert.strictEqual( result.output, 'preserved=1\ntargets=' + JSON.stringify( targets ) + '\n' ); + } ); + var workflow = readWorkflow( 'pr-vsix-build.yml' ); + assert.ok( workflow.indexOf( ' - name: Resolve platform targets' ) < + workflow.indexOf( ' - name: Build platform VSIX bundle' ) ); + assert.ok( workflow.includes( 'targets: ${{ steps.targets.outputs.targets }}' ) ); +} ); + +QUnit.test( 'target resolution ignores a malformed trusted manifest after immutable checkout', function( assert ) +{ + var manifest = JSON.stringify( require( '../scripts/release/targets.json' ) ); + var result = runPlatformTargetResolution( '{', manifest ); + assert.strictEqual( result.status, 0, result.stderr ); + assert.strictEqual( result.output, 'preserved=1\ntargets=' + manifest + '\n' ); +} ); + +[ { name: 'missing', contents: null }, { name: 'malformed', contents: '{' } ].forEach( function( fixture ) +{ + QUnit.test( 'target resolution fails without output for a ' + fixture.name + ' build manifest', function( assert ) + { + var result = runPlatformTargetResolution( JSON.stringify( require( '../scripts/release/targets.json' ) ), fixture.contents ); + assert.notStrictEqual( result.status, 0 ); + assert.ok( result.stderr.length > 0 ); + assert.strictEqual( result.output, 'preserved=1\n' ); + } ); +} ); + +QUnit.test( 'target resolution propagates output write failures', function( assert ) +{ + var manifest = JSON.stringify( require( '../scripts/release/targets.json' ) ); + var result = runPlatformTargetResolution( manifest, manifest, true ); + assert.notStrictEqual( result.status, 0 ); + assert.ok( result.stderr.length > 0 ); +} ); + +QUnit.test( 'target resolution rejects invalid target lists before output', function( assert ) +{ + var targets = require( '../scripts/release/targets.json' ); + [ null, {}, [], targets.concat( targets[ 0 ] ), targets.concat( 'web' ), targets.slice().reverse() ] + .forEach( function( invalidTargets ) + { + var result = runPlatformTargetResolution( JSON.stringify( targets ), JSON.stringify( invalidTargets ) ); + assert.notStrictEqual( result.status, 0 ); + assert.ok( result.stderr.includes( 'PrVsixVerificationError' ) ); + assert.strictEqual( result.output, 'preserved=1\n' ); + } ); +} ); + QUnit.test( 'staging cleanup requires an upload receipt across publish outcomes', function( assert ) { var workflow = readWorkflow( 'pr-vsix-build.yml' );