diff --git a/README.md b/README.md
index 0ae17a3d..8b5da9f3 100644
--- a/README.md
+++ b/README.md
@@ -392,9 +392,9 @@ Normally, multiline support is enabled by detecting the use of `\n` in the regex
**better-todo-tree.regex.regex** (
(//|#|<!--|;|/\\*|^|^[ \\t]*(-|\\d+.))\\s*($TAGS))
-This defines the regex used to locate TODOs. By default, it searches for tags in comments starting with //, #, ;, <!-- or /*, and markdown todo lists. The ($TAGS) placeholder is replaced by the expanded tag list. Prefix-only custom regexes that contain ($TAGS) render text from the detected tag to the end of the physical line.
+This defines the regex used to locate TODOs. By default, it searches for tags in comments starting with //, #, ;, <!-- or /*, and markdown todo lists. The ($TAGS) placeholder is replaced by the expanded tag list. Prefix-only custom regexes that contain ($TAGS) render text from the detected tag to the end of the physical line. Workspace searches use PCRE2 automatically when look-around or backreference syntax requires it.
-*Note: This is a [Rust regular expression](https://docs.rs/regex/1.0.0/regex), not javascript.*
+*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.
@@ -412,7 +412,7 @@ Set to false to allow tags to be matched regardless of case.
Empty value uses the packaged ripgrep binary. Set an absolute executable path to override it.
**better-todo-tree.ripgrep.ripgrepArgs** (`"--max-columns=1000"`)
-Use this to pass additional arguments to ripgrep. e.g. `"-i"` to make the search case insensitive. *Use with caution!*
+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.
diff --git a/artifacts/perf/issue42-regex-engine.md b/artifacts/perf/issue42-regex-engine.md
new file mode 100644
index 00000000..07431fcc
--- /dev/null
+++ b/artifacts/perf/issue42-regex-engine.md
@@ -0,0 +1,34 @@
+# Issue 42 Regex Engine Benchmark
+
+## Corpus
+
+| Field | Value |
+| --- | ---: |
+| Files | 160 |
+| Task lines per file | 8 |
+| Expected matches | 1280 |
+| Regex edit | Removed `|;` from `utils.DEFAULT_REGEX_SOURCE` |
+| `rg` binary | `node_modules/@vscode/ripgrep-universal/bin/linux-x64/rg` |
+
+## Results
+
+| Strategy | Exit | Lookaround error | Matches | p50 ms | p95 ms | Throughput matches/s | Peak RSS MiB |
+| --- | ---: | --- | ---: | ---: | ---: | ---: | ---: |
+| Raw edited regex without PCRE2 | 2 | true | 0 | - | - | - | - |
+| Raw edited regex with PCRE2 and workspace normalization | 0 | false | 1280 | 30.209 | 38.678 | 42370.9 | 94.12 |
+| Candidate tag scan plus local normalization | 0 | false | 1280 | 22.259 | 27.029 | 57505.8 | 96.06 |
+
+## Command
+
+```bash
+node --expose-gc scripts/perf/issue42-regex-engine.js
+```
+
+## Invariants
+
+| Invariant | Value |
+| --- | --- |
+| Broken raw path fails before result parsing | `exit=2` |
+| Fixed path returns all matches | `1280/1280` |
+| Fixed path preserves rendered task text | Covered by `issue #42 PCRE2 markdown task payload keeps display text` |
+| Candidate route avoids raw lookaround regex in `rg` | Covered by `issue #42 default-derived workspace regex uses candidate scan` |
diff --git a/artifacts/perf/local-user-flow-issue42.md b/artifacts/perf/local-user-flow-issue42.md
new file mode 100644
index 00000000..77cb0ac6
--- /dev/null
+++ b/artifacts/perf/local-user-flow-issue42.md
@@ -0,0 +1,106 @@
+# Runtime Benchmarks
+
+- Baseline ref: `a6f60e0ce830c4649ac34fc05e5a1799ec91d151`
+- Current source: working tree
+- Node: `v25.2.0`
+- Selection mode: `suite`
+- Declared suite: `user-flow`
+- Result-count validation: `12 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-124-generic |
+| Host | Architecture | x64 |
+| Host | Load Average | 3.76, 3.59, 3.75 |
+| 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,767,552 bytes`) |
+| Memory | Available At Collection | 15.95 GiB (`17,127,993,344 bytes`) |
+| Memory | Online Physical RAM | 66.00 GiB (`70,866,960,384 bytes`) |
+| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 106 GiB (`113,449,418,752 bytes`) |
+| Memory | DMI / SPD | Unavailable: /sys/firmware/dmi/tables/smbios_entry_point: Permission denied /dev/mem: Permission denied |
+| Storage | Root Device | nvme1n1 (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 |
+| --- | --- | --- | --- | --- |
+| open-file-default-save-rescan-visible-tree | user-flow | Save an already-open file that uses default tag scanning and redraw the visible tree. | Document save listener, document rescan, search-result replacement, and visible-tree render. | Real document text in a VS Code event harness. |
+| open-file-custom-save-rescan-visible-tree | user-flow | Save an already-open file that uses custom regex scanning and redraw the visible tree. | Document save listener, custom-regex document rescan, search-result replacement, and visible-tree render. | Real document text in a VS Code event harness. |
+| tree-view-cycle-visible-tree | user-flow | Cycle the tree between flat, tags-only, and tree views and redraw the visible tree each time. | View-mode commands, workspace-state mutation, and visible-tree rebuild/render. | Fixture workspace tree in a VS Code event harness. |
+| tree-group-toggle-tags-view | user-flow | Toggle tag grouping on and off while in tags view and redraw the visible tree. | Grouping commands, workspace-state mutation, and visible-tree rebuild/render. | Fixture workspace tree in a VS Code event harness. |
+| tree-filter-visible-tree | user-flow | Apply a tree filter and clear it again while the tree is visible. | Filter command handling, tree filtering, and visible-tree render. | Fixture workspace tree in a VS Code event harness. |
+| tree-view-repeat-click-burst | user-flow | Repeatedly click the same view button while the tree state is still mutating. | Overlapping command handling and workspace-state writes. | Command burst against the extension command handlers in a VS Code event harness. |
+| tree-expansion-toggle-visible-tree | user-flow | Expand and then collapse the visible tree. | Expansion commands, workspace-state mutation, and visible-tree rebuild/render. | Fixture workspace tree in a VS Code event harness. |
+| workspace-default-relative-rebuild-visible-tree | user-flow | Trigger a workspace refresh with default tag scanning and rebuild the visible tree from workspace matches. | Workspace refresh orchestration, ripgrep event handling, file reads, result application, and tree rebuild/render. | Fixture ripgrep matches, fixture file contents, and fixture scan results in a VS Code event harness. |
+| workspace-custom-relative-rebuild-visible-tree | user-flow | Trigger a workspace refresh with custom regex scanning and rebuild the visible tree from workspace matches. | Workspace refresh orchestration, ripgrep event handling, regex-match normalization, result application, and tree rebuild/render. | Fixture ripgrep matches, fixture file contents, and fixture normalized regex results in a VS Code event harness. |
+| 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-highlight-change-open-file | user-flow | Edit a visible file and refresh its highlights. | Text-change event handling, decoration 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 |
+| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
+| open-file-default-save-rescan-visible-tree | user-flow | 106.37 | 2.15 | 108.62 | 2.37 | 116.03 | 2.4 |
+| open-file-custom-save-rescan-visible-tree | user-flow | 93.1 | 3.18 | 94.28 | 3.39 | 97.26 | 3.48 |
+| tree-view-cycle-visible-tree | user-flow | 9.85 | 0.38 | 11.41 | 0.5 | 11.46 | 0.51 |
+| tree-group-toggle-tags-view | user-flow | 7.62 | 0.31 | 9.09 | 0.42 | 10.85 | 0.46 |
+| tree-filter-visible-tree | user-flow | 0.42 | 0.33 | 0.53 | 0.45 | 0.6 | 0.49 |
+| tree-view-repeat-click-burst | user-flow | 0.2 | 0.25 | 0.26 | 0.3 | 0.26 | 0.32 |
+| tree-expansion-toggle-visible-tree | user-flow | 8.02 | 0.44 | 8.48 | 0.52 | 11.3 | 0.54 |
+| workspace-default-relative-rebuild-visible-tree | user-flow | 7.36 | 1.77 | 9.19 | 2.6 | 9.36 | 2.71 |
+| workspace-custom-relative-rebuild-visible-tree | user-flow | 48.07 | 4.83 | 50.77 | 5.58 | 51.2 | 5.6 |
+| visible-editor-highlight-open-file | user-flow | 19.98 | 1.8 | 22.55 | 2.56 | 23.45 | 2.73 |
+| visible-editor-highlight-change-open-file | user-flow | 20.4 | 1.61 | 21.04 | 1.87 | 22.2 | 1.97 |
+| visible-editor-custom-highlight-config-open-file | user-flow | 1866.49 | 2.56 | 1901.63 | 3.75 | 2124.97 | 4.03 |
+
+## 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 |
+| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
+| open-file-default-save-rescan-visible-tree | user-flow | 26.25 | 24.25 | 27 | 24.63 | 27.13 | 24.88 | 27.13 | 24.88 |
+| open-file-custom-save-rescan-visible-tree | user-flow | 26.75 | 21 | 27.63 | 21.38 | 27.75 | 21.75 | 27.75 | 21.75 |
+| tree-view-cycle-visible-tree | user-flow | 17 | 9.13 | 17.88 | 9.38 | 17.88 | 9.63 | 17.88 | 9.63 |
+| tree-group-toggle-tags-view | user-flow | 16.38 | 9.13 | 16.88 | 9.5 | 17.25 | 9.5 | 17.25 | 9.5 |
+| tree-filter-visible-tree | user-flow | 18.5 | 11.75 | 19 | 12.13 | 19.5 | 12.13 | 19.5 | 12.13 |
+| tree-view-repeat-click-burst | user-flow | 1.25 | 1.63 | 1.38 | 1.75 | 1.5 | 1.75 | 1.5 | 1.75 |
+| tree-expansion-toggle-visible-tree | user-flow | 16.75 | 9.13 | 18 | 9.25 | 18.25 | 9.63 | 18.25 | 9.63 |
+| workspace-default-relative-rebuild-visible-tree | user-flow | 17.13 | 9.63 | 17.38 | 9.88 | 17.38 | 10.13 | 17.38 | 10.13 |
+| workspace-custom-relative-rebuild-visible-tree | user-flow | 71.5 | 13.88 | 74.25 | 14.13 | 75.63 | 14.13 | 75.63 | 14.13 |
+| visible-editor-highlight-open-file | user-flow | 48.63 | 11.88 | 49.38 | 13.13 | 49.88 | 13.25 | 49.88 | 13.25 |
+| visible-editor-highlight-change-open-file | user-flow | 47.73 | 11.75 | 49.5 | 12 | 49.88 | 12.13 | 49.88 | 12.13 |
+| visible-editor-custom-highlight-config-open-file | user-flow | 159.88 | 14.25 | 162.5 | 14.49 | 163.63 | 14.63 | 163.63 | 14.63 |
+
+## 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 |
+| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
+| open-file-default-save-rescan-visible-tree | user-flow | 81.83 | 79.71 | 82.42 | 80.09 | 82.43 | 80.34 | 82.43 | 80.34 |
+| open-file-custom-save-rescan-visible-tree | user-flow | 82.2 | 76.46 | 82.96 | 76.9 | 83.2 | 76.94 | 83.2 | 76.94 |
+| tree-view-cycle-visible-tree | user-flow | 72.55 | 64.64 | 73.09 | 65.07 | 73.39 | 65.23 | 73.39 | 65.23 |
+| tree-group-toggle-tags-view | user-flow | 71.89 | 64.42 | 72.26 | 64.91 | 72.29 | 65.04 | 72.29 | 65.04 |
+| tree-filter-visible-tree | user-flow | 73.81 | 67.27 | 74.63 | 67.42 | 74.88 | 67.46 | 74.88 | 67.46 |
+| tree-view-repeat-click-burst | user-flow | 56.72 | 57.03 | 56.81 | 57.46 | 57.16 | 57.54 | 57.16 | 57.54 |
+| tree-expansion-toggle-visible-tree | user-flow | 72.12 | 64.68 | 73.64 | 64.89 | 73.68 | 64.94 | 73.68 | 64.94 |
+| workspace-default-relative-rebuild-visible-tree | user-flow | 72.51 | 65.09 | 72.97 | 65.21 | 73.01 | 65.3 | 73.01 | 65.3 |
+| workspace-custom-relative-rebuild-visible-tree | user-flow | 126.91 | 69.36 | 129.91 | 69.56 | 131.12 | 69.6 | 131.12 | 69.6 |
+| visible-editor-highlight-open-file | user-flow | 104.12 | 67.45 | 104.94 | 68.59 | 105.26 | 68.66 | 105.26 | 68.66 |
+| visible-editor-highlight-change-open-file | user-flow | 103.36 | 67.13 | 104.87 | 67.45 | 105.2 | 67.89 | 105.2 | 67.89 |
+| visible-editor-custom-highlight-config-open-file | user-flow | 215.43 | 69.78 | 218.03 | 69.93 | 219.25 | 70.08 | 219.25 | 70.08 |
diff --git a/artifacts/perf/runtime-benchmarks.md b/artifacts/perf/runtime-benchmarks.md
index 2425e359..570ff1ec 100644
--- a/artifacts/perf/runtime-benchmarks.md
+++ b/artifacts/perf/runtime-benchmarks.md
@@ -13,9 +13,9 @@
| --- | --- | --- |
| Host | Hostname | n00ne-AERO-17-YD |
| Host | OS | Ubuntu 22.04.5 LTS |
-| Host | Kernel | 6.8.0-117-generic |
+| Host | Kernel | 6.8.0-124-generic |
| Host | Architecture | x64 |
-| Host | Load Average | 2.68, 2.68, 2.62 |
+| Host | Load Average | 4.47, 4.3, 4.04 |
| Host | Available Parallelism | - |
| CPU | Model | Intel(R) Core(TM) i9-14900HX |
| CPU | Vendor | GenuineIntel |
@@ -23,11 +23,11 @@
| 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,767,552 bytes`) |
-| Memory | Available At Collection | 29.79 GiB (`31,985,217,536 bytes`) |
+| Memory | Available At Collection | 16.06 GiB (`17,242,337,280 bytes`) |
| Memory | Online Physical RAM | 66.00 GiB (`70,866,960,384 bytes`) |
-| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 111 GiB (`119,200,763,904 bytes`) |
+| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 106 GiB (`113,448,108,032 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 |
+| Storage | Root Device | nvme1n1 (Samsung SSD 9100 PRO 4TB), 3.64 TiB (`4,000,787,030,016 bytes`), transport nvme, rotational=false, readOnly=false |
## Scenario Model
@@ -58,49 +58,49 @@
| Scenario | Kind | Baseline p50 ms | Current p50 ms | Baseline p90 ms | Current p90 ms | Baseline p95 ms | Current p95 ms |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
-| open-file-default-save-rescan-visible-tree | user-flow | 77.03 | 1.5 | 89.7 | 1.76 | 90.8 | 2.04 |
-| open-file-custom-save-rescan-visible-tree | user-flow | 78.88 | 2.52 | 82.55 | 2.57 | 84.69 | 2.66 |
-| tree-view-cycle-visible-tree | user-flow | 7.88 | 0.32 | 8.68 | 0.4 | 8.69 | 0.42 |
-| tree-group-toggle-tags-view | user-flow | 5.48 | 0.27 | 6.11 | 0.34 | 6.53 | 0.35 |
-| tree-filter-visible-tree | user-flow | 0.33 | 0.26 | 0.36 | 0.34 | 0.36 | 0.35 |
-| tree-view-repeat-click-burst | user-flow | 0.2 | 0.22 | 0.27 | 0.24 | 0.29 | 0.29 |
-| tree-expansion-toggle-visible-tree | user-flow | 6.14 | 0.34 | 6.57 | 0.42 | 7.88 | 0.48 |
-| workspace-default-relative-rebuild-visible-tree | user-flow | 5.86 | 1.4 | 6.58 | 2.03 | 6.99 | 2.18 |
-| workspace-custom-relative-rebuild-visible-tree | user-flow | 36.72 | 3.76 | 42.26 | 4.19 | 43.54 | 6.88 |
-| visible-editor-highlight-open-file | user-flow | 16 | 1.39 | 16.74 | 1.54 | 20.38 | 1.56 |
-| visible-editor-highlight-change-open-file | user-flow | 17.24 | 1.32 | 18.55 | 1.39 | 18.86 | 2.18 |
-| visible-editor-custom-highlight-config-open-file | user-flow | 1391.93 | 2 | 1582.05 | 2.13 | 1589.83 | 2.33 |
+| open-file-default-save-rescan-visible-tree | user-flow | 115.71 | 2.49 | 122.16 | 3.27 | 123.6 | 3.3 |
+| open-file-custom-save-rescan-visible-tree | user-flow | 96.25 | 3.1 | 99.6 | 3.79 | 102.31 | 5.88 |
+| tree-view-cycle-visible-tree | user-flow | 9.86 | 0.37 | 10.86 | 0.44 | 11.43 | 0.62 |
+| tree-group-toggle-tags-view | user-flow | 7.77 | 0.32 | 8.66 | 0.41 | 9.96 | 0.55 |
+| tree-filter-visible-tree | user-flow | 0.37 | 0.33 | 0.59 | 0.47 | 0.64 | 1.07 |
+| tree-view-repeat-click-burst | user-flow | 0.27 | 0.27 | 0.32 | 0.31 | 0.37 | 0.32 |
+| tree-expansion-toggle-visible-tree | user-flow | 7.69 | 0.42 | 9.23 | 0.52 | 9.84 | 0.58 |
+| workspace-default-relative-rebuild-visible-tree | user-flow | 6.91 | 1.62 | 8.73 | 2.51 | 8.9 | 2.6 |
+| workspace-custom-relative-rebuild-visible-tree | user-flow | 48.24 | 4.82 | 51.05 | 5.2 | 51.99 | 5.99 |
+| visible-editor-highlight-open-file | user-flow | 19.64 | 1.72 | 20.6 | 2.51 | 22.6 | 2.51 |
+| visible-editor-highlight-change-open-file | user-flow | 20.14 | 1.74 | 22.3 | 2.02 | 24.41 | 2.07 |
+| visible-editor-custom-highlight-config-open-file | user-flow | 1862.82 | 2.65 | 1890.26 | 2.84 | 1929.71 | 2.96 |
## 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 |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
-| open-file-default-save-rescan-visible-tree | user-flow | 26.88 | 24.5 | 27.38 | 24.88 | 27.5 | 27.75 | 27.5 | 27.75 |
-| open-file-custom-save-rescan-visible-tree | user-flow | 27.25 | 20.75 | 27.5 | 21.13 | 27.63 | 21.25 | 27.63 | 21.25 |
-| tree-view-cycle-visible-tree | user-flow | 17.13 | 8.88 | 17.63 | 9.13 | 18.5 | 9.25 | 18.5 | 9.25 |
-| tree-group-toggle-tags-view | user-flow | 16.25 | 8.88 | 16.75 | 9 | 17.5 | 9.38 | 17.5 | 9.38 |
-| tree-filter-visible-tree | user-flow | 17.88 | 10.38 | 19.13 | 12.13 | 21.13 | 12.25 | 21.13 | 12.25 |
-| tree-view-repeat-click-burst | user-flow | 1.25 | 1.63 | 1.38 | 1.75 | 1.38 | 1.75 | 1.38 | 1.75 |
-| tree-expansion-toggle-visible-tree | user-flow | 16.5 | 8.88 | 17.25 | 9.13 | 17.5 | 9.25 | 17.5 | 9.25 |
-| workspace-default-relative-rebuild-visible-tree | user-flow | 17.5 | 9.63 | 17.75 | 9.75 | 17.88 | 9.88 | 17.88 | 9.88 |
-| workspace-custom-relative-rebuild-visible-tree | user-flow | 75.38 | 14.13 | 77.75 | 14.25 | 78.13 | 14.5 | 78.13 | 14.5 |
-| visible-editor-highlight-open-file | user-flow | 47.38 | 11.88 | 49.13 | 12.25 | 49.63 | 13.25 | 49.63 | 13.25 |
-| visible-editor-highlight-change-open-file | user-flow | 48.88 | 11.75 | 49.13 | 12 | 49.5 | 12.63 | 49.5 | 12.63 |
-| visible-editor-custom-highlight-config-open-file | user-flow | 159.88 | 14.5 | 161.25 | 14.5 | 162.5 | 14.63 | 162.5 | 14.63 |
+| open-file-default-save-rescan-visible-tree | user-flow | 26.63 | 24.38 | 27 | 24.63 | 27.25 | 27.25 | 27.25 | 27.25 |
+| open-file-custom-save-rescan-visible-tree | user-flow | 27.13 | 21.13 | 27.5 | 21.38 | 27.63 | 21.5 | 27.63 | 21.5 |
+| tree-view-cycle-visible-tree | user-flow | 17 | 9.25 | 17.38 | 9.5 | 18.38 | 9.63 | 18.38 | 9.63 |
+| tree-group-toggle-tags-view | user-flow | 16.5 | 9 | 18 | 9.38 | 18.13 | 9.38 | 18.13 | 9.38 |
+| tree-filter-visible-tree | user-flow | 17.88 | 11.75 | 19.5 | 11.88 | 19.5 | 12 | 19.5 | 12 |
+| tree-view-repeat-click-burst | user-flow | 1.25 | 1.5 | 1.38 | 1.75 | 1.38 | 1.88 | 1.38 | 1.88 |
+| tree-expansion-toggle-visible-tree | user-flow | 16.38 | 9.25 | 16.88 | 9.63 | 16.88 | 9.63 | 16.88 | 9.63 |
+| workspace-default-relative-rebuild-visible-tree | user-flow | 17.38 | 9.63 | 18 | 9.75 | 18.88 | 9.75 | 18.88 | 9.75 |
+| workspace-custom-relative-rebuild-visible-tree | user-flow | 70.75 | 13.88 | 73 | 14.25 | 74.5 | 14.5 | 74.5 | 14.5 |
+| visible-editor-highlight-open-file | user-flow | 48.38 | 11.75 | 49.25 | 12 | 49.5 | 13.48 | 49.5 | 13.48 |
+| visible-editor-highlight-change-open-file | user-flow | 48.04 | 11.63 | 49.38 | 12.23 | 49.5 | 14 | 49.5 | 14 |
+| visible-editor-custom-highlight-config-open-file | user-flow | 161 | 14.13 | 162.06 | 14.38 | 164.5 | 14.38 | 164.5 | 14.38 |
## 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 |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
-| open-file-default-save-rescan-visible-tree | user-flow | 82.65 | 80.1 | 82.84 | 80.27 | 83.1 | 83.45 | 83.1 | 83.45 |
-| open-file-custom-save-rescan-visible-tree | user-flow | 82.85 | 76.57 | 83.18 | 76.71 | 83.31 | 76.89 | 83.31 | 76.89 |
-| tree-view-cycle-visible-tree | user-flow | 72.71 | 64.59 | 73.24 | 64.73 | 73.8 | 64.85 | 73.8 | 64.85 |
-| tree-group-toggle-tags-view | user-flow | 71.94 | 64.44 | 72.46 | 64.58 | 73.19 | 64.76 | 73.19 | 64.76 |
-| tree-filter-visible-tree | user-flow | 73.43 | 65.96 | 74.75 | 67.77 | 76.41 | 67.93 | 76.41 | 67.93 |
-| tree-view-repeat-click-burst | user-flow | 56.86 | 57.24 | 57.06 | 57.33 | 57.11 | 57.39 | 57.11 | 57.39 |
-| tree-expansion-toggle-visible-tree | user-flow | 72.05 | 64.54 | 72.58 | 64.63 | 73.08 | 64.8 | 73.08 | 64.8 |
-| workspace-default-relative-rebuild-visible-tree | user-flow | 72.94 | 65.17 | 73.26 | 65.41 | 73.38 | 65.42 | 73.38 | 65.42 |
-| workspace-custom-relative-rebuild-visible-tree | user-flow | 130.97 | 69.71 | 133.17 | 70.02 | 133.31 | 70.02 | 133.31 | 70.02 |
-| visible-editor-highlight-open-file | user-flow | 102.91 | 67.49 | 104.57 | 67.79 | 105.26 | 68.8 | 105.26 | 68.8 |
-| visible-editor-highlight-change-open-file | user-flow | 104.37 | 67.29 | 104.55 | 67.59 | 105.02 | 68.04 | 105.02 | 68.04 |
-| visible-editor-custom-highlight-config-open-file | user-flow | 215.58 | 69.98 | 216.96 | 70.09 | 218.18 | 70.32 | 218.18 | 70.32 |
+| open-file-default-save-rescan-visible-tree | user-flow | 82 | 79.77 | 82.34 | 80.32 | 82.95 | 82.22 | 82.95 | 82.22 |
+| open-file-custom-save-rescan-visible-tree | user-flow | 82.65 | 76.59 | 82.82 | 76.82 | 83.04 | 76.91 | 83.04 | 76.91 |
+| tree-view-cycle-visible-tree | user-flow | 72.63 | 64.8 | 73.04 | 64.87 | 73.59 | 64.96 | 73.59 | 64.96 |
+| tree-group-toggle-tags-view | user-flow | 71.94 | 64.48 | 73.34 | 64.71 | 73.7 | 64.71 | 73.7 | 64.71 |
+| tree-filter-visible-tree | user-flow | 73.21 | 66.73 | 74.86 | 67.44 | 74.86 | 67.52 | 74.86 | 67.52 |
+| tree-view-repeat-click-burst | user-flow | 56.47 | 57.13 | 56.77 | 57.21 | 56.91 | 57.29 | 56.91 | 57.29 |
+| tree-expansion-toggle-visible-tree | user-flow | 71.95 | 64.77 | 72.25 | 64.97 | 72.26 | 65.19 | 72.26 | 65.19 |
+| workspace-default-relative-rebuild-visible-tree | user-flow | 72.64 | 65.09 | 73.39 | 65.4 | 74.23 | 65.44 | 74.23 | 65.44 |
+| workspace-custom-relative-rebuild-visible-tree | user-flow | 126.12 | 69.31 | 128.63 | 69.67 | 129.95 | 69.71 | 129.95 | 69.71 |
+| visible-editor-highlight-open-file | user-flow | 103.85 | 67.17 | 104.78 | 67.55 | 105.05 | 69 | 105.05 | 69 |
+| visible-editor-highlight-change-open-file | user-flow | 103.75 | 67.05 | 104.75 | 67.55 | 104.82 | 69.15 | 104.82 | 69.15 |
+| visible-editor-custom-highlight-config-open-file | user-flow | 216.29 | 69.63 | 217.63 | 69.94 | 219.99 | 70.31 | 219.99 | 70.31 |
diff --git a/package.nls.json b/package.nls.json
index 82f5b1b5..bdae9469 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -111,8 +111,8 @@
"better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownEnumDescriptions.4": "Use the Files:Exclude and the Search:Exclude setting",
"todo-tree.configuration.general": "General",
"better-todo-tree.configuration.general": "General",
- "todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "Polling interval (in seconds) for automatically refreshing the tree when your repository is updated. Set to '0' to disable.",
- "better-todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "Polling interval (in seconds) for automatically refreshing the tree when your repository is updated. Set to '0' to disable.",
+ "todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "Polling interval in seconds for automatic tree refresh after repository changes. Set to `0` to disable.",
+ "better-todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "Polling interval in seconds for automatic tree refresh after repository changes. Set to `0` to disable.",
"todo-tree.configuration.general.debug.markdownDescription": "Create a debug channel in the Output view.",
"better-todo-tree.configuration.general.debug.markdownDescription": "Create a debug channel in the Output view.",
"todo-tree.configuration.general.enableFileWatcher.markdownDescription": "Set to true to enable automatic updates when files in the workspace are created, changed or deleted.",
@@ -167,10 +167,10 @@
"better-todo-tree.configuration.highlights": "Highlights",
"todo-tree.configuration.highlights.backgroundColourScheme.markdownDescription": "A list of colours which is applied to tag highlights in the same order as the tags. Repeats if necessary and is overridden by `better-todo-tree.highlights.customHighlight`.",
"better-todo-tree.configuration.highlights.backgroundColourScheme.markdownDescription": "A list of colours which is applied to tag highlights in the same order as the tags. Repeats if necessary and is overridden by `better-todo-tree.highlights.customHighlight`.",
- "todo-tree.configuration.highlights.customHighlight.markdownDescription": "Custom configuration for highlighting, [Read more...](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
- "better-todo-tree.configuration.highlights.customHighlight.markdownDescription": "Custom configuration for highlighting, [Read more...](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
- "todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "Default configuration for highlighting. [Read more...](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
- "better-todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "Default configuration for highlighting. [Read more...](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
+ "todo-tree.configuration.highlights.customHighlight.markdownDescription": "Custom highlight configuration. [Docs](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
+ "better-todo-tree.configuration.highlights.customHighlight.markdownDescription": "Custom highlight configuration. [Docs](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
+ "todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "Default highlight configuration. [Docs](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
+ "better-todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "Default highlight configuration. [Docs](https://github.com/FanaticPythoner/better-todo-tree#highlighting).",
"todo-tree.configuration.highlights.enabled.markdownDescription": "Set to false to disable highlighting.",
"better-todo-tree.configuration.highlights.enabled.markdownDescription": "Set to false to disable highlighting.",
"todo-tree.configuration.highlights.foregroundColourScheme.markdownDescription": "A list of colours which is applied to tag highlights in the same order as the tags. Repeats if necessary and is overridden by `better-todo-tree.highlights.customHighlight`.",
@@ -183,8 +183,8 @@
"better-todo-tree.configuration.regex": "Regex",
"todo-tree.configuration.regex.enableMultiLine": "Force the regex to match over multiple lines. Allows use of `[\\s\\S]` to match anything including newlines.",
"better-todo-tree.configuration.regex.enableMultiLine": "Force the regex to match over multiple lines. Allows use of `[\\s\\S]` to match anything including newlines.",
- "todo-tree.configuration.regex.regex.markdownDescription": "Regular expression for matching TODOs. **($TAGS)** is replaced by the expanded tag list. Prefix-only custom regexes that contain **($TAGS)** render text from the detected tag to the end of the physical line.",
- "better-todo-tree.configuration.regex.regex.markdownDescription": "Regular expression for matching TODOs. **($TAGS)** is replaced by the expanded tag list. Prefix-only custom regexes that contain **($TAGS)** render text from the detected tag to the end of the physical line.",
+ "todo-tree.configuration.regex.regex.markdownDescription": "Regular expression for matching TODOs. **($TAGS)** is replaced by the expanded tag list. Prefix-only custom regexes that contain **($TAGS)** render text from the detected tag to the end of the physical line. Look-around and backreference patterns use PCRE2 automatically when workspace search requires it.",
+ "better-todo-tree.configuration.regex.regex.markdownDescription": "Regular expression for matching TODOs. **($TAGS)** is replaced by the expanded tag list. Prefix-only custom regexes that contain **($TAGS)** render text from the detected tag to the end of the physical line. Look-around and backreference patterns use PCRE2 automatically when workspace search requires it.",
"todo-tree.configuration.regex.regexCaseSensitive.markdownDescription": "Use a case sensitive regular expression.",
"better-todo-tree.configuration.regex.regexCaseSensitive.markdownDescription": "Use a case sensitive regular expression.",
"todo-tree.configuration.regex.subTagRegex.markdownDescription": "Regular expression for processing the text to the right of the tag, e.g. for extracting a sub tag, or removing unwanted characters.",
@@ -193,12 +193,12 @@
"better-todo-tree.configuration.ripgrep": "Ripgrep",
"todo-tree.configuration.ripgrep.ripgrep.markdownDescription": "Custom ripgrep executable path. Empty value uses the packaged ripgrep binary.",
"better-todo-tree.configuration.ripgrep.ripgrep.markdownDescription": "Custom ripgrep executable path. Empty value uses the packaged ripgrep binary.",
- "todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "Additional arguments to pass to ripgrep (Use with caution!).",
- "better-todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "Additional arguments to pass to ripgrep (Use with caution!).",
+ "todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "Additional arguments to pass to ripgrep. Engine arguments such as `--pcre2`, `-P`, and `--engine` override automatic engine selection.",
+ "better-todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "Additional arguments to pass to ripgrep. Engine arguments such as `--pcre2`, `-P`, and `--engine` override automatic engine selection.",
"todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "Size of the buffer to use for retrieving output from ripgrep (kilobytes).",
"better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "Size of the buffer to use for retrieving output from ripgrep (kilobytes).",
- "todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "A pattern file is used with ripgrep by default. If you experience issues with deleting the pattern file, set this to false to use the legacy method of providing the regex to ripgrep.",
- "better-todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "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.",
+ "todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "A pattern file is used with ripgrep by default. Set to false to use the legacy regex argument mode.",
+ "better-todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "A pattern file is used with ripgrep by default. Set to false to use the legacy regex argument mode.",
"todo-tree.configuration.title": "Better Todo Tree",
"better-todo-tree.configuration.title": "Better Todo Tree",
"todo-tree.configuration.tree": "Tree",
@@ -227,8 +227,8 @@
"better-todo-tree.configuration.tree.disableCompactFolders.markdownDescription": "Prevent the tree from showing compact folders.",
"todo-tree.configuration.tree.expanded.markdownDescription": "When opening new workspaces, show the tree initially fully expanded.",
"better-todo-tree.configuration.tree.expanded.markdownDescription": "When opening new workspaces, show the tree initially fully expanded.",
- "todo-tree.configuration.tree.filterCaseSensitive.markdownDescription": "Set to true if you want the view filtering to be case sensitive.",
- "better-todo-tree.configuration.tree.filterCaseSensitive.markdownDescription": "Set to true if you want the view filtering to be case sensitive.",
+ "todo-tree.configuration.tree.filterCaseSensitive.markdownDescription": "Set to true for case-sensitive view filtering.",
+ "better-todo-tree.configuration.tree.filterCaseSensitive.markdownDescription": "Set to true for case-sensitive view filtering.",
"todo-tree.configuration.tree.flat.markdownDescription": "When opening new workspaces, show the tree initially as flat list of files.",
"better-todo-tree.configuration.tree.flat.markdownDescription": "When opening new workspaces, show the tree initially as flat list of files.",
"todo-tree.configuration.tree.groupedBySubTag.markdownDescription": "When opening new workspaces, show the tree initially grouped by sub tag.",
@@ -241,8 +241,8 @@
"better-todo-tree.configuration.tree.hideTreeWhenEmpty.markdownDescription": "Hide the view if it is empty.",
"todo-tree.configuration.tree.labelFormat.markdownDescription": "Format for tree items.",
"better-todo-tree.configuration.tree.labelFormat.markdownDescription": "Format for tree items.",
- "todo-tree.configuration.tree.scanAtStartup.markdownDescription": "Normally the tree is built as soon as the window is opened. If you have a large code base and want to manually start the scan, set this to false.",
- "better-todo-tree.configuration.tree.scanAtStartup.markdownDescription": "Normally the tree is built as soon as the window is opened. If you have a large code base and want to manually start the scan, set this to false.",
+ "todo-tree.configuration.tree.scanAtStartup.markdownDescription": "Build the tree when the window opens. Set to false for manual scans in large workspaces.",
+ "better-todo-tree.configuration.tree.scanAtStartup.markdownDescription": "Build the tree when the window opens. Set to false for manual scans in large workspaces.",
"todo-tree.configuration.tree.scanMode.markdownDescription": "Set this to change which files are scanned.",
"better-todo-tree.configuration.tree.scanMode.markdownDescription": "Set this to change which files are scanned.",
"todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.1": "Scan the whole workspace (or workspaces) and open file",
@@ -265,8 +265,8 @@
"better-todo-tree.configuration.tree.showScanOpenFilesOrWorkspaceButton.deprecationMessage": "This setting is no longer used, please use \"better-todo-tree.tree.scanMode\" instead.",
"todo-tree.configuration.tree.showTagsFromOpenFilesOnly.deprecationMessage": "This setting is no longer used, please use \"better-todo-tree.tree.scanMode\" instead.",
"better-todo-tree.configuration.tree.showTagsFromOpenFilesOnly.deprecationMessage": "This setting is no longer used, please use \"better-todo-tree.tree.scanMode\" instead.",
- "todo-tree.configuration.tree.sort.markdownDescription": "ripgrep searches using multiple threads to improve performance. The tree is sorted when it is populated so that it stays stable. If you want to use ripgrep's own sort arguments, set this to false.",
- "better-todo-tree.configuration.tree.sort.markdownDescription": "ripgrep searches using multiple threads to improve performance. The tree is sorted when it is populated so that it stays stable. If you want to use ripgrep's own sort arguments, set this to false.",
+ "todo-tree.configuration.tree.sort.markdownDescription": "ripgrep searches with multiple threads. The tree is sorted after population for stable display. Set to false for ripgrep sort arguments.",
+ "better-todo-tree.configuration.tree.sort.markdownDescription": "ripgrep searches with multiple threads. The tree is sorted after population for stable display. Set to false for ripgrep sort arguments.",
"todo-tree.configuration.tree.sortTagsOnlyViewAlphabetically.markdownDescription": "Sort items in the tags only view alphabetically instead of by file and line number.",
"better-todo-tree.configuration.tree.sortTagsOnlyViewAlphabetically.markdownDescription": "Sort items in the tags only view alphabetically instead of by file and line number.",
"todo-tree.configuration.tree.subTagClickUrl.markdownDescription": "The URL to open when clicking on a sub tag in the tree. Can include placeholders as defined in `better-todo-tree.tree.labelFormat`.",
diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json
index 98277217..d4cf4db8 100644
--- a/package.nls.zh-cn.json
+++ b/package.nls.zh-cn.json
@@ -111,8 +111,8 @@
"better-todo-tree.configuration.filtering.useBuiltInExcludes.markdownEnumDescriptions.4": "使用 Files:Exclude 和 Search:Exclude 设置。",
"todo-tree.configuration.general": "常规",
"better-todo-tree.configuration.general": "常规",
- "todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "Polling interval (in seconds) for automatically refreshing the tree when your repository is updated. Set to '0' to disable.",
- "better-todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "Polling interval (in seconds) for automatically refreshing the tree when your repository is updated. Set to '0' to disable.",
+ "todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "仓库变更后自动刷新树的轮询间隔(秒)。设为 `0` 可禁用。",
+ "better-todo-tree.configuration.general.automaticGitRefreshInterval.markdownDescription": "仓库变更后自动刷新树的轮询间隔(秒)。设为 `0` 可禁用。",
"todo-tree.configuration.general.debug.markdownDescription": "在输出视图中生成一个调试通道。",
"better-todo-tree.configuration.general.debug.markdownDescription": "在输出视图中生成一个调试通道。",
"todo-tree.configuration.general.enableFileWatcher.markdownDescription": "在工作区中创建文件、改变文件或删除文件时自动更新标签。",
@@ -167,10 +167,10 @@
"better-todo-tree.configuration.highlights": "突出",
"todo-tree.configuration.highlights.backgroundColourScheme.markdownDescription": "应用于标签突出显示的颜色列表。标签突出显示的颜色由标签顺序决定,颜色在必要时可以重复,并会被 `better-todo-tree.highlights.customHighlight` 中的设置覆盖。",
"better-todo-tree.configuration.highlights.backgroundColourScheme.markdownDescription": "应用于标签突出显示的颜色列表。标签突出显示的颜色由标签顺序决定,颜色在必要时可以重复,并会被 `better-todo-tree.highlights.customHighlight` 中的设置覆盖。",
- "todo-tree.configuration.highlights.customHighlight.markdownDescription": "配置自定义突出显示,[阅读更多...](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
- "better-todo-tree.configuration.highlights.customHighlight.markdownDescription": "配置自定义突出显示,[阅读更多...](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
- "todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "配置默认突出显示,[阅读更多...](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
- "better-todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "配置默认突出显示,[阅读更多...](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
+ "todo-tree.configuration.highlights.customHighlight.markdownDescription": "自定义高亮配置。[文档](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
+ "better-todo-tree.configuration.highlights.customHighlight.markdownDescription": "自定义高亮配置。[文档](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
+ "todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "默认高亮配置。[文档](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
+ "better-todo-tree.configuration.highlights.defaultHighlight.markdownDescription": "默认高亮配置。[文档](https://github.com/FanaticPythoner/better-todo-tree#highlighting)",
"todo-tree.configuration.highlights.enabled.markdownDescription": "启用突出显示。",
"better-todo-tree.configuration.highlights.enabled.markdownDescription": "启用突出显示。",
"todo-tree.configuration.highlights.foregroundColourScheme.markdownDescription": "应用于标签突出显示的颜色列表。标签突出显示的颜色由标签顺序决定,颜色在必要时可以重复,并会被 `better-todo-tree.highlights.customHighlight` 中的设置覆盖。",
@@ -183,8 +183,8 @@
"better-todo-tree.configuration.regex": "雷格克斯",
"todo-tree.configuration.regex.enableMultiLine": "Force the regex to match over multiple lines. Allows use of `[\\s\\S]` to match anything including newlines.",
"better-todo-tree.configuration.regex.enableMultiLine": "Force the regex to match over multiple lines. Allows use of `[\\s\\S]` to match anything including newlines.",
- "todo-tree.configuration.regex.regex.markdownDescription": "控制匹配待办事项标签的 Rust 正则表达式。`$TAGS` 会被标签列表内的项替换。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。",
- "better-todo-tree.configuration.regex.regex.markdownDescription": "控制匹配待办事项标签的 Rust 正则表达式。`$TAGS` 会被标签列表内的项替换。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。",
+ "todo-tree.configuration.regex.regex.markdownDescription": "用于匹配待办事项的正则表达式。`$TAGS` 会替换为展开后的标签列表。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。工作区搜索需要环视或反向引用时会自动使用 PCRE2。",
+ "better-todo-tree.configuration.regex.regex.markdownDescription": "用于匹配待办事项的正则表达式。`$TAGS` 会替换为展开后的标签列表。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。工作区搜索需要环视或反向引用时会自动使用 PCRE2。",
"todo-tree.configuration.regex.regexCaseSensitive.markdownDescription": "使用区分大小写的正则表达式。",
"better-todo-tree.configuration.regex.regexCaseSensitive.markdownDescription": "使用区分大小写的正则表达式。",
"todo-tree.configuration.regex.subTagRegex.markdownDescription": "Regular expression for processing the text to the right of the tag, e.g. for extracting a sub tag, or removing unwanted characters.",
@@ -193,8 +193,8 @@
"better-todo-tree.configuration.ripgrep": "Ripgrep",
"todo-tree.configuration.ripgrep.ripgrep.markdownDescription": "自定义 ripgrep 可执行文件路径。空值使用随扩展打包的 ripgrep。",
"better-todo-tree.configuration.ripgrep.ripgrep.markdownDescription": "自定义 ripgrep 可执行文件路径。空值使用随扩展打包的 ripgrep。",
- "todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "配置要传递给 ripgrep 的额外参数(谨慎使用)。",
- "better-todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "配置要传递给 ripgrep 的额外参数(谨慎使用)。",
+ "todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "传递给 ripgrep 的额外参数。`--pcre2`、`-P` 和 `--engine` 等引擎参数会覆盖自动引擎选择。",
+ "better-todo-tree.configuration.ripgrep.ripgrepArgs.markdownDescription": "传递给 ripgrep 的额外参数。`--pcre2`、`-P` 和 `--engine` 等引擎参数会覆盖自动引擎选择。",
"todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "配置 ripgrep 缓冲区的大小(KB)。当目前缓冲区大小不足以查看所有标签时,可适当增加缓冲区大小。",
"better-todo-tree.configuration.ripgrep.ripgrepMaxBuffer.markdownDescription": "配置 ripgrep 缓冲区的大小(KB)。当目前缓冲区大小不足以查看所有标签时,可适当增加缓冲区大小。",
"todo-tree.configuration.ripgrep.usePatternFile.markdownDescription": "默认情况下,模板文件与 ripgrep 一起使用。当删除模板文件时遇到问题,请将此项设置为 false,即可使用向 ripgrep 提供正则表达式的旧方法。",
diff --git a/scripts/perf/issue42-regex-engine.js b/scripts/perf/issue42-regex-engine.js
new file mode 100644
index 00000000..77d43ff1
--- /dev/null
+++ b/scripts/perf/issue42-regex-engine.js
@@ -0,0 +1,334 @@
+/* jshint esversion:6, node: true */
+
+'use strict';
+
+var childProcess = require( 'child_process' );
+var fs = require( 'fs' );
+var os = require( 'os' );
+var path = require( 'path' );
+var performance = require( 'perf_hooks' ).performance;
+
+var detection = require( '../../src/detection.js' );
+var utils = require( '../../src/utils.js' );
+
+var TAGS = Object.freeze( [ 'BUG', 'HACK', 'FIXME', 'TODO', 'XXX', '[ ]', '[x]' ] );
+var FILE_COUNT = 160;
+var TASKS_PER_FILE = 8;
+var ITERATIONS = 31;
+var DEFAULT_DERIVED_REGEX = utils.DEFAULT_REGEX_SOURCE.replace( '|;', '' );
+
+var resourceConfig = {
+ tags: TAGS.slice(),
+ regex: DEFAULT_DERIVED_REGEX,
+ regexCaseSensitive: true,
+ enableMultiLine: false,
+ subTagRegex: '^:\\s*',
+ isDefaultRegex: false
+};
+var config = {
+ tags: function() { return TAGS.slice(); },
+ regex: function()
+ {
+ return {
+ tags: TAGS.slice(),
+ regex: DEFAULT_DERIVED_REGEX,
+ caseSensitive: true,
+ multiLine: false
+ };
+ },
+ subTagRegex: function() { return '^:\\s*'; }
+};
+var snapshot = {
+ getResourceConfig: function() { return resourceConfig; }
+};
+
+function resolveRipgrepPath()
+{
+ var executableName = process.platform === 'win32' ? 'rg.exe' : 'rg';
+ var platformArch = process.platform + '-' + process.arch;
+ var rgPath = path.resolve(
+ __dirname,
+ '..',
+ '..',
+ 'node_modules',
+ '@vscode',
+ 'ripgrep-universal',
+ 'bin',
+ platformArch,
+ executableName
+ );
+
+ if( fs.existsSync( rgPath ) !== true )
+ {
+ throw new Error( 'ripgrep benchmark binary missing: ' + rgPath );
+ }
+
+ return rgPath;
+}
+
+function createUri( filePath )
+{
+ return {
+ scheme: 'file',
+ fsPath: filePath,
+ path: filePath,
+ toString: function() { return 'file://' + filePath; }
+ };
+}
+
+function createCorpus( root )
+{
+ var fileIndex;
+ var taskIndex;
+
+ for( fileIndex = 0; fileIndex < FILE_COUNT; fileIndex++ )
+ {
+ var lines = [];
+
+ for( taskIndex = 0; taskIndex < TASKS_PER_FILE; taskIndex++ )
+ {
+ lines.push( '- [ ] Task ' + fileIndex + '-' + taskIndex );
+ lines.push( 'plain text ' + fileIndex + '-' + taskIndex );
+ }
+
+ fs.writeFileSync( path.join( root, 'todo-' + fileIndex + '.md' ), lines.join( '\n' ) + '\n', 'utf8' );
+ }
+}
+
+function runRipgrep( rgPath, root, args )
+{
+ return childProcess.spawnSync( rgPath, args, { cwd: root, encoding: 'utf8' } );
+}
+
+function decodeJsonValue( value )
+{
+ if( value === undefined || value === null )
+ {
+ return undefined;
+ }
+
+ if( value.text !== undefined )
+ {
+ return value.text;
+ }
+
+ if( value.bytes !== undefined )
+ {
+ return Buffer.from( value.bytes, 'base64' ).toString( 'utf8' );
+ }
+
+ return undefined;
+}
+
+function parseJsonLines( stdout )
+{
+ return stdout.split( '\n' ).filter( function( line )
+ {
+ return line.length > 0;
+ } ).map( function( line )
+ {
+ return JSON.parse( line );
+ } );
+}
+
+function resolveResultPath( root, value )
+{
+ var filePath = decodeJsonValue( value );
+
+ return path.isAbsolute( filePath ) ? filePath : path.resolve( root, filePath );
+}
+
+function toWorkspaceMatch( root, message )
+{
+ var data = message.data;
+ var submatches = Array.isArray( data.submatches ) ? data.submatches.map( function( submatch )
+ {
+ return {
+ match: decodeJsonValue( submatch.match ),
+ start: submatch.start,
+ end: submatch.end
+ };
+ } ) : [];
+ var firstSubmatch = submatches[ 0 ] || { start: 0, match: decodeJsonValue( data.lines ) || '' };
+
+ return {
+ fsPath: resolveResultPath( root, data.path ),
+ line: data.line_number,
+ column: firstSubmatch.start + 1,
+ match: firstSubmatch.match,
+ absoluteOffset: data.absolute_offset,
+ submatches: submatches,
+ lines: decodeJsonValue( data.lines )
+ };
+}
+
+function quantile( values, q )
+{
+ var sorted = values.slice().sort( function( a, b ) { return a - b; } );
+ var index = Math.ceil( q * sorted.length ) - 1;
+
+ return sorted[ Math.max( 0, Math.min( sorted.length - 1, index ) ) ];
+}
+
+function pcre2RawStrategy( rgPath, root, fullRegex )
+{
+ var result = runRipgrep( rgPath, root, [ '--no-messages', '--json', '--color', 'never', '--pcre2', '-e', fullRegex, '.' ] );
+ var matchesByFile = new Map();
+ var count = 0;
+
+ if( result.status !== 0 )
+ {
+ throw new Error( result.stderr.trim() );
+ }
+
+ parseJsonLines( result.stdout ).filter( function( message )
+ {
+ return message.type === 'match';
+ } ).forEach( function( message )
+ {
+ var match = toWorkspaceMatch( root, message );
+
+ if( matchesByFile.has( match.fsPath ) !== true )
+ {
+ matchesByFile.set( match.fsPath, [] );
+ }
+
+ matchesByFile.get( match.fsPath ).push( match );
+ } );
+
+ matchesByFile.forEach( function( matches, filePath )
+ {
+ matches.map( function( match )
+ {
+ return detection.normalizeWorkspaceRegexMatch( createUri( filePath ), match, snapshot );
+ } ).filter( function( normalized )
+ {
+ return normalized !== undefined;
+ } ).forEach( function()
+ {
+ count++;
+ } );
+ } );
+
+ return count;
+}
+
+function candidateStrategy( rgPath, root, candidateRegex )
+{
+ var result = runRipgrep( rgPath, root, [ '--no-messages', '--json', '--color', 'never', '-e', candidateRegex, '.' ] );
+ var files = new Set();
+ var count = 0;
+
+ if( result.status !== 0 )
+ {
+ throw new Error( result.stderr.trim() );
+ }
+
+ parseJsonLines( result.stdout ).filter( function( message )
+ {
+ return message.type === 'match';
+ } ).forEach( function( message )
+ {
+ files.add( resolveResultPath( root, message.data.path ) );
+ } );
+
+ files.forEach( function( filePath )
+ {
+ count += detection.scanText( createUri( filePath ), fs.readFileSync( filePath, 'utf8' ) ).length;
+ } );
+
+ return count;
+}
+
+function measure( name, expectedMatches, fn )
+{
+ var durations = [];
+ var count = 0;
+ var peakRss = 0;
+ var index;
+
+ for( index = 0; index < ITERATIONS; index++ )
+ {
+ if( global.gc )
+ {
+ global.gc();
+ }
+
+ var start = performance.now();
+ count = fn();
+ var duration = performance.now() - start;
+
+ if( count !== expectedMatches )
+ {
+ throw new Error( name + ' match count mismatch: expected ' + expectedMatches + ', got ' + count );
+ }
+
+ if( index > 0 )
+ {
+ durations.push( duration );
+ }
+
+ peakRss = Math.max( peakRss, process.memoryUsage().rss );
+ }
+
+ var p50 = quantile( durations, 0.50 );
+ var p95 = quantile( durations, 0.95 );
+
+ return {
+ name: name,
+ count: count,
+ p50_ms: Number( p50.toFixed( 3 ) ),
+ p95_ms: Number( p95.toFixed( 3 ) ),
+ throughput_matches_per_s: Number( ( count / ( p50 / 1000 ) ).toFixed( 1 ) ),
+ peak_rss_mib: Number( ( peakRss / 1048576 ).toFixed( 2 ) )
+ };
+}
+
+function run()
+{
+ var rgPath = resolveRipgrepPath();
+ var root = fs.mkdtempSync( path.join( os.tmpdir(), 'issue42-bench-' ) );
+ var expectedMatches = FILE_COUNT * TASKS_PER_FILE;
+ var fullRegex;
+ var candidateRegex;
+ var brokenRaw;
+
+ utils.init( config );
+ createCorpus( root );
+ fullRegex = DEFAULT_DERIVED_REGEX.replace( '($TAGS)', '(' + utils.getTagRegexSource( undefined, TAGS ) + ')' );
+ candidateRegex = '(' + utils.getTagRegexSource( undefined, TAGS ) + ')';
+
+ try
+ {
+ brokenRaw = runRipgrep( rgPath, root, [ '--no-messages', '--json', '--color', 'never', '-e', fullRegex, '.' ] );
+
+ return {
+ corpus: {
+ fileCount: FILE_COUNT,
+ tasksPerFile: TASKS_PER_FILE,
+ expectedMatches: expectedMatches
+ },
+ brokenRawExit: brokenRaw.status,
+ brokenRawHasLookaroundError: /look-around/.test( brokenRaw.stderr ),
+ pcre2Raw: measure( 'pcre2Raw', expectedMatches, function()
+ {
+ return pcre2RawStrategy( rgPath, root, fullRegex );
+ } ),
+ candidateRoute: measure( 'candidateRoute', expectedMatches, function()
+ {
+ return candidateStrategy( rgPath, root, candidateRegex );
+ } )
+ };
+ }
+ finally
+ {
+ fs.rmSync( root, { recursive: true, force: true } );
+ }
+}
+
+if( require.main === module )
+{
+ process.stdout.write( JSON.stringify( run(), null, 2 ) + '\n' );
+}
+
+module.exports.run = run;
diff --git a/src/detection.js b/src/detection.js
index a1b298ad..4c19caec 100644
--- a/src/detection.js
+++ b/src/detection.js
@@ -1,5 +1,6 @@
var path = require( 'path' );
+var regexEngine = require( './regexEngine.js' );
var utils = require( './utils.js' );
function getUriFsPath( uri )
@@ -671,6 +672,7 @@ function createScanContext( uri, text, snapshot, options )
resolveResourceConfig( uri );
var flags = resourceConfig.regexCaseSensitive === true ? '' : 'i';
var regexSource = options.regexSource || utils.getRegexSource( uri );
+ var skipExactRegex = resourceConfig.isDefaultRegex === true || options.skipExactRegex === true;
var tagRegex = resourceConfig.regex.indexOf( "$TAGS" ) > -1 ?
new RegExp( '(' + utils.getTagRegexSource( uri, resourceConfig.tags ) + ')', flags ) :
undefined;
@@ -683,7 +685,7 @@ function createScanContext( uri, text, snapshot, options )
resourceConfig: resourceConfig,
lineOffsets: createLineOffsets( text ),
regexSource: regexSource,
- exactRegex: resourceConfig.isDefaultRegex === true ? undefined : utils.getRegexForEditorSearch( true, uri, {
+ exactRegex: skipExactRegex === true ? undefined : utils.getRegexForEditorSearch( true, uri, {
includeIndices: true,
resourceConfig: resourceConfig,
regexSource: regexSource
@@ -1282,7 +1284,13 @@ function normalizeWorkspaceRegexMatch( uri, match, snapshot )
var localMatchStart = match.submatches && match.submatches.length > 0 && typeof match.submatches[ 0 ].start === 'number' ?
match.submatches[ 0 ].start :
Math.max( ( match.column || 1 ) - 1, 0 );
- var context = createScanContext( uri, contextText, snapshot );
+ var resourceConfig = snapshot && typeof ( snapshot.getResourceConfig ) === 'function' ?
+ snapshot.getResourceConfig( uri ) :
+ resolveResourceConfig( uri );
+ var skipExactRegex = regexEngine.containsJavaScriptIncompatibleBackreference( resourceConfig.regex ) === true;
+ var context = createScanContext( uri, contextText, snapshot, {
+ skipExactRegex: skipExactRegex
+ } );
var exactMatch = findExactRegexExecMatch( context, localMatchStart );
var normalized = normalizeRegexExecMatchWithContext(
context,
diff --git a/src/extension.js b/src/extension.js
index 20c17539..2ba79beb 100644
--- a/src/extension.js
+++ b/src/extension.js
@@ -23,6 +23,7 @@ var identity = require( './extensionIdentity.js' );
var settingsSnapshotModule = require( './runtime/settingsSnapshot.js' );
var documentScanCacheModule = require( './runtime/documentScanCache.js' );
var streamScanner = require( './runtime/streamScanner.js' );
+var regexEngine = require( './regexEngine.js' );
var packageJson = require( '../package.json' );
var searchList = [];
@@ -2219,7 +2220,7 @@ function activate( context )
assertGenerationActive( generation );
beginScanRoot( generation, entry );
- var scanPromise = workspaceConfig.isDefaultRegex === true ?
+ var scanPromise = regexEngine.shouldUseTagCandidateScan( workspaceConfig ) === true ?
scanWorkspaceCandidates( entry, generation, store ) :
scanWorkspaceRegexMatches( entry, generation, store );
diff --git a/src/regexEngine.js b/src/regexEngine.js
new file mode 100644
index 00000000..1a46e73a
--- /dev/null
+++ b/src/regexEngine.js
@@ -0,0 +1,203 @@
+/* jshint esversion:6, node: true */
+
+'use strict';
+
+var BACKREFERENCE_DELIMITERS = "<'{";
+
+function isEscaped( source, index )
+{
+ var slashCount = 0;
+ var cursor = index - 1;
+
+ while( cursor >= 0 && source[ cursor ] === '\\' )
+ {
+ slashCount++;
+ cursor--;
+ }
+
+ return slashCount % 2 === 1;
+}
+
+function scanRegexSource( source, visitors )
+{
+ var inCharacterClass = false;
+ var index;
+
+ for( index = 0; index < source.length; index++ )
+ {
+ if( isEscaped( source, index ) )
+ {
+ continue;
+ }
+
+ if( source[ index ] === '[' )
+ {
+ inCharacterClass = true;
+ continue;
+ }
+
+ if( source[ index ] === ']' )
+ {
+ inCharacterClass = false;
+ continue;
+ }
+
+ if( inCharacterClass === true )
+ {
+ continue;
+ }
+
+ if( visitors( source, index ) === true )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function containsLookAround( source )
+{
+ if( typeof source !== 'string' || source.length === 0 )
+ {
+ return false;
+ }
+
+ return scanRegexSource( source, function( value, index )
+ {
+ return value[ index ] === '(' &&
+ value[ index + 1 ] === '?' &&
+ (
+ value[ index + 2 ] === '=' ||
+ value[ index + 2 ] === '!' ||
+ (
+ value[ index + 2 ] === '<' &&
+ ( value[ index + 3 ] === '=' || value[ index + 3 ] === '!' )
+ )
+ );
+ } );
+}
+
+function containsBackreference( source )
+{
+ if( typeof source !== 'string' || source.length === 0 )
+ {
+ return false;
+ }
+
+ return scanRegexSource( source, function( value, index )
+ {
+ if( containsPythonStyleNamedBackreferenceAt( value, index ) === true )
+ {
+ return true;
+ }
+
+ if( value[ index ] !== '\\' )
+ {
+ return false;
+ }
+
+ return /[1-9]/.test( value[ index + 1 ] || '' ) ||
+ hasDelimitedBackreferenceAt( value, index, 'k', BACKREFERENCE_DELIMITERS ) ||
+ hasDelimitedBackreferenceAt( value, index, 'g', BACKREFERENCE_DELIMITERS );
+ } );
+}
+
+function hasDelimitedBackreferenceAt( value, index, prefix, delimiters )
+{
+ return value[ index + 1 ] === prefix && delimiters.indexOf( value[ index + 2 ] || '' ) !== -1;
+}
+
+function containsPythonStyleNamedBackreferenceAt( value, index )
+{
+ return value[ index ] === '(' &&
+ value[ index + 1 ] === '?' &&
+ value[ index + 2 ] === 'P' &&
+ value[ index + 3 ] === '=';
+}
+
+function containsJavaScriptIncompatibleBackreference( source )
+{
+ if( typeof source !== 'string' || source.length === 0 )
+ {
+ return false;
+ }
+
+ return scanRegexSource( source, function( value, index )
+ {
+ if( containsPythonStyleNamedBackreferenceAt( value, index ) === true )
+ {
+ return true;
+ }
+
+ if( value[ index ] !== '\\' )
+ {
+ return false;
+ }
+
+ return ( value[ index + 1 ] === 'k' && value[ index + 2 ] === '{' ) ||
+ hasDelimitedBackreferenceAt( value, index, 'g', BACKREFERENCE_DELIMITERS );
+ } );
+}
+
+function requiresPcre2( source )
+{
+ if( typeof source !== 'string' || source.length === 0 )
+ {
+ return false;
+ }
+
+ return containsLookAround( source ) || containsBackreference( source );
+}
+
+function hasTagPlaceholder( source )
+{
+ return typeof source === 'string' && source.indexOf( '$TAGS' ) !== -1;
+}
+
+function hasRipgrepEngineArg( args )
+{
+ return ( args || [] ).some( function( arg, index )
+ {
+ return arg === '--pcre2' ||
+ arg === '-P' ||
+ arg === '--auto-hybrid-regex' ||
+ arg === '--engine' ||
+ arg.indexOf( '--engine=' ) === 0 ||
+ ( index > 0 && args[ index - 1 ] === '--engine' );
+ } );
+}
+
+function buildRegexEngineArgs( source, additionalArgs )
+{
+ if( requiresPcre2( source ) !== true || hasRipgrepEngineArg( additionalArgs ) === true )
+ {
+ return [];
+ }
+
+ return [ '--pcre2' ];
+}
+
+function shouldUseTagCandidateScan( resourceConfig )
+{
+ if( !resourceConfig )
+ {
+ return false;
+ }
+
+ return resourceConfig.isDefaultRegex === true ||
+ (
+ hasTagPlaceholder( resourceConfig.regex ) === true &&
+ requiresPcre2( resourceConfig.regex ) === true &&
+ containsJavaScriptIncompatibleBackreference( resourceConfig.regex ) !== true
+ );
+}
+
+module.exports.containsLookAround = containsLookAround;
+module.exports.containsBackreference = containsBackreference;
+module.exports.containsJavaScriptIncompatibleBackreference = containsJavaScriptIncompatibleBackreference;
+module.exports.requiresPcre2 = requiresPcre2;
+module.exports.hasTagPlaceholder = hasTagPlaceholder;
+module.exports.hasRipgrepEngineArg = hasRipgrepEngineArg;
+module.exports.buildRegexEngineArgs = buildRegexEngineArgs;
+module.exports.shouldUseTagCandidateScan = shouldUseTagCandidateScan;
diff --git a/src/ripgrep.js b/src/ripgrep.js
index 7de91748..3f14a180 100644
--- a/src/ripgrep.js
+++ b/src/ripgrep.js
@@ -5,6 +5,7 @@
var child_process = require( 'child_process' );
var fs = require( 'fs' );
+var regexEngine = require( './regexEngine.js' );
var currentProcess;
var currentCancellationRequested = false;
@@ -126,12 +127,13 @@ function parseArgumentString( input )
function buildArgs( options )
{
+ var additionalArgs = parseArgumentString( options.additional );
var args = [
'--no-messages',
'--json',
'--color',
'never'
- ].concat( parseArgumentString( options.additional ) );
+ ].concat( regexEngine.buildRegexEngineArgs( options.regex, additionalArgs ) ).concat( additionalArgs );
if( options.multiline )
{
diff --git a/test/detection.regex-matrix.test.js b/test/detection.regex-matrix.test.js
index 7c1c92cb..4699e871 100644
--- a/test/detection.regex-matrix.test.js
+++ b/test/detection.regex-matrix.test.js
@@ -132,7 +132,7 @@ QUnit.module( "detection regex matrix", function()
QUnit.test( "regexes without $TAGS use the raw match as the actual tag", function( assert )
{
- var results = scanWithConfig( '/tmp/note.js', 'NOTE important', function( config )
+ var results = scanWithConfig( '/tmp/note.js', 'NOTE relevant', function( config )
{
config.tagList = [ 'TODO' ];
config.regexSource = '(NOTE)';
@@ -432,6 +432,73 @@ QUnit.module( "detection regex matrix", function()
assert.equal( normalized.matchEndOffset, scanned.matchEndOffset );
} );
+ QUnit.test( "issue #42 PCRE2 markdown task payload keeps display text", function( assert )
+ {
+ var regexSource = utils.DEFAULT_REGEX_SOURCE.replace( '|;', '' );
+ var config = matrixHelpers.createConfig( {
+ tagList: [ 'TODO', '[ ]', '[x]' ],
+ regexSource: regexSource,
+ subTagRegexString: '^:\\s*'
+ } );
+ var uri = matrixHelpers.createUri( '/tmp/issue-42.md' );
+ var text = '- [ ] Task 1\n- [ ] Task 2\n';
+
+ utils.init( config );
+
+ var scanned = detection.scanText( uri, text );
+ var normalized = detection.normalizeWorkspaceRegexMatch( uri, {
+ fsPath: uri.fsPath,
+ line: 1,
+ column: 1,
+ match: '- [ ]',
+ lines: '- [ ] Task 1\n',
+ absoluteOffset: 0,
+ submatches: [ {
+ match: '- [ ]',
+ start: 0,
+ end: 5
+ } ]
+ } );
+
+ assert.equal( scanned[ 0 ].actualTag, '[ ]' );
+ assert.equal( scanned[ 0 ].displayText, 'Task 1' );
+ assert.equal( normalized.actualTag, scanned[ 0 ].actualTag );
+ assert.equal( normalized.displayText, scanned[ 0 ].displayText );
+ assert.equal( normalized.match, scanned[ 0 ].match );
+ } );
+
+ QUnit.test( "PCRE2-only workspace tag payload normalizes without JavaScript regex compilation", function( assert )
+ {
+ var config = matrixHelpers.createConfig( {
+ tagList: [ 'TODO' ],
+ regexSource: '($TAGS)\\s+\\g{1}',
+ subTagRegexString: '^:\\s*'
+ } );
+ var uri = matrixHelpers.createUri( '/tmp/pcre2-only.js' );
+
+ utils.init( config );
+
+ var normalized = detection.normalizeWorkspaceRegexMatch( uri, {
+ fsPath: uri.fsPath,
+ line: 1,
+ column: 1,
+ match: 'TODO TODO',
+ lines: 'TODO TODO\n',
+ absoluteOffset: 0,
+ submatches: [ {
+ match: 'TODO TODO',
+ start: 0,
+ end: 9
+ } ]
+ } );
+
+ assert.equal( normalized.actualTag, 'TODO' );
+ assert.equal( normalized.displayText, 'TODO' );
+ assert.equal( normalized.match, 'TODO TODO' );
+ assert.equal( normalized.line, 1 );
+ assert.equal( normalized.column, 1 );
+ } );
+
QUnit.test( "issue #888 multiline banner regex anchors the star tag to the content line", function( assert )
{
var uri = matrixHelpers.createUri( '/tmp/issue-888.js' );
diff --git a/test/extension.scan-parity.test.js b/test/extension.scan-parity.test.js
index 1317bd6c..ac3685df 100644
--- a/test/extension.scan-parity.test.js
+++ b/test/extension.scan-parity.test.js
@@ -1386,6 +1386,94 @@ QUnit.test( "open-files mode stores canonical document results through the refre
} );
} );
+QUnit.test( "issue #42 default-derived workspace regex uses candidate scan", function( assert )
+{
+ var regexSource = actualUtils.DEFAULT_REGEX_SOURCE.replace( '|;', '' );
+ var filePath = '/workspace/todo.md';
+ var uri = matrixHelpers.createUri( filePath );
+ var fixture = [ {
+ uri: uri,
+ actualTag: '[ ]',
+ displayText: 'Task 1',
+ continuationText: []
+ } ];
+ var harness = createExtensionHarness( {
+ scanMode: 'workspace',
+ regexSource: regexSource,
+ resourceConfig: { isDefaultRegex: false, enableMultiLine: false, regexCaseSensitive: true },
+ workspaceFolders: [ { uri: matrixHelpers.createUri( '/workspace' ), name: 'workspace' } ],
+ fileContents: {
+ '/workspace/todo.md': '- [ ] Task 1\n'
+ },
+ workspaceResults: fixture,
+ ripgrepMatches: [ {
+ fsPath: filePath,
+ line: 1,
+ column: 3,
+ match: '[ ]',
+ lines: '- [ ] Task 1\n'
+ } ]
+ } );
+
+ harness.extension.activate( harness.context );
+
+ return matrixHelpers.flushAsyncWork().then( function()
+ {
+ assert.equal( harness.ripgrepSearchCalls.length, 1 );
+ assert.equal( harness.ripgrepSearchCalls[ 0 ].regex, '(TODO|FIXME|BUG|HACK|XXX|\\[ \\]|\\[x\\])' );
+ assert.equal( harness.ripgrepSearchCalls[ 0 ].unquotedRegex, '(TODO|FIXME|BUG|HACK|XXX|\\[ \\]|\\[x\\])' );
+ assert.equal( harness.scanTextCalls.length, 1 );
+ assert.equal( harness.scanTextCalls[ 0 ].text, '- [ ] Task 1\n' );
+ assert.deepEqual( getLatestReplaceCallForPath( harness, filePath ).results, fixture );
+ } );
+} );
+
+QUnit.test( "PCRE2-only workspace tag regex uses raw normalization", function( assert )
+{
+ var filePath = '/workspace/repeated.js';
+ var uri = matrixHelpers.createUri( filePath );
+ var fixture = [ {
+ uri: uri,
+ actualTag: 'TODO',
+ displayText: 'TODO',
+ continuationText: []
+ } ];
+ var harness = createExtensionHarness( {
+ scanMode: 'workspace',
+ regexSource: '($TAGS)\\s+\\g{1}',
+ resourceConfig: { isDefaultRegex: false, enableMultiLine: false, regexCaseSensitive: true },
+ workspaceFolders: [ { uri: matrixHelpers.createUri( '/workspace' ), name: 'workspace' } ],
+ fileContents: {
+ '/workspace/repeated.js': 'TODO TODO\n'
+ },
+ ripgrepMatches: [ {
+ fsPath: filePath,
+ line: 1,
+ column: 1,
+ match: 'TODO TODO',
+ lines: 'TODO TODO\n'
+ } ],
+ normalizeResult: function()
+ {
+ return fixture[ 0 ];
+ },
+ normalizeWorkspaceResult: function()
+ {
+ return fixture[ 0 ];
+ }
+ } );
+
+ harness.extension.activate( harness.context );
+
+ return matrixHelpers.flushAsyncWork().then( function()
+ {
+ assert.equal( harness.ripgrepSearchCalls.length, 1 );
+ assert.equal( harness.ripgrepSearchCalls[ 0 ].regex, '($TAGS)\\s+\\g{1}' );
+ assert.equal( harness.scanTextCalls.length, 0 );
+ assert.deepEqual( getLatestReplaceCallForPath( harness, filePath ).results, fixture );
+ } );
+} );
+
QUnit.test( "default workspace startup keeps plain visible editors on the O(1) non-notebook path", function( assert )
{
var firstDocument = matrixHelpers.createDocument( '/workspace/src/first.js', '// TODO first item' );
diff --git a/test/regexEngine.test.js b/test/regexEngine.test.js
new file mode 100644
index 00000000..e9a90f1f
--- /dev/null
+++ b/test/regexEngine.test.js
@@ -0,0 +1,68 @@
+var regexEngine = require( '../src/regexEngine.js' );
+
+QUnit.module( "regex engine classification" );
+
+QUnit.test( "lookaround requires PCRE2", function( assert )
+{
+ assert.equal( regexEngine.containsLookAround( undefined ), false );
+ assert.equal( regexEngine.requiresPcre2( '($TAGS)(?![A-Za-z0-9_])' ), true );
+ assert.equal( regexEngine.requiresPcre2( '(?<=# )($TAGS)' ), true );
+ assert.equal( regexEngine.requiresPcre2( '\\(\\?=literal\\)($TAGS)' ), false );
+ assert.equal( regexEngine.requiresPcre2( '[()?=]+($TAGS)' ), false );
+} );
+
+QUnit.test( "backreference requires PCRE2", function( assert )
+{
+ assert.equal( regexEngine.containsBackreference( undefined ), false );
+ assert.equal( regexEngine.requiresPcre2( '($TAGS)\\s+\\1' ), true );
+ assert.equal( regexEngine.requiresPcre2( '(?TODO)\\s+\\k' ), true );
+ assert.equal( regexEngine.requiresPcre2( '(?TODO)\\s+\\k{tag}' ), true );
+ assert.equal( regexEngine.requiresPcre2( '(?TODO)\\s+\\g{tag}' ), true );
+ assert.equal( regexEngine.requiresPcre2( '($TAGS)\\s+\\g{1}' ), true );
+ assert.equal( regexEngine.requiresPcre2( '(?TODO)\\s+(?P=tag)' ), true );
+ assert.equal( regexEngine.requiresPcre2( '($TAGS)\\\\1' ), false );
+} );
+
+QUnit.test( "ripgrep engine args respect explicit engine selection", function( assert )
+{
+ assert.deepEqual( regexEngine.buildRegexEngineArgs( '($TAGS)(?!x)', [] ), [ '--pcre2' ] );
+ assert.deepEqual( regexEngine.buildRegexEngineArgs( '($TAGS)(?!x)', [ '--pcre2' ] ), [] );
+ assert.deepEqual( regexEngine.buildRegexEngineArgs( '($TAGS)(?!x)', [ '-P' ] ), [] );
+ assert.deepEqual( regexEngine.buildRegexEngineArgs( '($TAGS)(?!x)', [ '--engine=default' ] ), [] );
+ assert.deepEqual( regexEngine.buildRegexEngineArgs( '($TAGS)', [] ), [] );
+} );
+
+QUnit.test( "tag candidate scan covers default and PCRE2 tag regexes", function( assert )
+{
+ assert.equal( regexEngine.shouldUseTagCandidateScan( {
+ isDefaultRegex: true,
+ regex: '($TAGS)'
+ } ), true );
+ assert.equal( regexEngine.shouldUseTagCandidateScan( {
+ isDefaultRegex: false,
+ regex: '($TAGS)(?![A-Za-z0-9_])'
+ } ), true );
+ assert.equal( regexEngine.shouldUseTagCandidateScan( {
+ isDefaultRegex: false,
+ regex: '($TAGS):.*'
+ } ), false );
+ assert.equal( regexEngine.shouldUseTagCandidateScan( {
+ isDefaultRegex: false,
+ regex: '(?TODO)\\s+\\k' ), false );
+ assert.equal( regexEngine.containsJavaScriptIncompatibleBackreference( '(?TODO)\\s+\\k{tag}' ), true );
+ assert.equal( regexEngine.containsJavaScriptIncompatibleBackreference( '(?TODO)\\s+\\g{tag}' ), true );
+ assert.equal( regexEngine.containsJavaScriptIncompatibleBackreference( '($TAGS)\\s+\\g{1}' ), true );
+ assert.equal( regexEngine.containsJavaScriptIncompatibleBackreference( '(?TODO)\\s+(?P=tag)' ), true );
+ assert.equal( regexEngine.containsJavaScriptIncompatibleBackreference( '[\\g{1}]($TAGS)' ), false );
+ assert.equal( regexEngine.shouldUseTagCandidateScan( {
+ isDefaultRegex: false,
+ regex: '($TAGS)\\s+\\g{1}'
+ } ), false );
+} );
diff --git a/test/ripgrep.test.js b/test/ripgrep.test.js
index 342e4b82..7fee9bce 100644
--- a/test/ripgrep.test.js
+++ b/test/ripgrep.test.js
@@ -54,6 +54,30 @@ QUnit.module( "ripgrep streaming search", function( hooks )
);
} );
+ QUnit.test( "buildArgs adds PCRE2 for lookaround regexes", function( assert )
+ {
+ var args = ripgrep.buildArgs( {
+ regex: '($TAGS)(?![A-Za-z0-9_])',
+ additional: '--max-columns=1000',
+ globs: []
+ } );
+
+ assert.notEqual( args.indexOf( '--pcre2' ), -1 );
+ assert.ok( args.indexOf( '--pcre2' ) < args.indexOf( '-e' ) );
+ } );
+
+ QUnit.test( "buildArgs preserves explicit ripgrep engine args", function( assert )
+ {
+ var args = ripgrep.buildArgs( {
+ regex: '($TAGS)(?![A-Za-z0-9_])',
+ additional: '--engine=default',
+ globs: []
+ } );
+
+ assert.equal( args.indexOf( '--pcre2' ), -1 );
+ assert.notEqual( args.indexOf( '--engine=default' ), -1 );
+ } );
+
QUnit.test( "search parses streamed json messages across chunk boundaries", function( assert )
{
var seenMessages = [];