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/pr-vsix-build.yml b/.github/workflows/pr-vsix-build.yml index 0dbba010..c482f960 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: @@ -47,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 @@ -60,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 @@ -82,6 +90,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 +131,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/.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..4f98bbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ artifacts/**/*.log artifacts/perf/runtime-benchmarks.json artifacts/**/*.json Better.TODO.Tree.settings.txt +.forge/ diff --git a/.npmignore b/.npmignore index d2d77ecc..cc3a5f28 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,6 @@ .vscode/ +.devcontainer/ +.vscodeignore .gitignore node_modules/ src/ @@ -16,11 +18,11 @@ TODOS_LISTS/ TODOS_LISTS/** .tools/ .act-artifacts/ +.forge/ 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 728bdd2a..705bea9a 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -17,12 +17,13 @@ TODOS_LISTS/ TODOS_LISTS/** .tools/ .act-artifacts/ +.forge/ MIGRATION.md OPEN_VSX_CERTIFICATE_REPORT.md CHANGELOG.upstream.md Better.TODO.Tree.settings.txt -buildCodiconNames.js old-*.js *.bak *~ .devcontainer/ +.npmignore 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 9d424072..9884d3d1 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,13 @@ 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. 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 @@ -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). @@ -299,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). @@ -336,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). @@ -380,15 +385,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`)
@@ -402,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: @@ -419,11 +424,10 @@ 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. +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. @@ -647,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/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/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 9c66754b..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", @@ -1472,6 +1470,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1508,6 +1507,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 +1660,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 +1716,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 +1741,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 +1760,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 +1872,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 +2358,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 +2611,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 +2624,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.20.1", @@ -3341,9 +3344,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 +3358,7 @@ "url": "https://github.com/sponsors/nodeca" } ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3879,11 +3883,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 +4373,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" @@ -4532,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", @@ -4779,15 +4772,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" }, @@ -5322,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", @@ -5399,10 +5384,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 +5423,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 +6868,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 +6889,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 +6981,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 +7020,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 +7037,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 +7109,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 +7425,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 +7600,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 +8065,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 +8408,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 +8742,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": { @@ -8862,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", @@ -9004,14 +8981,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" } @@ -9369,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", @@ -9426,9 +9398,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 +9422,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..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 5c35eed3..aff49d77 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 ) @@ -614,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, @@ -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: { @@ -700,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, @@ -738,10 +781,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 ) { @@ -816,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 ); }, @@ -835,6 +907,7 @@ function createVscodeStub( options ) } } }; + return vscodeStub; } function createExtensionHarness( options ) @@ -861,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 } ); @@ -881,7 +959,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' ), @@ -1215,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() {}, @@ -1377,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', '' ); } } @@ -1403,6 +1494,7 @@ function createExtensionHarness( options ) return { extension: extension, + config: configStub, context: context, options: options, get provider() @@ -1484,6 +1576,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 ) @@ -1543,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( { @@ -1663,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 = [ { @@ -4866,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( { @@ -4932,6 +5207,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,7 +5275,240 @@ QUnit.test( "revealInFile reports VS Code open failures through the command boun } ); } ); -QUnit.test( "markdown migration writes the shared default regex source", function( assert ) +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( "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( { + 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( "explicit historical markdown regex remains unchanged without migration prompts", function( assert ) { var harness = createExtensionHarness( { scanMode: 'open files', @@ -4989,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 8772374a..fc50dd1a 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 ); } }; } @@ -229,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' ) ]; @@ -412,7 +440,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 +501,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 +1053,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 +1157,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..ecc590a1 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 ); @@ -121,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 ); + 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(); @@ -162,14 +212,15 @@ QUnit.test( 'prepublish builds are deterministic and codicon updates are explici { var scripts = readPackageJson().scripts; - assert.equal( scripts[ 'vscode:prepublish' ], 'webpack --mode production' ); - assert.equal( scripts[ 'codicons:update' ], 'node ./buildCodiconNames.js' ); - assert.equal( scripts[ 'vscode:prepublish' ].indexOf( 'buildCodiconNames' ), -1 ); + assert.equal( scripts[ 'vscode:prepublish' ], + 'node ./scripts/sync-settings-aliases.js --check && webpack --mode production' ); + assert.equal( scripts[ 'codicons:update' ], 'python3 ./scripts/build-codicon-assets.py' ); + assert.equal( scripts[ 'vscode:prepublish' ].indexOf( 'build-codicon-assets' ), -1 ); } ); QUnit.test( 'generated codicon table stores aliases instead of codepoints', function( assert ) { - var codiconNames = require( '../src/codiconNames.js' ); + var codiconNames = Object.keys( require( '../src/codiconAliases.json' ) ); assert.ok( codiconNames.indexOf( 'bug' ) !== -1 ); assert.ok( codiconNames.indexOf( 'flame' ) !== -1 ); @@ -190,81 +241,93 @@ QUnit.test( 'release packaging has the ripgrep-universal build dependency', func QUnit.test( 'regex defaults are generated by the shared registry', function( assert ) { var currentRegex = getConfigurationProperty( 'better-todo-tree.regex.regex' ); - var legacyRegex = getConfigurationProperty( 'todo-tree.regex.regex' ); assert.equal( currentRegex.default, regexRegistry.DEFAULT_REGEX_SOURCE ); - 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', 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.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 ) { var englishNls = readPackageNls( 'package.nls.json' ); + assert.equal( englishNls[ 'better-todo-tree.configuration.ripgrep.ripgrep.markdownDescription' ], + 'Custom ripgrep executable path. Empty value uses the packaged ripgrep binary.' ); +} ); - assert.equal( - englishNls[ 'todo-tree.configuration.ripgrep.ripgrep.markdownDescription' ], - 'Custom ripgrep executable path. Empty value uses the packaged ripgrep binary.' - ); - assert.equal( - englishNls[ 'better-todo-tree.configuration.ripgrep.ripgrep.markdownDescription' ], - 'Custom ripgrep executable path. Empty value uses the packaged ripgrep binary.' - ); +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( 'legacy settings remain present and deprecated', function( assert ) +QUnit.test( 'streaming ripgrep output exposes no inert buffer-size setting', function( assert ) { - var currentSetting = getConfigurationProperty( 'better-todo-tree.general.tags' ); - var legacySetting = getConfigurationProperty( 'todo-tree.general.tags' ); + 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.equal( configurationProperties[ 'todo-tree.ripgrep.ripgrepMaxBuffer' ].type, 'integer' ); + assert.strictEqual( packageNls[ 'better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription' ], undefined ); + assert.strictEqual( packageNlsZhCn[ 'better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription' ], undefined ); + assert.strictEqual( extensionSource.indexOf( 'ripgrepMaxBuffer' ), -1 ); +} ); - assert.ok( currentSetting ); - assert.ok( legacySetting ); - assert.equal( legacySetting.deprecationMessage, '%todo-tree.configuration.legacyNamespace.deprecationMessage%' ); - assert.equal( legacySetting.markdownDeprecationMessage, '%todo-tree.configuration.legacyNamespace.markdownDeprecationMessage%' ); +QUnit.test( 'Todo Tree aliases share the current schema and explicit palette opt-in', function( assert ) +{ + var synchronize = require( '../scripts/sync-settings-aliases.js' ).synchronize; + var manifest = readPackageJson(); + assert.deepEqual( synchronize( JSON.parse( JSON.stringify( manifest ) ) ), manifest ); + assert.deepEqual( getConfigurationProperty( 'todo-tree.general.tags' ), + getConfigurationProperty( 'better-todo-tree.general.tags' ) ); + assert.strictEqual( getConfigurationProperty( 'todo-tree.highlights.useColourScheme' ).default, false ); } ); QUnit.test( 'issue #905 filtering defaults keep Go files included without any file-type allowlist', function( assert ) { var currentIncludeGlobs = getConfigurationProperty( 'better-todo-tree.filtering.includeGlobs' ); - var legacyIncludeGlobs = getConfigurationProperty( 'todo-tree.filtering.includeGlobs' ); var currentExcludeGlobs = getConfigurationProperty( 'better-todo-tree.filtering.excludeGlobs' ); - var legacyExcludeGlobs = getConfigurationProperty( 'todo-tree.filtering.excludeGlobs' ); assert.deepEqual( currentIncludeGlobs.default, [] ); - assert.deepEqual( legacyIncludeGlobs.default, [] ); assert.deepEqual( currentExcludeGlobs.default, [ '**/node_modules/*/**' ] ); - assert.deepEqual( legacyExcludeGlobs.default, [ '**/node_modules/*/**' ] ); } ); QUnit.test( 'issue #883 notebook scanning keeps vscode-notebook-cell enabled in the default schemes list', function( assert ) { var currentSchemes = getConfigurationProperty( 'better-todo-tree.general.schemes' ); - var legacySchemes = getConfigurationProperty( 'todo-tree.general.schemes' ); assert.ok( currentSchemes.default.indexOf( 'vscode-notebook-cell' ) !== -1 ); - assert.ok( legacySchemes.default.indexOf( 'vscode-notebook-cell' ) !== -1 ); } ); QUnit.test( 'issue #57 and #77 scanning progress setting defaults to status bar only', function( assert ) { var currentProgress = getConfigurationProperty( 'better-todo-tree.general.showScanningProgress' ); - var legacyProgress = getConfigurationProperty( 'todo-tree.general.showScanningProgress' ); var expectedModes = [ 'none', 'status bar', 'notification', 'tree', 'all' ]; var english = readPackageNls( 'package.nls.json' ); var chinese = readPackageNls( 'package.nls.zh-cn.json' ); assert.equal( currentProgress.default, 'status bar' ); - assert.equal( legacyProgress.default, 'status bar' ); assert.deepEqual( currentProgress.enum, expectedModes ); - assert.deepEqual( legacyProgress.enum, expectedModes ); - assert.equal( legacyProgress.deprecationMessage, '%todo-tree.configuration.legacyNamespace.deprecationMessage%' ); assert.equal( typeof english[ 'better-todo-tree.configuration.general.showScanningProgress.markdownDescription' ], 'string' ); assert.equal( typeof chinese[ 'better-todo-tree.configuration.general.showScanningProgress.markdownDescription' ], 'string' ); } ); @@ -407,7 +470,5 @@ QUnit.test( 'showBadges metadata documents file icon theme coupling', function( var chinese = readPackageNls( 'package.nls.zh-cn.json' ); assert.ok( english[ 'better-todo-tree.configuration.tree.showBadges.markdownDescription' ].indexOf( 'file icon theme resources' ) >= 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 86dd3e31..6c060855 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,8 +40,10 @@ 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' ].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..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; @@ -273,7 +257,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 +294,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 ) @@ -335,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/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/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/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 deleted file mode 100644 index 8ff2eae4..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, 73 ); - assert.equal( legacySettings.length, 73 ); - 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/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..250f3231 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,97 @@ 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( "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 ) 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/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/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 6f172bbe..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 ); @@ -439,6 +501,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 +513,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 ); @@ -518,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', @@ -539,6 +609,83 @@ 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' ); + 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' ); @@ -834,7 +981,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;