From 1ce7978f0fe74ba72f89d2296a71f5ec5b733351 Mon Sep 17 00:00:00 2001 From: Nathan Trudeau Date: Fri, 29 May 2026 15:12:06 -0400 Subject: [PATCH 1/3] fix: restore labels, codicon aliases, and issue branch flow Prefix-only regex matches that contain ($TAGS) keep the physical-line suffix available to displayText and ${after} across editor and workspace scans. Detection: - extend non-multiline ($TAGS) regex matches to the physical line end before tag extraction - preserve whole-match tags for explicit regexes without ($TAGS) - cover legacy prefix regex, default labels, and workspace normalization Icons: - generate codicon alias names instead of codepoints - parse "$(icon)" syntax through a shared helper for tree, gutter, and configuration checks - keep malformed codicon syntax invalid before alias lookup Workflow: - add issue-derived branch recipes for full flow and standalone branch steps - derive default branch names from GitHub issue numbers and titles - create local and remote branches from upstream master without pushing master - move source-branch changes onto the issue branch, stage them, wait for commit, then push the issue branch - prompt for PR creation after branch push with yes as the default answer Docs and artifacts: - document prefix-only regex suffix rendering and octicon/codicon icon syntax - replace checked-in perf JSON with markdown benchmark summaries - ignore generated perf JSON artifacts Fixes https://github.com/FanaticPythoner/better-todo-tree/issues/28 Fixes https://github.com/FanaticPythoner/better-todo-tree/issues/36 --- .gitignore | 1 + README.md | 4 +- artifacts/perf/issue22-ripgrep-path.json | 192 --- artifacts/perf/issue28-drop-in-replacement.md | 74 + .../perf/issue28-scan-large-custom-regex.md | 62 + artifacts/perf/issue29-tree-icons.json | 239 --- artifacts/perf/issue36-label-after.md | 66 + buildCodiconNames.js | 26 +- justfile | 48 + package.nls.json | 4 +- package.nls.zh-cn.json | 4 +- scripts/branch/issue-branch.sh | 489 +++++++ src/codiconNames.js | 1303 +++++++++-------- src/detection.js | 52 +- src/icons.js | 8 +- src/utils.js | 14 +- test/detection.behavior.test.js | 46 + test/detection.regex-matrix.test.js | 68 +- test/icons.behavior.test.js | 48 + test/issue-branch-script.test.js | 224 +++ test/package.manifest.test.js | 10 + test/tests.js | 6 + 22 files changed, 1941 insertions(+), 1047 deletions(-) delete mode 100644 artifacts/perf/issue22-ripgrep-path.json create mode 100644 artifacts/perf/issue28-drop-in-replacement.md create mode 100644 artifacts/perf/issue28-scan-large-custom-regex.md delete mode 100644 artifacts/perf/issue29-tree-icons.json create mode 100644 artifacts/perf/issue36-label-after.md create mode 100755 scripts/branch/issue-branch.sh create mode 100644 test/issue-branch-script.test.js diff --git a/.gitignore b/.gitignore index 44c2748f..48c40c6b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ artifacts/vsix/ artifacts/release-notes/ artifacts/**/*.log artifacts/perf/runtime-benchmarks.json +artifacts/perf/*.json diff --git a/README.md b/README.md index 655a0e8d..0ae17a3d 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Both `defaultHighlight` and `customHighlight` allow for the following settings: `borderRadius` - used to set the border radius of the background of the highlight. -`icon` - used to set a different icon in the tree view. Must be a valid octicon (see ) or codicon (see ). If using codicons, specify them in the format "$(*icon*)". The icon defaults to a tick if it's not valid. You can also use "better-todo-tree", or "better-todo-tree-filled" if you want to use the icon from the activity view. +`icon` - used to set a different icon in the tree view. Valid values are octicon names, codicon names in `"$(icon)"` format, `"better-todo-tree"`, and `"better-todo-tree-filled"`. Example: `"bug"` selects the Primer octicon, and `"$(bug)"` selects the VS Code codicon. `iconColour` - used to set the colour of the icon in the tree. If not specified, it will try to use the foreground colour or the background colour. Colour can be specified as per foreground and background colours, but see note below. @@ -392,7 +392,7 @@ 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 also markdown todo lists. This should cover most languages. However if you want to refine it, make sure that the ($TAGS) is kept as ($TAGS) will be replaced by the expanded tag list. For some of the extension features to work, ($TAGS) should be present in the regex, however, the basic functionality should still work if you need to explicitly expand the tag list. +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. *Note: This is a [Rust regular expression](https://docs.rs/regex/1.0.0/regex), not javascript.* diff --git a/artifacts/perf/issue22-ripgrep-path.json b/artifacts/perf/issue22-ripgrep-path.json deleted file mode 100644 index e93efd6b..00000000 --- a/artifacts/perf/issue22-ripgrep-path.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "generatedAt": "2026-05-26T11:33:24.626Z", - "node": "v25.2.0", - "baselineRef": "HEAD", - "selection": { - "mode": "scenario-list", - "suite": "microbenchmark", - "scenarios": [ - "config-ripgrep-path-resolution" - ], - "kinds": [ - "microbenchmark" - ] - }, - "machine": { - "host": { - "hostname": "n00ne-AERO-17-YD", - "osPrettyName": "Ubuntu 22.04.5 LTS", - "kernel": "6.8.0-117-generic", - "architecture": "x64", - "loadAverage": "5.05, 4.33, 3.72", - "availableParallelism": 16 - }, - "cpu": { - "accessible": true, - "modelName": "Intel(R) Core(TM) i9-14900HX", - "vendorId": "GenuineIntel", - "logicalCpus": 16, - "threadsPerCore": 2, - "coresPerSocket": 8, - "sockets": 1, - "minMHz": 800, - "maxMHz": 5800, - "l1dCache": "384 KiB (8 instances)", - "l1iCache": "256 KiB (8 instances)", - "l2Cache": "16 MiB (8 instances)", - "l3Cache": "36 MiB (1 instance)", - "numaNodes": 1 - }, - "memory": { - "totalBytes": 67119759360, - "availableBytesAtCollection": 21595205632, - "swapTotalBytes": 128848973824, - "swapFreeBytesAtCollection": 98287824896, - "layoutAccessible": true, - "onlinePhysicalBytes": 70866960384, - "dmi": { - "accessible": false, - "error": "/sys/firmware/dmi/tables/smbios_entry_point: Permission denied\n/dev/mem: Permission denied" - } - }, - "storage": { - "accessible": true, - "rootDevice": { - "name": "nvme1n1", - "model": "Samsung SSD 9100 PRO 4TB", - "sizeBytes": 4000787030016, - "transport": "nvme", - "rotational": false, - "readOnly": false - } - } - }, - "measurementModel": { - "latency": { - "valueModel": "Wall-clock elapsed time around each harness flow iteration.", - "summary": "Min, p50, p90, p95, and max across the recorded iterations.", - "accuracy": "Exact for each sampled iteration in this run." - }, - "profiledRssBurst": { - "valueModel": "Difference between the isolated scenario worker RSS measured immediately before the flow and that worker iteration's peak RSS.", - "startSource": "process.memoryUsage().rss", - "peakSource": "process.resourceUsage().maxRSS", - "accuracy": "Exact for the measured worker iteration." - }, - "profiledPeakRss": { - "valueModel": "Highest process RSS reached by the isolated scenario worker iteration.", - "peakSource": "process.resourceUsage().maxRSS", - "accuracy": "Exact worker-process OS high-water mark." - } - }, - "validation": { - "resultCount": 1, - "allResultsMatchSelection": true, - "allResultsAreUserFlow": false - }, - "results": [ - { - "name": "config-ripgrep-path-resolution", - "current": { - "name": "config-ripgrep-path-resolution-current-universal", - "iterations": 25, - "p50Ms": 11.39, - "p90Ms": 15.71, - "p95Ms": 15.75, - "minMs": 10.29, - "maxMs": 22.73, - "sampleMs": [ - 10.29, - 10.33, - 10.46, - 10.52, - 10.58, - 10.63, - 10.69, - 10.73, - 10.81, - 11, - 11.03, - 11.24, - 11.39, - 11.49, - 11.55, - 11.59, - 11.67, - 11.92, - 12.12, - 12.41, - 12.5, - 13.25, - 15.71, - 15.75, - 22.73 - ], - "peakRssP50MiB": 68.06, - "peakRssP90MiB": 68.31, - "peakRssP95MiB": 68.31, - "peakRssMiB": 68.31, - "peakAdditionalRssMiB": 11.63, - "rssBurstP50MiB": 0, - "rssBurstP90MiB": 0.13, - "rssBurstP95MiB": 0.25, - "rssBurstMaxMiB": 11.63, - "lastValue": 2048, - "operationsPerIteration": 2048, - "throughputOpsPerSecond": 179806.85 - }, - "baseline": { - "name": "config-ripgrep-path-resolution-baseline-legacy", - "iterations": 25, - "p50Ms": 6.62, - "p90Ms": 8, - "p95Ms": 8.06, - "minMs": 6.1, - "maxMs": 8.49, - "sampleMs": [ - 6.1, - 6.17, - 6.24, - 6.4, - 6.4, - 6.43, - 6.45, - 6.45, - 6.49, - 6.5, - 6.52, - 6.62, - 6.62, - 6.7, - 6.74, - 6.75, - 6.83, - 6.85, - 6.89, - 6.97, - 7.02, - 7.32, - 8, - 8.06, - 8.49 - ], - "peakRssP50MiB": 68.81, - "peakRssP90MiB": 68.81, - "peakRssP95MiB": 68.81, - "peakRssMiB": 68.81, - "peakAdditionalRssMiB": 0.25, - "rssBurstP50MiB": 0, - "rssBurstP90MiB": 0.13, - "rssBurstP95MiB": 0.13, - "rssBurstMaxMiB": 0.25, - "lastValue": 2048, - "operationsPerIteration": 2048, - "throughputOpsPerSecond": 309365.56 - }, - "kind": "microbenchmark", - "userFlow": "", - "measurementScope": "Cold config.ripgrepPath resolution with changing VS Code app roots.", - "inputModel": "One configured miss and one packaged ripgrep hit per operation." - } - ] -} diff --git a/artifacts/perf/issue28-drop-in-replacement.md b/artifacts/perf/issue28-drop-in-replacement.md new file mode 100644 index 00000000..0bac30c2 --- /dev/null +++ b/artifacts/perf/issue28-drop-in-replacement.md @@ -0,0 +1,74 @@ +# Runtime Benchmarks + +- Baseline ref: `a6f60e0ce830c4649ac34fc05e5a1799ec91d151` +- Current source: working tree +- Node: `v25.2.0` +- Selection mode: `scenario-list` +- Declared suite: `mixed` +- Result-count validation: `4 rows, suite-consistent=true, all-user-flow=false` + +## Machine Profile + +| Category | Field | Value | +| --- | --- | --- | +| Host | Hostname | n00ne-AERO-17-YD | +| Host | OS | Ubuntu 22.04.5 LTS | +| Host | Kernel | 6.8.0-117-generic | +| Host | Architecture | x64 | +| Host | Load Average | 3.34, 3.63, 3.14 | +| 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 | 17.92 GiB (`19,236,577,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 109 GiB (`116,994,105,344 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 | +| --- | --- | --- | --- | --- | +| scan-large-custom-regex | microbenchmark | - | - | - | +| attributes-custom-highlight | microbenchmark | - | - | - | +| 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-render-counts | microbenchmark | - | - | - | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-custom-regex | microbenchmark | 5.17 | 9.12 | 6.06 | 9.81 | 6.61 | 10.34 | +| attributes-custom-highlight | microbenchmark | 59.84 | 0.06 | 62.13 | 0.09 | 63.14 | 0.24 | +| open-file-custom-save-rescan-visible-tree | user-flow | 83.45 | 2.79 | 87.28 | 3.04 | 90.21 | 3.05 | +| tree-render-counts | microbenchmark | 12.11 | 4.73 | 13.49 | 5.68 | 18.63 | 6.17 | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-custom-regex | microbenchmark | 0.75 | 0.75 | 1.13 | 2.13 | 1.13 | 5 | 1.13 | 5 | +| attributes-custom-highlight | microbenchmark | 0 | 0 | 0 | 0 | 0 | 0 | 1.63 | 0 | +| open-file-custom-save-rescan-visible-tree | user-flow | 26.88 | 21 | 27.25 | 21.5 | 27.38 | 21.73 | 27.38 | 21.73 | +| tree-render-counts | microbenchmark | 0 | 0 | 1.63 | 0 | 4.25 | 0 | 4.25 | 0 | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-custom-regex | microbenchmark | 76.2 | 75.42 | 76.58 | 75.56 | 76.58 | 75.56 | 76.58 | 75.56 | +| attributes-custom-highlight | microbenchmark | 76.33 | 76.02 | 76.33 | 76.02 | 76.33 | 76.02 | 77.64 | 76.02 | +| open-file-custom-save-rescan-visible-tree | user-flow | 82.46 | 76.59 | 82.94 | 77.06 | 83.09 | 77.08 | 83.09 | 77.08 | +| tree-render-counts | microbenchmark | 238.73 | 241.16 | 242.91 | 241.16 | 247.16 | 241.16 | 247.16 | 241.16 | diff --git a/artifacts/perf/issue28-scan-large-custom-regex.md b/artifacts/perf/issue28-scan-large-custom-regex.md new file mode 100644 index 00000000..b8d389bc --- /dev/null +++ b/artifacts/perf/issue28-scan-large-custom-regex.md @@ -0,0 +1,62 @@ +# Runtime Benchmarks + +- Baseline ref: `a6f60e0ce830c4649ac34fc05e5a1799ec91d151` +- Current source: working tree +- Node: `v25.2.0` +- Selection mode: `scenario-list` +- Declared suite: `microbenchmark` +- Result-count validation: `1 rows, suite-consistent=true, all-user-flow=false` + +## Machine Profile + +| Category | Field | Value | +| --- | --- | --- | +| Host | Hostname | n00ne-AERO-17-YD | +| Host | OS | Ubuntu 22.04.5 LTS | +| Host | Kernel | 6.8.0-117-generic | +| Host | Architecture | x64 | +| Host | Load Average | 3.72, 3.72, 3.16 | +| 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 | 17.70 GiB (`19,005,583,360 bytes`) | +| Memory | Online Physical RAM | 66.00 GiB (`70,866,960,384 bytes`) | +| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 109 GiB (`116,994,105,344 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 | +| --- | --- | --- | --- | --- | +| scan-large-custom-regex | microbenchmark | - | - | - | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-custom-regex | microbenchmark | 5.39 | 8.74 | 5.99 | 9.96 | 6.96 | 10.51 | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-custom-regex | microbenchmark | 0.75 | 0.75 | 1 | 2.38 | 1.13 | 5.25 | 1.13 | 5.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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-custom-regex | microbenchmark | 76.43 | 75.44 | 76.68 | 75.61 | 76.68 | 75.68 | 76.68 | 75.68 | diff --git a/artifacts/perf/issue29-tree-icons.json b/artifacts/perf/issue29-tree-icons.json deleted file mode 100644 index 6485556b..00000000 --- a/artifacts/perf/issue29-tree-icons.json +++ /dev/null @@ -1,239 +0,0 @@ -{ - "generatedAt": "2026-05-29T17:05:26.408Z", - "node": "v25.2.0", - "baselineRef": "a6f60e0ce830c4649ac34fc05e5a1799ec91d151", - "selection": { - "mode": "scenario-list", - "suite": "mixed", - "scenarios": [ - "tree-render-counts", - "tree-view-cycle-visible-tree" - ], - "kinds": [ - "microbenchmark", - "user-flow" - ] - }, - "machine": { - "host": { - "hostname": "n00ne-AERO-17-YD", - "osPrettyName": "Ubuntu 22.04.5 LTS", - "kernel": "6.8.0-117-generic", - "architecture": "x64", - "loadAverage": "3.53, 2.65, 2.38", - "availableParallelism": 16 - }, - "cpu": { - "accessible": true, - "modelName": "Intel(R) Core(TM) i9-14900HX", - "vendorId": "GenuineIntel", - "logicalCpus": 16, - "threadsPerCore": 2, - "coresPerSocket": 8, - "sockets": 1, - "minMHz": 800, - "maxMHz": 5800, - "l1dCache": "384 KiB (8 instances)", - "l1iCache": "256 KiB (8 instances)", - "l2Cache": "16 MiB (8 instances)", - "l3Cache": "36 MiB (1 instance)", - "numaNodes": 1 - }, - "memory": { - "totalBytes": 67119767552, - "availableBytesAtCollection": 18434547712, - "swapTotalBytes": 128848973824, - "swapFreeBytesAtCollection": 117100265472, - "layoutAccessible": true, - "onlinePhysicalBytes": 70866960384, - "dmi": { - "accessible": false, - "error": "/sys/firmware/dmi/tables/smbios_entry_point: Permission denied\n/dev/mem: Permission denied" - } - }, - "storage": { - "accessible": true, - "rootDevice": { - "name": "nvme0n1", - "model": "Samsung SSD 9100 PRO 4TB", - "sizeBytes": 4000787030016, - "transport": "nvme", - "rotational": false, - "readOnly": false - } - } - }, - "measurementModel": { - "latency": { - "valueModel": "Wall-clock elapsed time around each harness flow iteration.", - "summary": "Min, p50, p90, p95, and max across the recorded iterations.", - "accuracy": "Exact for each sampled iteration in this run." - }, - "profiledRssBurst": { - "valueModel": "Difference between the isolated scenario worker RSS measured immediately before the flow and that worker iteration's peak RSS.", - "startSource": "process.memoryUsage().rss", - "peakSource": "process.resourceUsage().maxRSS", - "accuracy": "Exact for the measured worker iteration." - }, - "profiledPeakRss": { - "valueModel": "Highest process RSS reached by the isolated scenario worker iteration.", - "peakSource": "process.resourceUsage().maxRSS", - "accuracy": "Exact worker-process OS high-water mark." - } - }, - "validation": { - "resultCount": 2, - "allResultsMatchSelection": true, - "allResultsAreUserFlow": false - }, - "results": [ - { - "name": "tree-render-counts", - "current": { - "name": "tree-render-counts-current", - "iterations": 15, - "p50Ms": 4.84, - "p90Ms": 6.12, - "p95Ms": 6.46, - "minMs": 4.16, - "maxMs": 6.46, - "sampleMs": [ - 4.16, - 4.37, - 4.54, - 4.62, - 4.67, - 4.67, - 4.67, - 4.84, - 4.93, - 5.05, - 5.06, - 5.62, - 5.77, - 6.12, - 6.46 - ], - "peakRssP50MiB": 149.29, - "peakRssP90MiB": 149.29, - "peakRssP95MiB": 151.29, - "peakRssMiB": 151.29, - "peakAdditionalRssMiB": 2.38, - "rssBurstP50MiB": 0, - "rssBurstP90MiB": 0, - "rssBurstP95MiB": 2.38, - "rssBurstMaxMiB": 2.38, - "lastValue": 1 - }, - "baseline": { - "name": "tree-render-counts-baseline", - "iterations": 15, - "p50Ms": 14.76, - "p90Ms": 18.15, - "p95Ms": 23.49, - "minMs": 9.75, - "maxMs": 23.49, - "sampleMs": [ - 9.75, - 11.33, - 11.88, - 12.17, - 12.38, - 12.77, - 14.05, - 14.76, - 15.92, - 15.95, - 16.53, - 16.89, - 17.66, - 18.15, - 23.49 - ], - "peakRssP50MiB": 149.4, - "peakRssP90MiB": 150.54, - "peakRssP95MiB": 157.67, - "peakRssMiB": 157.67, - "peakAdditionalRssMiB": 7.13, - "rssBurstP50MiB": 0, - "rssBurstP90MiB": 1.25, - "rssBurstP95MiB": 7.13, - "rssBurstMaxMiB": 7.13, - "lastValue": 1 - }, - "kind": "microbenchmark", - "userFlow": "", - "measurementScope": "", - "inputModel": "" - }, - { - "name": "tree-view-cycle-visible-tree", - "current": { - "name": "tree-view-cycle-visible-tree-current", - "iterations": 10, - "p50Ms": 0.39, - "p90Ms": 0.47, - "p95Ms": 0.49, - "minMs": 0.34, - "maxMs": 0.49, - "sampleMs": [ - 0.34, - 0.36, - 0.37, - 0.38, - 0.39, - 0.39, - 0.42, - 0.42, - 0.47, - 0.49 - ], - "lastValue": 1, - "peakRssP50MiB": 64.47, - "peakRssP90MiB": 64.77, - "peakRssP95MiB": 64.88, - "peakRssMiB": 64.88, - "peakAdditionalRssMiB": 9.25, - "rssBurstP50MiB": 9.13, - "rssBurstP90MiB": 9.25, - "rssBurstP95MiB": 9.25, - "rssBurstMaxMiB": 9.25 - }, - "baseline": { - "name": "tree-view-cycle-visible-tree-baseline", - "iterations": 10, - "p50Ms": 9.65, - "p90Ms": 10.44, - "p95Ms": 11.61, - "minMs": 9.06, - "maxMs": 11.61, - "sampleMs": [ - 9.06, - 9.47, - 9.58, - 9.63, - 9.65, - 9.98, - 10.16, - 10.34, - 10.44, - 11.61 - ], - "lastValue": 1, - "peakRssP50MiB": 72.66, - "peakRssP90MiB": 73.02, - "peakRssP95MiB": 73.21, - "peakRssMiB": 73.21, - "peakAdditionalRssMiB": 17.38, - "rssBurstP50MiB": 17, - "rssBurstP90MiB": 17.25, - "rssBurstP95MiB": 17.38, - "rssBurstMaxMiB": 17.38 - }, - "kind": "user-flow", - "userFlow": "Cycle the tree between flat, tags-only, and tree views and redraw the visible tree each time.", - "measurementScope": "View-mode commands, workspace-state mutation, and visible-tree rebuild/render.", - "inputModel": "Fixture workspace tree in a VS Code event harness." - } - ] -} diff --git a/artifacts/perf/issue36-label-after.md b/artifacts/perf/issue36-label-after.md new file mode 100644 index 00000000..cbfc0b25 --- /dev/null +++ b/artifacts/perf/issue36-label-after.md @@ -0,0 +1,66 @@ +# Runtime Benchmarks + +- Baseline ref: `a6f60e0ce830c4649ac34fc05e5a1799ec91d151` +- Current source: working tree +- Node: `v25.2.0` +- Selection mode: `scenario-list` +- Declared suite: `microbenchmark` +- Result-count validation: `2 rows, suite-consistent=true, all-user-flow=false` + +## Machine Profile + +| Category | Field | Value | +| --- | --- | --- | +| Host | Hostname | n00ne-AERO-17-YD | +| Host | OS | Ubuntu 22.04.5 LTS | +| Host | Kernel | 6.8.0-117-generic | +| Host | Architecture | x64 | +| Host | Load Average | 2.83, 3.42, 3.14 | +| 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 | 18.18 GiB (`19,521,007,616 bytes`) | +| Memory | Online Physical RAM | 66.00 GiB (`70,866,960,384 bytes`) | +| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 109 GiB (`116,895,039,488 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 | +| --- | --- | --- | --- | --- | +| scan-large-default | microbenchmark | - | - | - | +| scan-large-custom-regex | microbenchmark | - | - | - | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-default | microbenchmark | 7.33 | 25.34 | 7.99 | 30.95 | 8.55 | 36.68 | +| scan-large-custom-regex | microbenchmark | 5.35 | 7.59 | 6.12 | 9.46 | 7.17 | 9.67 | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-default | microbenchmark | 0.75 | 0.84 | 0.75 | 12.95 | 0.88 | 27.21 | 0.88 | 27.21 | +| scan-large-custom-regex | microbenchmark | 0.75 | 0.75 | 0.88 | 0.75 | 1.38 | 0.75 | 1.38 | 0.75 | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| scan-large-default | microbenchmark | 116.31 | 116.18 | 116.43 | 116.45 | 116.73 | 116.48 | 116.73 | 116.48 | +| scan-large-custom-regex | microbenchmark | 116.69 | 116.49 | 117.3 | 116.51 | 117.34 | 117.71 | 117.34 | 117.71 | diff --git a/buildCodiconNames.js b/buildCodiconNames.js index 6673eb98..130e7266 100755 --- a/buildCodiconNames.js +++ b/buildCodiconNames.js @@ -14,7 +14,31 @@ if( !mappings || Array.isArray( mappings ) || typeof mappings !== "object" ) throw new Error( "codicon mapping: object required" ); } -var output = "module.exports = " + JSON.stringify( Object.keys( mappings ), null, 2 ) + ";\n"; +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 ) { diff --git a/justfile b/justfile index 28b174ff..c5d14c7a 100644 --- a/justfile +++ b/justfile @@ -228,6 +228,54 @@ build-ext *platforms: {{node_bootstrap}} node scripts/release/build-vsix.mjs {{platforms}} +# Whole flow: create branch, stage local source changes, wait, push branch, prompt PR. +# Examples: +# just issue-branch-all https://github.com/FanaticPythoner/better-todo-tree/issues/28 https://github.com/FanaticPythoner/better-todo-tree/issues/36 +issue-branch-all *args: + #!/usr/bin/env bash + set -euo pipefail + bash scripts/branch/issue-branch.sh flow {{args}} + +# Step only: print derived branch name. +# Examples: +# just issue-branch-name https://github.com/FanaticPythoner/better-todo-tree/issues/28 https://github.com/FanaticPythoner/better-todo-tree/issues/36 +issue-branch-name *args: + #!/usr/bin/env bash + set -euo pipefail + bash scripts/branch/issue-branch.sh name {{args}} + +# Step only: create local and remote branch from remote base. +# Examples: +# just issue-branch-create https://github.com/FanaticPythoner/better-todo-tree/issues/28 https://github.com/FanaticPythoner/better-todo-tree/issues/36 +issue-branch-create *args: + #!/usr/bin/env bash + set -euo pipefail + bash scripts/branch/issue-branch.sh create {{args}} + +# Step only: move current source-branch changes onto target branch and stage. +# Examples: +# just issue-branch-stage https://github.com/FanaticPythoner/better-todo-tree/issues/28 https://github.com/FanaticPythoner/better-todo-tree/issues/36 +issue-branch-stage *args: + #!/usr/bin/env bash + set -euo pipefail + bash scripts/branch/issue-branch.sh stage {{args}} + +# Step only: wait for local commit, then push target branch. +# Examples: +# just issue-branch-push https://github.com/FanaticPythoner/better-todo-tree/issues/28 https://github.com/FanaticPythoner/better-todo-tree/issues/36 +issue-branch-push *args: + #!/usr/bin/env bash + set -euo pipefail + bash scripts/branch/issue-branch.sh push {{args}} + +# Step only: create PR for target branch. +# Examples: +# just issue-branch-pr https://github.com/FanaticPythoner/better-todo-tree/issues/28 https://github.com/FanaticPythoner/better-todo-tree/issues/36 +issue-branch-pr *args: + #!/usr/bin/env bash + set -euo pipefail + bash scripts/branch/issue-branch.sh pr {{args}} + # Examples: # just clean # just clean --force diff --git a/package.nls.json b/package.nls.json index 4af2df8e..82f5b1b5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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. Note: **($TAGS)** will be replaced by the expanded tag list. For some of the extension features to work, **($TAGS)** should be present in the regex, however, the basic functionality should still work if you need to explicitly expand the tag list.", - "better-todo-tree.configuration.regex.regex.markdownDescription": "Regular expression for matching TODOs. Note: **($TAGS)** will be replaced by the expanded tag list. For some of the extension features to work, **($TAGS)** should be present in the regex, however, the basic functionality should still work if you need to explicitly expand the tag list.", + "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.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.", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index d2d53ff0..98277217 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -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` 必须在正则表达式中出现。但在正则表达式中完整列出标签(而不使用 `$TAGS`)也不会影响到基本的功能。", - "better-todo-tree.configuration.regex.regex.markdownDescription": "控制匹配待办事项标签的 Rust 正则表达式。注意:`$TAGS` 会被标签列表内的项替换。为了某些功能的正常使用,`$TAGS` 必须在正则表达式中出现。但在正则表达式中完整列出标签(而不使用 `$TAGS`)也不会影响到基本的功能。", + "todo-tree.configuration.regex.regex.markdownDescription": "控制匹配待办事项标签的 Rust 正则表达式。`$TAGS` 会被标签列表内的项替换。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。", + "better-todo-tree.configuration.regex.regex.markdownDescription": "控制匹配待办事项标签的 Rust 正则表达式。`$TAGS` 会被标签列表内的项替换。包含 `$TAGS` 的前缀匹配自定义正则表达式会从检测到的标签渲染到物理行末尾。", "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.", diff --git a/scripts/branch/issue-branch.sh b/scripts/branch/issue-branch.sh new file mode 100755 index 00000000..8db154f1 --- /dev/null +++ b/scripts/branch/issue-branch.sh @@ -0,0 +1,489 @@ +#!/usr/bin/env bash +set -euo pipefail + +remote_name='origin' +base_branch='master' +source_branch='master' +branch_name='' +no_wait=0 +pr_mode='prompt' +issue_urls=() +parsed_owner='' +parsed_repo='' +parsed_number='' + +usage() +{ + cat <<'EOF' +Usage: + issue-branch.sh name [options] ISSUE_URL... + issue-branch.sh create [options] ISSUE_URL... + issue-branch.sh stage [options] ISSUE_URL... + issue-branch.sh push [options] ISSUE_URL... + issue-branch.sh pr [options] ISSUE_URL... + issue-branch.sh flow [options] ISSUE_URL... + +Options: + --branch NAME Override derived branch name. + --remote NAME Git remote for base fetch and branch push. Default: origin. + --base NAME Remote base branch. Default: master. + --source NAME Local source branch carrying pending changes. Default: master. + --no-wait Push without interactive Enter prompt. + --pr Create PR without prompt after push. + --no-pr Skip PR creation after push. + -h, --help Show usage. + +Default branch: + fix/issue-N-title-slug + fix/issues-N-N-title-slugs +EOF +} + +fail() +{ + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +require_command() +{ + command -v "$1" >/dev/null 2>&1 || fail "required command '$1' is not available." +} + +repo_root() +{ + git rev-parse --show-toplevel 2>/dev/null +} + +current_branch() +{ + git symbolic-ref --quiet --short HEAD +} + +parse_args() +{ + while [[ $# -gt 0 ]]; do + case "$1" in + --branch) + [[ $# -ge 2 ]] || fail '--branch requires a value.' + branch_name="$2" + shift 2 + ;; + --remote) + [[ $# -ge 2 ]] || fail '--remote requires a value.' + remote_name="$2" + shift 2 + ;; + --base) + [[ $# -ge 2 ]] || fail '--base requires a value.' + base_branch="$2" + shift 2 + ;; + --source) + [[ $# -ge 2 ]] || fail '--source requires a value.' + source_branch="$2" + shift 2 + ;; + --no-wait) + no_wait=1 + shift + ;; + --pr) + pr_mode='yes' + shift + ;; + --no-pr) + pr_mode='no' + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + while [[ $# -gt 0 ]]; do + issue_urls+=( "$1" ) + shift + done + ;; + -*) + fail "unknown option '$1'." + ;; + *) + issue_urls+=( "$1" ) + shift + ;; + esac + done +} + +parse_issue_url() +{ + local url="$1" + + if [[ ! "$url" =~ ^https://github\.com/([^/]+)/([^/]+)/issues/([0-9]+)([/?#].*)?$ ]]; then + fail "unsupported issue URL '$url'. Expected https://github.com/OWNER/REPO/issues/NUMBER." + fi + + parsed_owner="${BASH_REMATCH[1]}" + parsed_repo="${BASH_REMATCH[2]}" + parsed_number="${BASH_REMATCH[3]}" +} + +slugify_title() +{ + local title="$1" + + printf '%s' "$title" \ + | LC_ALL=C tr '[:upper:]' '[:lower:]' \ + | LC_ALL=C sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g' +} + +read_issue_title() +{ + local url="$1" + local metadata='' + local title='' + local viewed_number='' + + require_command gh + metadata="$(env -u DEBUG -u GH_DEBUG GH_PROMPT_DISABLED=1 gh issue view "$url" --json number,title --jq '[.number, .title] | @tsv' 2>&1)" \ + || fail "could not read issue metadata for '$url': $metadata" + + viewed_number="${metadata%%$'\t'*}" + title="${metadata#*$'\t'}" + + [[ "$viewed_number" == "$parsed_number" ]] || fail "metadata number mismatch for '$url'." + [[ -n "$title" ]] || fail "issue '$url' has an empty title." + + printf '%s\n' "$title" +} + +issue_records() +{ + local url='' + local title='' + local first_owner='' + local first_repo='' + + [[ ${#issue_urls[@]} -gt 0 ]] || fail 'at least one GitHub issue URL is required.' + + for url in "${issue_urls[@]}"; do + parse_issue_url "$url" + + if [[ -z "$first_owner" ]]; then + first_owner="$parsed_owner" + first_repo="$parsed_repo" + elif [[ "$parsed_owner/$parsed_repo" != "$first_owner/$first_repo" ]]; then + fail 'issue URLs must belong to one GitHub repository.' + fi + + title="$(read_issue_title "$url")" + printf '%s\t%s\n' "$parsed_number" "$title" + done | sort -n -k1,1 +} + +derive_branch_name() +{ + local records=() + local record='' + local number='' + local title='' + local slug='' + local joined_numbers='' + local joined_slugs='' + local records_output='' + + records_output="$(issue_records)" + mapfile -t records <<< "$records_output" + + for record in "${records[@]}"; do + number="${record%%$'\t'*}" + title="${record#*$'\t'}" + slug="$(slugify_title "$title")" + [[ -n "$slug" ]] || fail "issue '$number' title cannot form a branch slug." + + if [[ -z "$joined_numbers" ]]; then + joined_numbers="$number" + joined_slugs="$slug" + else + joined_numbers="${joined_numbers}-${number}" + joined_slugs="${joined_slugs}-${slug}" + fi + done + + if [[ ${#records[@]} -eq 1 ]]; then + printf 'fix/issue-%s-%s\n' "$joined_numbers" "$joined_slugs" + else + printf 'fix/issues-%s-%s\n' "$joined_numbers" "$joined_slugs" + fi +} + +resolve_branch_name() +{ + if [[ -z "$branch_name" ]]; then + branch_name="$(derive_branch_name)" + fi + + git check-ref-format --branch "$branch_name" >/dev/null 2>&1 || fail "invalid branch name '$branch_name'." + + case "$branch_name" in + "$base_branch"|"$source_branch"|master|main) + fail "branch '$branch_name' is protected." + ;; + esac + + printf '%s\n' "$branch_name" +} + +require_git_repo() +{ + local root='' + + require_command git + root="$(repo_root)" || fail 'current directory is not inside a Git repository.' + cd "$root" +} + +require_remote() +{ + git remote get-url "$remote_name" >/dev/null 2>&1 || fail "remote '$remote_name' was not found." +} + +fetch_base() +{ + require_remote + git fetch --no-tags --force "$remote_name" "+refs/heads/$base_branch:refs/remotes/$remote_name/$base_branch" + git rev-parse --verify "refs/remotes/$remote_name/$base_branch^{commit}" >/dev/null +} + +fetch_branch() +{ + require_remote + git fetch --no-tags --force "$remote_name" "+refs/heads/$branch_name:refs/remotes/$remote_name/$branch_name" + git rev-parse --verify "refs/remotes/$remote_name/$branch_name^{commit}" >/dev/null +} + +remote_branch_exists() +{ + local status=0 + + set +e + git ls-remote --exit-code --heads "$remote_name" "$branch_name" >/dev/null 2>&1 + status="$?" + set -e + + case "$status" in + 0) + return 0 + ;; + 2) + return 1 + ;; + *) + fail "could not inspect remote branch '$remote_name/$branch_name'." + ;; + esac +} + +require_no_branch_collision() +{ + if git rev-parse --verify "refs/heads/$branch_name" >/dev/null 2>&1; then + fail "local branch '$branch_name' already exists." + fi + + if remote_branch_exists; then + fail "remote branch '$remote_name/$branch_name' already exists." + fi +} + +create_branch() +{ + local base_ref="refs/remotes/$remote_name/$base_branch" + + require_git_repo + resolve_branch_name >/dev/null + fetch_base + require_no_branch_collision + + git branch "$branch_name" "$base_ref" + git push "$remote_name" "refs/heads/$branch_name:refs/heads/$branch_name" + git branch --set-upstream-to="$remote_name/$branch_name" "$branch_name" >/dev/null + + printf 'created branch %s from %s/%s\n' "$branch_name" "$remote_name" "$base_branch" +} + +require_clean_merge_state() +{ + local unmerged='' + + unmerged="$(git diff --name-only --diff-filter=U)" + [[ -z "$unmerged" ]] || fail "unmerged paths block branch preparation: $unmerged" +} + +stash_source_changes() +{ + local status_output='' + + status_output="$(git status --porcelain=v1)" + if [[ -z "$status_output" ]]; then + printf '\n' + return 0 + fi + + git stash push --include-untracked --message "issue-branch:$branch_name:$(date -u +%Y%m%dT%H%M%SZ)" >/dev/null + printf 'stash@{0}\n' +} + +stage_branch_changes() +{ + local active_branch='' + local stash_ref='' + + require_git_repo + branch_name="$(resolve_branch_name)" + require_clean_merge_state + + active_branch="$(current_branch)" || fail 'detached HEAD cannot supply source changes.' + [[ "$active_branch" == "$source_branch" ]] || fail "current branch '$active_branch' does not match source '$source_branch'." + git rev-parse --verify "refs/heads/$branch_name" >/dev/null 2>&1 || fail "local branch '$branch_name' was not found." + + stash_ref="$(stash_source_changes)" + git switch "$branch_name" >/dev/null + + if [[ -n "$stash_ref" ]]; then + if git stash apply --index "$stash_ref"; then + git stash drop "$stash_ref" >/dev/null + else + fail "stash apply failed on '$branch_name'. Retained stash: $stash_ref" + fi + fi + + git add -A + printf 'staged changes on %s\n' "$branch_name" +} + +commits_ahead() +{ + git rev-list --count "$remote_name/$branch_name"..HEAD +} + +push_after_commit() +{ + local active_branch='' + local ahead_count='0' + + require_git_repo + branch_name="$(resolve_branch_name)" + git rev-parse --verify "refs/remotes/$remote_name/$branch_name" >/dev/null 2>&1 || fetch_branch >/dev/null + git rev-parse --verify "refs/remotes/$remote_name/$branch_name" >/dev/null 2>&1 || fail "remote branch '$remote_name/$branch_name' was not found." + + while true; do + active_branch="$(current_branch)" || fail 'detached HEAD cannot be pushed.' + [[ "$active_branch" == "$branch_name" ]] || fail "current branch '$active_branch' does not match target '$branch_name'." + + ahead_count="$(commits_ahead)" + if [[ "$ahead_count" != '0' ]]; then + git push "$remote_name" "HEAD:refs/heads/$branch_name" + printf 'pushed %s commit(s) to %s/%s\n' "$ahead_count" "$remote_name" "$branch_name" + return 0 + fi + + [[ "$no_wait" -eq 0 ]] || fail "branch '$branch_name' has no local commits ahead of '$remote_name/$branch_name'." + + printf "Commit staged changes on '%s' in another terminal. Press Enter to push '%s/%s' after commit." "$branch_name" "$remote_name" "$branch_name" + read -r _ + done +} + +create_pr() +{ + require_git_repo + require_command gh + branch_name="$(resolve_branch_name)" + gh pr create --base "$base_branch" --head "$branch_name" --fill +} + +prompt_pr() +{ + local answer='' + + case "$pr_mode" in + yes) + create_pr + ;; + no) + printf 'pull request creation skipped\n' + ;; + prompt) + [[ -t 0 ]] || fail 'TTY required for PR prompt. Use --pr or --no-pr.' + printf 'Create pull request for %s? [Y/n] ' "$branch_name" + read -r answer + case "$answer" in + ''|y|Y|yes|YES) + create_pr + ;; + n|N|no|NO) + printf 'pull request creation skipped\n' + ;; + *) + fail "unsupported answer '$answer'." + ;; + esac + ;; + *) + fail "unsupported PR mode '$pr_mode'." + ;; + esac +} + +flow() +{ + branch_name="$(resolve_branch_name)" + create_branch + stage_branch_changes + push_after_commit + prompt_pr +} + +main() +{ + local command_name="${1:-flow}" + + if [[ $# -gt 0 ]]; then + shift + fi + + parse_args "$@" + + case "$command_name" in + name) + require_command git + resolve_branch_name + ;; + create) + create_branch + ;; + stage) + stage_branch_changes + ;; + push) + push_after_commit + ;; + pr) + create_pr + ;; + flow) + flow + ;; + -h|--help|help) + usage + ;; + *) + fail "unknown command '$command_name'." + ;; + esac +} + +main "$@" diff --git a/src/codiconNames.js b/src/codiconNames.js index 27983005..afba0b7e 100644 --- a/src/codiconNames.js +++ b/src/codiconNames.js @@ -1,599 +1,708 @@ module.exports = [ - "60000", - "60001", - "60002", - "60003", - "60004", - "60005", - "60006", - "60007", - "60008", - "60009", - "60010", - "60011", - "60012", - "60013", - "60014", - "60015", - "60016", - "60017", - "60018", - "60019", - "60020", - "60021", - "60022", - "60023", - "60024", - "60025", - "60026", - "60027", - "60028", - "60029", - "60030", - "60031", - "60032", - "60033", - "60034", - "60035", - "60036", - "60037", - "60038", - "60039", - "60040", - "60042", - "60043", - "60044", - "60047", - "60048", - "60049", - "60050", - "60051", - "60052", - "60053", - "60054", - "60055", - "60056", - "60057", - "60058", - "60059", - "60060", - "60061", - "60062", - "60063", - "60064", - "60065", - "60066", - "60067", - "60068", - "60069", - "60070", - "60071", - "60072", - "60073", - "60074", - "60075", - "60076", - "60077", - "60078", - "60079", - "60080", - "60081", - "60082", - "60083", - "60084", - "60085", - "60086", - "60087", - "60088", - "60089", - "60090", - "60091", - "60092", - "60093", - "60094", - "60095", - "60096", - "60097", - "60098", - "60099", - "60100", - "60101", - "60102", - "60103", - "60105", - "60108", - "60109", - "60110", - "60111", - "60112", - "60113", - "60114", - "60115", - "60116", - "60117", - "60118", - "60119", - "60120", - "60121", - "60122", - "60123", - "60124", - "60125", - "60126", - "60127", - "60128", - "60129", - "60130", - "60131", - "60132", - "60133", - "60134", - "60135", - "60136", - "60137", - "60138", - "60139", - "60140", - "60141", - "60142", - "60143", - "60144", - "60145", - "60146", - "60147", - "60148", - "60149", - "60150", - "60151", - "60152", - "60153", - "60154", - "60155", - "60156", - "60157", - "60158", - "60159", - "60160", - "60161", - "60162", - "60163", - "60164", - "60165", - "60166", - "60167", - "60168", - "60169", - "60171", - "60172", - "60173", - "60174", - "60175", - "60176", - "60177", - "60178", - "60179", - "60180", - "60181", - "60182", - "60183", - "60184", - "60185", - "60186", - "60187", - "60188", - "60189", - "60190", - "60191", - "60192", - "60193", - "60194", - "60195", - "60196", - "60197", - "60198", - "60199", - "60200", - "60201", - "60202", - "60203", - "60204", - "60205", - "60206", - "60207", - "60208", - "60209", - "60210", - "60211", - "60212", - "60213", - "60214", - "60215", - "60216", - "60217", - "60218", - "60219", - "60220", - "60221", - "60222", - "60223", - "60224", - "60225", - "60226", - "60227", - "60228", - "60229", - "60230", - "60231", - "60232", - "60233", - "60234", - "60235", - "60236", - "60237", - "60238", - "60240", - "60241", - "60242", - "60243", - "60244", - "60245", - "60246", - "60247", - "60248", - "60249", - "60250", - "60251", - "60252", - "60253", - "60254", - "60255", - "60256", - "60257", - "60258", - "60259", - "60260", - "60261", - "60262", - "60263", - "60264", - "60265", - "60266", - "60267", - "60268", - "60269", - "60270", - "60271", - "60272", - "60273", - "60274", - "60275", - "60276", - "60277", - "60278", - "60279", - "60280", - "60281", - "60282", - "60283", - "60284", - "60285", - "60286", - "60287", - "60288", - "60289", - "60290", - "60291", - "60292", - "60293", - "60294", - "60295", - "60296", - "60297", - "60298", - "60299", - "60300", - "60301", - "60302", - "60303", - "60304", - "60305", - "60306", - "60307", - "60308", - "60309", - "60310", - "60311", - "60312", - "60313", - "60314", - "60315", - "60316", - "60317", - "60318", - "60319", - "60320", - "60321", - "60322", - "60323", - "60324", - "60325", - "60326", - "60327", - "60328", - "60329", - "60330", - "60331", - "60332", - "60333", - "60334", - "60335", - "60336", - "60337", - "60338", - "60339", - "60340", - "60341", - "60342", - "60343", - "60344", - "60345", - "60346", - "60347", - "60348", - "60349", - "60350", - "60351", - "60352", - "60353", - "60354", - "60355", - "60356", - "60357", - "60358", - "60359", - "60360", - "60361", - "60362", - "60363", - "60364", - "60365", - "60366", - "60367", - "60368", - "60369", - "60370", - "60371", - "60372", - "60373", - "60374", - "60375", - "60376", - "60377", - "60378", - "60379", - "60380", - "60381", - "60382", - "60383", - "60384", - "60385", - "60386", - "60387", - "60388", - "60389", - "60390", - "60391", - "60392", - "60393", - "60394", - "60395", - "60396", - "60397", - "60398", - "60399", - "60400", - "60401", - "60402", - "60403", - "60404", - "60405", - "60406", - "60407", - "60408", - "60409", - "60410", - "60411", - "60412", - "60413", - "60414", - "60415", - "60416", - "60417", - "60418", - "60419", - "60420", - "60421", - "60422", - "60423", - "60424", - "60425", - "60426", - "60427", - "60428", - "60429", - "60430", - "60431", - "60432", - "60433", - "60434", - "60435", - "60436", - "60437", - "60438", - "60439", - "60440", - "60441", - "60442", - "60443", - "60444", - "60445", - "60446", - "60447", - "60448", - "60449", - "60450", - "60451", - "60452", - "60453", - "60454", - "60455", - "60456", - "60457", - "60458", - "60459", - "60460", - "60461", - "60462", - "60463", - "60464", - "60465", - "60466", - "60467", - "60468", - "60469", - "60470", - "60471", - "60472", - "60473", - "60474", - "60475", - "60476", - "60477", - "60478", - "60479", - "60480", - "60481", - "60482", - "60483", - "60484", - "60485", - "60486", - "60487", - "60488", - "60489", - "60490", - "60491", - "60492", - "60493", - "60494", - "60495", - "60496", - "60497", - "60498", - "60499", - "60500", - "60501", - "60502", - "60503", - "60504", - "60505", - "60506", - "60507", - "60508", - "60509", - "60510", - "60512", - "60513", - "60514", - "60515", - "60516", - "60517", - "60518", - "60519", - "60520", - "60521", - "60522", - "60523", - "60524", - "60525", - "60526", - "60527", - "60528", - "60529", - "60530", - "60531", - "60532", - "60533", - "60534", - "60535", - "60536", - "60537", - "60538", - "60539", - "60540", - "60541", - "60542", - "60543", - "60544", - "60545", - "60546", - "60547", - "60548", - "60549", - "60550", - "60551", - "60552", - "60553", - "60554", - "60555", - "60556", - "60557", - "60558", - "60559", - "60560", - "60561", - "60562", - "60563", - "60564", - "60565", - "60566", - "60567", - "60568", - "60569", - "60570", - "60571", - "60572", - "60573", - "60574", - "60575", - "60576", - "60577", - "60578", - "60579", - "60580", - "60581", - "60582", - "60583", - "60584", - "60585", - "60586", - "60587", - "60588", - "60589", - "60590", - "60591", - "60592", - "60593", - "60594", - "60595", - "60596", - "60597", - "60598", - "60599", - "60600", - "60601", - "60602", - "60603", - "60604", - "60605" + "account", + "activate-breakpoints", + "add", + "add-compact", + "add-small", + "agent", + "alert", + "archive", + "array", + "arrow-both", + "arrow-circle-down", + "arrow-circle-left", + "arrow-circle-right", + "arrow-circle-up", + "arrow-down", + "arrow-left", + "arrow-right", + "arrow-small-down", + "arrow-small-left", + "arrow-small-right", + "arrow-small-up", + "arrow-swap", + "arrow-up", + "ask", + "attach", + "attach-compact", + "azure", + "azure-devops", + "beaker", + "beaker-compact", + "beaker-stop", + "bell", + "bell-dot", + "bell-slash", + "bell-slash-dot", + "blank", + "bold", + "book", + "bookmark", + "bracket", + "bracket-dot", + "bracket-error", + "briefcase", + "broadcast", + "browser", + "bug", + "build", + "calendar", + "call-incoming", + "call-outgoing", + "case-sensitive", + "chat-export", + "chat-import", + "chat-sparkle", + "chat-sparkle-error", + "chat-sparkle-warning", + "check", + "check-all", + "check-compact", + "checklist", + "checklist-compact", + "chevron-down", + "chevron-down-compact", + "chevron-left", + "chevron-left-compact", + "chevron-right", + "chevron-right-compact", + "chevron-up", + "chevron-up-compact", + "chip", + "chrome-close", + "chrome-maximize", + "chrome-minimize", + "chrome-restore", + "circle", + "circle-filled", + "circle-filled-compact", + "circle-large", + "circle-large-filled", + "circle-large-outline", + "circle-outline", + "circle-slash", + "circle-slash-compact", + "circle-small", + "circle-small-filled", + "circle-small-filled-compact", + "circuit-board", + "claude", + "clear-all", + "clippy", + "clock", + "clockface", + "clone", + "close", + "close-all", + "close-compact", + "close-dirty", + "cloud", + "cloud-compact", + "cloud-download", + "cloud-small", + "cloud-upload", + "code", + "code-oss", + "code-review", + "coffee", + "collapse-all", + "collapse-all-compact", + "collection", + "collection-small", + "color-mode", + "combine", + "comment", + "comment-add", + "comment-compact", + "comment-discussion", + "comment-discussion-quote", + "comment-discussion-sparkle", + "comment-draft", + "comment-unresolved", + "comment-unresolved-compact", + "compare-changes", + "compass", + "compass-active", + "compass-dot", + "console", + "copilot", + "copilot-blocked", + "copilot-compact", + "copilot-error", + "copilot-in-progress", + "copilot-large", + "copilot-not-connected", + "copilot-snooze", + "copilot-success", + "copilot-unavailable", + "copilot-warning", + "copilot-warning-large", + "copy", + "coverage", + "credit-card", + "cursor", + "dash", + "dashboard", + "database", + "debug", + "debug-all", + "debug-alt", + "debug-alt-small", + "debug-breakpoint", + "debug-breakpoint-conditional", + "debug-breakpoint-conditional-disabled", + "debug-breakpoint-conditional-unverified", + "debug-breakpoint-data", + "debug-breakpoint-data-disabled", + "debug-breakpoint-data-unverified", + "debug-breakpoint-disabled", + "debug-breakpoint-function", + "debug-breakpoint-function-disabled", + "debug-breakpoint-function-unverified", + "debug-breakpoint-log", + "debug-breakpoint-log-disabled", + "debug-breakpoint-log-unverified", + "debug-breakpoint-unsupported", + "debug-breakpoint-unverified", + "debug-connected", + "debug-connected-compact", + "debug-console", + "debug-continue", + "debug-continue-small", + "debug-coverage", + "debug-disconnect", + "debug-disconnect-compact", + "debug-hint", + "debug-line-by-line", + "debug-pause", + "debug-rerun", + "debug-restart", + "debug-restart-frame", + "debug-reverse-continue", + "debug-stackframe", + "debug-stackframe-active", + "debug-stackframe-dot", + "debug-stackframe-focused", + "debug-start", + "debug-step-back", + "debug-step-into", + "debug-step-out", + "debug-step-over", + "debug-stop", + "desktop-download", + "developer-tools", + "device-camera", + "device-camera-video", + "device-desktop", + "device-mobile", + "diff", + "diff-added", + "diff-ignored", + "diff-modified", + "diff-multiple", + "diff-removed", + "diff-renamed", + "diff-sidebyside", + "diff-single", + "discard", + "download", + "edit", + "edit-code", + "edit-compact", + "edit-session", + "edit-sparkle", + "editor-layout", + "ellipsis", + "empty-window", + "eraser", + "error", + "error-compact", + "error-small", + "exclude", + "expand-all", + "export", + "extensions", + "extensions-large", + "eye", + "eye-closed", + "eye-unwatch", + "eye-watch", + "feedback", + "file", + "file-add", + "file-binary", + "file-code", + "file-directory", + "file-directory-create", + "file-media", + "file-media-compact", + "file-pdf", + "file-submodule", + "file-symlink-directory", + "file-symlink-file", + "file-text", + "file-zip", + "files", + "filter", + "filter-filled", + "flag", + "flame", + "fold", + "fold-down", + "fold-horizontal", + "fold-horizontal-filled", + "fold-up", + "fold-vertical", + "fold-vertical-filled", + "folder", + "folder-active", + "folder-compact", + "folder-library", + "folder-opened", + "folder-opened-compact", + "forward", + "game", + "gather", + "gear", + "gear-compact", + "gift", + "gist", + "gist-fork", + "gist-new", + "gist-private", + "gist-secret", + "git-branch", + "git-branch-changes", + "git-branch-compact", + "git-branch-conflicts", + "git-branch-create", + "git-branch-delete", + "git-branch-staged-changes", + "git-commit", + "git-compare", + "git-fetch", + "git-fork-private", + "git-merge", + "git-pull-request", + "git-pull-request-abandoned", + "git-pull-request-assignee", + "git-pull-request-closed", + "git-pull-request-create", + "git-pull-request-done", + "git-pull-request-draft", + "git-pull-request-go-to-changes", + "git-pull-request-label", + "git-pull-request-milestone", + "git-pull-request-new-changes", + "git-pull-request-reviewer", + "git-stash", + "git-stash-apply", + "git-stash-pop", + "github", + "github-action", + "github-alt", + "github-inverted", + "github-project", + "globe", + "go-to-editing-session", + "go-to-file", + "go-to-search", + "grabber", + "graph", + "graph-left", + "graph-line", + "graph-scatter", + "gripper", + "group-by-ref-type", + "heart", + "heart-filled", + "history", + "home", + "horizontal-rule", + "hubot", + "important", + "important-compact", + "inbox", + "indent", + "index-zero", + "info", + "insert", + "inspect", + "issue-closed", + "issue-draft", + "issue-opened", + "issue-reopened", + "issues", + "italic", + "jersey", + "json", + "kebab-horizontal", + "kebab-vertical", + "key", + "keyboard", + "keyboard-tab", + "keyboard-tab-above", + "keyboard-tab-below", + "law", + "layers", + "layers-active", + "layers-dot", + "layout", + "layout-activitybar-left", + "layout-activitybar-right", + "layout-centered", + "layout-menubar", + "layout-panel", + "layout-panel-center", + "layout-panel-dock", + "layout-panel-justify", + "layout-panel-left", + "layout-panel-off", + "layout-panel-right", + "layout-sidebar-left", + "layout-sidebar-left-dock", + "layout-sidebar-left-off", + "layout-sidebar-right", + "layout-sidebar-right-dock", + "layout-sidebar-right-off", + "layout-statusbar", + "library", + "library-compact", + "light-bulb", + "lightbulb", + "lightbulb-autofix", + "lightbulb-compact", + "lightbulb-empty", + "lightbulb-sparkle", + "link", + "link-external", + "list-filter", + "list-flat", + "list-ordered", + "list-selection", + "list-tree", + "list-unordered", + "live-share", + "loading", + "loading-compact", + "location", + "lock", + "lock-small", + "log-in", + "log-out", + "logo-github", + "magnet", + "mail", + "mail-read", + "mail-reply", + "map", + "map-filled", + "map-horizontal", + "map-horizontal-filled", + "map-vertical", + "map-vertical-filled", + "mark-github", + "markdown", + "mcp", + "megaphone", + "mention", + "menu", + "merge", + "merge-into", + "mic", + "mic-filled", + "microscope", + "milestone", + "mirror", + "mirror-private", + "mirror-public", + "more", + "mortar-board", + "move", + "multiple-windows", + "music", + "mute", + "new-collection", + "new-file", + "new-folder", + "new-session", + "newline", + "no-newline", + "note", + "notebook", + "notebook-template", + "octoface", + "open-in-product", + "open-in-window", + "open-preview", + "openai", + "organization", + "organization-filled", + "organization-outline", + "output", + "package", + "paintcan", + "pass", + "pass-compact", + "pass-filled", + "pass-filled-compact", + "pencil", + "percentage", + "person", + "person-add", + "person-filled", + "person-follow", + "person-outline", + "piano", + "pie-chart", + "pin", + "pinned", + "pinned-dirty", + "play", + "play-circle", + "plug", + "plus", + "preserve-case", + "preview", + "primitive-dot", + "primitive-square", + "project", + "project-compact", + "pulse", + "python", + "question", + "quote", + "quotes", + "radio-tower", + "reactions", + "record", + "record-keys", + "record-keys-compact", + "record-small", + "redo", + "references", + "refresh", + "refresh-compact", + "regex", + "remote", + "remote-compact", + "remote-explorer", + "remove", + "remove-close", + "remove-small", + "rename", + "repl", + "replace", + "replace-all", + "reply", + "repo", + "repo-clone", + "repo-compact", + "repo-create", + "repo-delete", + "repo-fetch", + "repo-force-push", + "repo-forked", + "repo-forked-compact", + "repo-pinned", + "repo-pull", + "repo-push", + "repo-selected", + "repo-sync", + "report", + "request-changes", + "robot", + "rocket", + "rocket-compact", + "root-folder", + "root-folder-opened", + "rss", + "ruby", + "run", + "run-above", + "run-all", + "run-all-coverage", + "run-below", + "run-coverage", + "run-errors", + "run-with-deps", + "save", + "save-all", + "save-as", + "screen-cut", + "screen-full", + "screen-normal", + "search", + "search-compact", + "search-fuzzy", + "search-large", + "search-save", + "search-sparkle", + "search-stop", + "selection", + "send", + "send-to-remote-agent", + "server", + "server-environment", + "server-process", + "session-in-progress", + "session-in-progress-compact", + "settings", + "settings-gear", + "share", + "share-window", + "shield", + "shield-compact", + "sign-in", + "sign-out", + "skip", + "smiley", + "snake", + "sort-percentage", + "sort-precedence", + "source-control", + "sparkle", + "sparkle-compact", + "sparkle-filled", + "split-horizontal", + "split-vertical", + "squirrel", + "star", + "star-add", + "star-delete", + "star-empty", + "star-full", + "star-half", + "stop", + "stop-circle", + "strikethrough", + "surround-with", + "symbol-array", + "symbol-boolean", + "symbol-class", + "symbol-color", + "symbol-color-compact", + "symbol-constant", + "symbol-constructor", + "symbol-enum", + "symbol-enum-member", + "symbol-event", + "symbol-field", + "symbol-file", + "symbol-folder", + "symbol-function", + "symbol-interface", + "symbol-key", + "symbol-keyword", + "symbol-method", + "symbol-method-arrow", + "symbol-misc", + "symbol-module", + "symbol-namespace", + "symbol-null", + "symbol-number", + "symbol-numeric", + "symbol-object", + "symbol-operator", + "symbol-package", + "symbol-parameter", + "symbol-property", + "symbol-reference", + "symbol-ruler", + "symbol-snippet", + "symbol-string", + "symbol-struct", + "symbol-structure", + "symbol-text", + "symbol-type-parameter", + "symbol-unit", + "symbol-value", + "symbol-variable", + "sync", + "sync-compact", + "sync-ignored", + "table", + "tag", + "tag-add", + "tag-remove", + "target", + "tasklist", + "telescope", + "terminal", + "terminal-bash", + "terminal-cmd", + "terminal-compact", + "terminal-debian", + "terminal-decoration-error", + "terminal-decoration-incomplete", + "terminal-decoration-mark", + "terminal-decoration-success", + "terminal-git-bash", + "terminal-linux", + "terminal-powershell", + "terminal-secure", + "terminal-tmux", + "terminal-ubuntu", + "text-size", + "thinking", + "three-bars", + "thumbsdown", + "thumbsdown-filled", + "thumbsup", + "thumbsup-filled", + "tools", + "trash", + "trashcan", + "triangle-down", + "triangle-left", + "triangle-right", + "triangle-up", + "twitter", + "type-hierarchy", + "type-hierarchy-sub", + "type-hierarchy-super", + "unarchive", + "unfold", + "ungroup-by-ref-type", + "unlock", + "unmute", + "unpin", + "unverified", + "variable", + "variable-group", + "verified", + "verified-filled", + "versions", + "vm", + "vm-active", + "vm-connect", + "vm-outline", + "vm-pending", + "vm-running", + "vm-small", + "vr", + "vscode", + "vscode-insiders", + "wand", + "warning", + "warning-compact", + "watch", + "whitespace", + "whole-word", + "window", + "window-active", + "window-compact", + "word-wrap", + "workspace-trusted", + "workspace-unknown", + "workspace-untrusted", + "worktree", + "worktree-compact", + "worktree-small", + "wrench", + "wrench-subaction", + "x", + "zap", + "zoom-in", + "zoom-out" ]; diff --git a/src/detection.js b/src/detection.js index b843412c..e58c3f40 100644 --- a/src/detection.js +++ b/src/detection.js @@ -453,6 +453,39 @@ function resolveTagCaptureRange( context, match, rawStartOffset ) return undefined; } +function canExtendTagPlaceholderMatch( context ) +{ + return context.resourceConfig.enableMultiLine !== true && + context.resourceConfig.regex.indexOf( "$TAGS" ) > -1; +} + +function shouldExtendTagPlaceholderMatchToLineEnd( allowExtension, rawEndOffset, lineBounds ) +{ + return allowExtension === true && lineBounds.endOffset > rawEndOffset; +} + +function extractRegexMatchText( context, matchText, preferredTagOffset ) +{ + if( context.resourceConfig.regex.indexOf( "$TAGS" ) === -1 ) + { + return { + tag: matchText, + withoutTag: "", + before: "", + after: "", + tagOffset: 0, + subTag: undefined, + subTagOffset: undefined + }; + } + + return utils.extractTag( matchText, undefined, context.uri, preferredTagOffset, { + resourceConfig: context.resourceConfig, + tagRegex: context.tagRegex, + subTagRegex: context.subTagRegex + } ); +} + function splitTextLines( text ) { return splitPhysicalLines( text, 0 ); @@ -978,6 +1011,7 @@ function normalizeRegexExecMatchWithContext( context, match ) var tagCaptureRange = resolveTagCaptureRange( context, match, rawStartOffset ); var tagStartOffset = tagCaptureRange ? tagCaptureRange[ 0 ] : undefined; var tagEndOffset = tagCaptureRange ? tagCaptureRange[ 1 ] : undefined; + var allowLineExtension = canExtendTagPlaceholderMatch( context ); if( tagCaptureRange ) { @@ -993,19 +1027,25 @@ function normalizeRegexExecMatchWithContext( context, match ) matchText = context.text.slice( logicalStartOffset, logicalEndOffset ); preferredTagOffset = tagStartOffset - logicalStartOffset; } - else if( context.resourceConfig.isDefaultRegex === true && tagLineBounds.endOffset > rawEndOffset ) + else if( shouldExtendTagPlaceholderMatchToLineEnd( allowLineExtension, rawEndOffset, tagLineBounds ) ) { logicalEndOffset = tagLineBounds.endOffset; matchText = context.text.slice( logicalStartOffset, logicalEndOffset ); preferredTagOffset = tagStartOffset - logicalStartOffset; } } + else if( allowLineExtension === true ) + { + var rawLineBounds = getLineBoundsForOffset( context.text, context.lineOffsets, rawStartOffset ); - var extracted = utils.extractTag( matchText, undefined, context.uri, preferredTagOffset, { - resourceConfig: context.resourceConfig, - tagRegex: context.tagRegex, - subTagRegex: context.subTagRegex - } ); + if( shouldExtendTagPlaceholderMatchToLineEnd( allowLineExtension, rawEndOffset, rawLineBounds ) ) + { + logicalEndOffset = rawLineBounds.endOffset; + matchText = context.text.slice( logicalStartOffset, logicalEndOffset ); + } + } + + var extracted = extractRegexMatchText( context, matchText, preferredTagOffset ); var actualTag = extracted.tag && extracted.tag.length > 0 ? extracted.tag : matchText; if( tagStartOffset === undefined ) diff --git a/src/icons.js b/src/icons.js index 18bbe0d1..9eceb207 100644 --- a/src/icons.js +++ b/src/icons.js @@ -148,8 +148,7 @@ function getFileBackedIcon( context, tag, debug ) } else if( iconName && utils.isCodicon( iconName ) ) { - var codiconName = iconName.trim().substr( 2, iconName.trim().length - 3 ); - darkIconPath = createCodiconFile( context, codiconName, colour ); + darkIconPath = createCodiconFile( context, utils.getCodiconName( iconName ), colour ); lightIconPath = darkIconPath; } else if( iconName ) @@ -179,12 +178,13 @@ function getTreeIcon( context, tag, debug ) if( iconName && utils.isCodicon( iconName ) ) { + var codiconName = utils.getCodiconName( iconName ); var themeColour; if( themeColourNames.indexOf( colour ) > -1 ) { themeColour = new vscode.ThemeColor( colour ); } - return new vscode.ThemeIcon( iconName.trim().substr( 2, iconName.trim().length - 3 ), themeColour ); + return new vscode.ThemeIcon( codiconName, themeColour ); } return getFileBackedIcon( context, tag, debug ); @@ -204,7 +204,7 @@ function validateIcons( workspace ) { if( utils.isCodicon( icon ) ) { - var codicon = icon.substr( 2, icon.length - 3 ); + var codicon = utils.getCodiconName( icon ); if( codiconNames.indexOf( codicon ) === -1 && productIconNames.indexOf( codicon ) === -1 ) { invalidIcons.push( setting + '.icon(' + icon + ')' ); diff --git a/src/utils.js b/src/utils.js index fc2a545a..a08fe301 100644 --- a/src/utils.js +++ b/src/utils.js @@ -705,9 +705,20 @@ function setRgbAlpha( rgb, alpha ) return rgb; } +function getCodiconName( icon ) +{ + if( typeof icon !== 'string' ) + { + return undefined; + } + + var match = icon.trim().match( /^\$\(([a-z0-9-]+)\)$/i ); + return match ? match[ 1 ] : undefined; +} + function isCodicon( icon ) { - return icon.trim().indexOf( "$(" ) === 0; + return getCodiconName( icon ) !== undefined; } function toGlobArray( globs ) @@ -754,5 +765,6 @@ module.exports.formatExportPath = formatExportPath; module.exports.complementaryColour = complementaryColour; module.exports.isValidColour = isValidColour; module.exports.setRgbAlpha = setRgbAlpha; +module.exports.getCodiconName = getCodiconName; module.exports.isCodicon = isCodicon; module.exports.toGlobArray = toGlobArray; diff --git a/test/detection.behavior.test.js b/test/detection.behavior.test.js index af148ae6..ec3bb7db 100644 --- a/test/detection.behavior.test.js +++ b/test/detection.behavior.test.js @@ -162,6 +162,52 @@ QUnit.module( "behavioral detection", function( hooks ) assert.equal( results[ 1 ].displayText, "pending task" ); } ); + QUnit.test( "issue #28 legacy prefix regex keeps rendered text after tag", function( assert ) + { + utils.init( createConfig( { + tagList: [ "TODO", "FIXME", "BUG", "HACK", "[ ]", "[x]" ], + regexSource: "(?:(?://|#|`). This change introduces new utility functions to accurately identify and remove these trailing end tokens from the detected match text. The new `trimTrailingEndToken` and `trimTrailingMultiLineCommentEnd` functions are integrated into the match normalization process, ensuring that the `displayText` and `after` properties only contain the relevant comment content. To support this, a `resolveBlockCommentPattern` utility centralizes the logic for determining the correct block comment pattern for a given file type, including language aliases (e.g., Markdown to HTML, Haskell to C++). Fixes #28 --- src/detection.js | 98 +++++++++++++++++++++++++++++---- src/utils.js | 37 +++++++++---- test/detection.behavior.test.js | 64 +++++++++++++++++++++ test/tests.js | 14 +++++ 4 files changed, 191 insertions(+), 22 deletions(-) diff --git a/src/detection.js b/src/detection.js index e58c3f40..a1b298ad 100644 --- a/src/detection.js +++ b/src/detection.js @@ -122,6 +122,24 @@ function splitPhysicalLines( text, startOffset ) return lines; } +function trimTrailingEndToken( text, contentStart, endToken ) +{ + var trimmedRight = text.replace( /[ \t]+$/, '' ); + + if( !endToken || !trimmedRight.endsWith( endToken ) ) + { + return text.length; + } + + var contentEnd = trimmedRight.length - endToken.length; + while( contentEnd > contentStart && text[ contentEnd - 1 ] === ' ' ) + { + contentEnd--; + } + + return contentEnd; +} + function trimCommentLine( lineText, options ) { var rawText = lineText; @@ -183,15 +201,7 @@ function trimCommentLine( lineText, options ) if( endToken ) { - var trimmedRight = rawText.replace( /[ \t]+$/, '' ); - if( trimmedRight.endsWith( endToken ) ) - { - contentEnd = rawText.lastIndexOf( endToken ); - while( contentEnd > contentStart && rawText[ contentEnd - 1 ] === ' ' ) - { - contentEnd--; - } - } + contentEnd = trimTrailingEndToken( rawText, contentStart, endToken ); } } @@ -486,6 +496,67 @@ function extractRegexMatchText( context, matchText, preferredTagOffset ) } ); } +function resolveCommentEndTrimPattern( context ) +{ + return utils.resolveBlockCommentPattern( context.patternFileName || getUriFsPath( context.uri ) ).pattern; +} + +function createNormalizedMatchTextResult( text, endOffset ) +{ + return { + text: text, + endOffset: endOffset + }; +} + +function findTrailingMultiLineCommentEnd( pattern, text ) +{ + return pattern.multiLineComment.reduce( function( best, entry ) + { + var contentEnd; + + if( + typeof ( entry.end ) === 'string' && + ( best === undefined || entry.end.length > best.entry.end.length ) && + ( contentEnd = trimTrailingEndToken( text, 0, entry.end ) ) < text.length && + findTokenStart( text, entry.start, 0 ) !== undefined + ) + { + return { + entry: entry, + contentEnd: contentEnd + }; + } + + return best; + }, undefined ); +} + +function trimTrailingMultiLineCommentEnd( context, matchText, logicalStartOffset, logicalEndOffset ) +{ + var pattern = resolveCommentEndTrimPattern( context ); + + if( !pattern || !Array.isArray( pattern.multiLineComment ) ) + { + return createNormalizedMatchTextResult( matchText, logicalEndOffset ); + } + + var lineBounds = getLineBoundsForOffset( context.text, context.lineOffsets, logicalStartOffset ); + var linePrefix = context.text.slice( lineBounds.startOffset, logicalStartOffset ); + var candidateText = linePrefix + matchText; + var trailingEnd = findTrailingMultiLineCommentEnd( pattern, candidateText ); + + if( !trailingEnd ) + { + return createNormalizedMatchTextResult( matchText, logicalEndOffset ); + } + + return createNormalizedMatchTextResult( + candidateText.slice( linePrefix.length, trailingEnd.contentEnd ), + lineBounds.startOffset + trailingEnd.contentEnd + ); +} + function splitTextLines( text ) { return splitPhysicalLines( text, 0 ); @@ -735,7 +806,7 @@ function collectCommentPatternMatches( uri, text, pattern, lineOffsets, resource function resolveMarkdownCommentPattern() { - var markdownCommentPattern = utils.getCommentPattern( '.html' ); + var markdownCommentPattern = utils.resolveBlockCommentPattern( '.md' ).pattern; if( markdownCommentPattern === undefined ) { @@ -1045,6 +1116,13 @@ function normalizeRegexExecMatchWithContext( context, match ) } } + if( allowLineExtension === true ) + { + var trimmedMatch = trimTrailingMultiLineCommentEnd( context, matchText, logicalStartOffset, logicalEndOffset ); + matchText = trimmedMatch.text; + logicalEndOffset = trimmedMatch.endOffset; + } + var extracted = extractRegexMatchText( context, matchText, preferredTagOffset ); var actualTag = extracted.tag && extracted.tag.length > 0 ? extracted.tag : matchText; diff --git a/src/utils.js b/src/utils.js index a08fe301..5e16f140 100644 --- a/src/utils.js +++ b/src/utils.js @@ -149,30 +149,42 @@ function getCommentPatternRegex( fileName ) } } -function removeBlockComments( text, fileName ) +function resolveBlockCommentPattern( fileName ) { - var extension = path.extname( fileName ); - var normalisedFileName = normaliseCommentPatternFileName( fileName ); - var commentPattern = getCommentPattern( fileName ); + var extension = path.extname( fileName ).toLowerCase(); + var patternFileName = normaliseCommentPatternFileName( fileName ); + var pattern = getCommentPattern( fileName ); - if( extension == ".hs" ) + if( extension === ".hs" ) { - commentPattern = getCommentPattern( ".cpp" ); - normalisedFileName = ".cpp"; + patternFileName = ".cpp"; + pattern = getCommentPattern( patternFileName ); } - else if( commentPattern && commentPattern.name === 'Markdown' ) + else if( pattern && pattern.name === 'Markdown' ) { - commentPattern = getCommentPattern( ".html" ); - normalisedFileName = ".html"; + patternFileName = ".html"; + pattern = getCommentPattern( patternFileName ); } + return { + extension: extension, + fileName: patternFileName, + pattern: pattern + }; +} + +function removeBlockComments( text, fileName ) +{ + var blockCommentPattern = resolveBlockCommentPattern( fileName ); + var commentPattern = blockCommentPattern.pattern; + if( commentPattern && commentPattern.multiLineComment && commentPattern.multiLineComment.length > 0 ) { - commentPattern = getCommentPatternRegex( normalisedFileName ); + commentPattern = getCommentPatternRegex( blockCommentPattern.fileName ); if( commentPattern && commentPattern.regex ) { var regex = commentPattern.regex; - if( extension == ".hs" ) + if( blockCommentPattern.extension === ".hs" ) { var source = regex.source; var flags = regex.flags; @@ -744,6 +756,7 @@ module.exports.removeBlockComments = removeBlockComments; module.exports.removeLineComments = removeLineComments; module.exports.getCommentPattern = getCommentPattern; module.exports.getCommentPatternRegex = getCommentPatternRegex; +module.exports.resolveBlockCommentPattern = resolveBlockCommentPattern; module.exports.getResourceConfig = getResourceConfig; module.exports.getTagRegexSource = getTagRegexSource; module.exports.supportsRegExpIndices = supportsRegExpIndices; diff --git a/test/detection.behavior.test.js b/test/detection.behavior.test.js index ec3bb7db..1cad320a 100644 --- a/test/detection.behavior.test.js +++ b/test/detection.behavior.test.js @@ -208,6 +208,70 @@ QUnit.module( "behavioral detection", function( hooks ) assert.equal( result.column, 4 ); } ); + QUnit.test( "issue #28 legacy prefix regex strips markdown html comment end", function( assert ) + { + utils.init( createConfig( { + tagList: [ "TODO", "FIXME", "BUG", "HACK", "[ ]", "[x]" ], + regexSource: "(?:(?://|#|" + ); + + assert.equal( results.length, 1 ); + assert.equal( results[ 0 ].actualTag, "TODO" ); + assert.equal( results[ 0 ].displayText, "html comment text should appear after TODO" ); + assert.equal( results[ 0 ].after, "html comment text should appear after TODO" ); + } ); + + QUnit.test( "issue #28 legacy prefix regex strips block comment end", function( assert ) + { + utils.init( createConfig( { + tagList: [ "TODO", "FIXME", "BUG", "HACK", "[ ]", "[x]" ], + regexSource: "(?:(?://|#|", + match: "