From a63793078dac7ac2b4c454e4b31acdaeddb1d22b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 07:45:08 +0000 Subject: [PATCH 1/4] feat(lsp): gate symbol actions on the highlighter's token class The symbol menu and hover suppress themselves on comments and string literals. Answering that needed a second parser; the diff viewer had already made the same call while painting the token the user clicked. Vendor three patches that surface it. `@pierre/diffs` asks Shiki for the TextMate token class and stamps it on the span it already annotates with `data-char`. Shiki drops that class twice on the way through a dual-theme render -- once merging the per-theme tokenizations, once flattening the variants into CSS variables -- so both rebuilds now carry it. A viewer rendering one theme keeps `type` on its own and hides both gaps, which is why the guard test renders two. `targetOf` reads the attribute off the clicked span, so `openSymbolMenu` and `requestHover` answer synchronously instead of awaiting a parse. The patches are a local stand-in until this lands upstream; tree-sitter still runs, and still owns keywords, which `StandardTokenType` cannot express. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01P8ro9sQLfwxHnmbyDDBmJ3 --- package-lock.json | 705 +++++++++++++++++++++++-- package.json | 2 + patches/@pierre+diffs+1.3.6.patch | 209 ++++++++ patches/@shikijs+core+4.4.3.patch | 14 + patches/@shikijs+primitive+4.4.3.patch | 12 + src/client/lsp/target.ts | 17 + src/client/review/ReviewPane.tsx | 8 +- src/client/store.ts | 7 +- test/client/ReviewPane.test.ts | 2 +- test/client/tokenType.test.ts | 66 +++ 10 files changed, 989 insertions(+), 53 deletions(-) create mode 100644 patches/@pierre+diffs+1.3.6.patch create mode 100644 patches/@shikijs+core+4.4.3.patch create mode 100644 patches/@shikijs+primitive+4.4.3.patch create mode 100644 test/client/tokenType.test.ts diff --git a/package-lock.json b/package-lock.json index 8ba1de9..704982d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "jsdom": "^30.0.1", "oxfmt": "^0.67.0", "oxlint": "^1.82.0", + "patch-package": "^8.0.1", "tailwindcss": "^4.3.3", "tree-sitter-wasms": "^0.1.13", "tsdown": "^0.22.14", @@ -1224,9 +1225,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1244,9 +1242,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1264,9 +1259,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1284,9 +1276,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1304,9 +1293,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1324,9 +1310,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1344,9 +1327,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1364,9 +1344,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1840,6 +1817,17 @@ "react-dom": "^18.3.1 || ^19.0.0" } }, + "node_modules/@pierre/trees/node_modules/@pierre/theme": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pierre/theme/-/theme-1.1.0.tgz", + "integrity": "sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "vscode": "^1.0.0" + } + }, "node_modules/@pierre/trees/node_modules/@pierre/theming": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pierre/theming/-/theming-1.0.0.tgz", @@ -2437,9 +2425,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2461,9 +2446,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2485,9 +2467,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2509,9 +2488,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2684,9 +2660,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2704,9 +2677,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2724,9 +2694,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2744,9 +2711,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3234,6 +3198,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/@yuku-codegen/binding-android-arm64": { "version": "0.8.7", "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-android-arm64/-/binding-android-arm64-0.8.7.tgz", @@ -3646,6 +3617,19 @@ "require-from-string": "^2.0.2" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", @@ -3690,6 +3674,56 @@ "node": ">=20.19.0" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001810", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", @@ -3803,6 +3837,22 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3861,6 +3911,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -3947,6 +4012,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/defu": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", @@ -4016,6 +4099,21 @@ } } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.420", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", @@ -4060,6 +4158,26 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", @@ -4067,6 +4185,19 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -4218,6 +4349,44 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4233,6 +4402,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4243,6 +4422,45 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "5.0.0-beta.5", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.5.tgz", @@ -4259,6 +4477,19 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4276,6 +4507,45 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -4441,6 +4711,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", @@ -4451,6 +4737,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -4470,6 +4766,33 @@ "dev": true, "license": "MIT" }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4551,6 +4874,26 @@ "node": ">=6" } }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4564,6 +4907,39 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -4880,6 +5256,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -5720,6 +6106,33 @@ ], "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -5787,6 +6200,16 @@ "node": ">=18" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -5818,6 +6241,23 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/oxfmt": { "version": "0.67.0", "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.67.0.tgz", @@ -5957,6 +6397,59 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -6361,6 +6854,47 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shiki": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", @@ -6387,6 +6921,16 @@ "dev": true, "license": "ISC" }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6626,6 +7170,29 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tough-cookie": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", @@ -6947,6 +7514,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", @@ -7264,6 +7841,22 @@ "node": "^22.14.0 || >=24.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -7326,6 +7919,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yuku-ast": { "version": "0.8.7", "resolved": "https://registry.npmjs.org/yuku-ast/-/yuku-ast-0.8.7.tgz", diff --git a/package.json b/package.json index d4e5bb4..43eb2ac 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "format": "oxfmt", "format:check": "oxfmt --check", "test": "vitest run", + "prepare": "patch-package", "prepack": "npm run build" }, "dependencies": { @@ -75,6 +76,7 @@ "jsdom": "^30.0.1", "oxfmt": "^0.67.0", "oxlint": "^1.82.0", + "patch-package": "^8.0.1", "tailwindcss": "^4.3.3", "tree-sitter-wasms": "^0.1.13", "tsdown": "^0.22.14", diff --git a/patches/@pierre+diffs+1.3.6.patch b/patches/@pierre+diffs+1.3.6.patch new file mode 100644 index 0000000..b0bfc4b --- /dev/null +++ b/patches/@pierre+diffs+1.3.6.patch @@ -0,0 +1,209 @@ +diff --git a/node_modules/@pierre/diffs/dist/utils/createTransformerWithState.js b/node_modules/@pierre/diffs/dist/utils/createTransformerWithState.js +index 1ed89b0..dcfde3b 100644 +--- a/node_modules/@pierre/diffs/dist/utils/createTransformerWithState.js ++++ b/node_modules/@pierre/diffs/dist/utils/createTransformerWithState.js +@@ -3,6 +3,7 @@ import { processLine } from "./processLine.js"; + import { wrapTokenFragments } from "./wrapTokenFragments.js"; + import { transformerStyleToClass } from "@shikijs/transformers"; + //#region src/utils/createTransformerWithState.ts ++const TOKEN_TYPE_ATTR = { 1: "comment", 2: "string", 3: "regex" }; + function createTransformerWithState(useTokenTransformer = false, useCSSClasses = false) { + const state = { lineInfo: [] }; + const transformers = [{ +@@ -43,6 +44,8 @@ function createTransformerWithState(useTokenTransformer = false, useCSSClasses = + if (token?.offset != null && token.content != null) { + const tokenChar = token.__lineChar; + if (tokenChar != null) hast.properties["data-char"] = tokenChar; ++ const tokenType = TOKEN_TYPE_ATTR[token.type]; ++ if (tokenType != null) hast.properties["data-token-type"] = tokenType; + return hast; + } + return hast; +diff --git a/node_modules/@pierre/diffs/dist/utils/renderDiffWithHighlighter.js b/node_modules/@pierre/diffs/dist/utils/renderDiffWithHighlighter.js +index a6cb642..1a4888c 100644 +--- a/node_modules/@pierre/diffs/dist/utils/renderDiffWithHighlighter.js ++++ b/node_modules/@pierre/diffs/dist/utils/renderDiffWithHighlighter.js +@@ -229,7 +229,8 @@ function renderTwoFiles({ deletionFile, additionFile, deletionInfo, additionInfo + decorations: void 0, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + } : { + ...options, + lang: "text", +@@ -238,7 +239,8 @@ function renderTwoFiles({ deletionFile, additionFile, deletionInfo, additionInfo + decorations: void 0, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + })(); + return { +diff --git a/node_modules/@pierre/diffs/dist/utils/renderFileWithHighlighter.js b/node_modules/@pierre/diffs/dist/utils/renderFileWithHighlighter.js +index 14391d5..05e3c4d 100644 +--- a/node_modules/@pierre/diffs/dist/utils/renderFileWithHighlighter.js ++++ b/node_modules/@pierre/diffs/dist/utils/renderFileWithHighlighter.js +@@ -36,7 +36,8 @@ function renderFileWithHighlighter(file, highlighter, { theme = DEFAULT_THEMES, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), + tokenizeMaxLineLength, +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + return { + lang, +@@ -45,7 +46,8 @@ function renderFileWithHighlighter(file, highlighter, { theme = DEFAULT_THEMES, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), + tokenizeMaxLineLength, +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + })(); + const highlightedLines = getLineNodes(highlighter.codeToHast(isWindowedHighlight ? extractWindowedFileContent(lines ?? linesFromFileContents(file.contents), startingLine, totalLines) : file.contents, hastConfig)); +diff --git a/node_modules/@pierre/diffs/dist/worker/worker-portable.js b/node_modules/@pierre/diffs/dist/worker/worker-portable.js +index 8f25249..81909c4 100644 +--- a/node_modules/@pierre/diffs/dist/worker/worker-portable.js ++++ b/node_modules/@pierre/diffs/dist/worker/worker-portable.js +@@ -3109,6 +3109,7 @@ function codeToTokensWithThemes(primitive, code, options, codeToTokensBaseFn = c + offset: _token.offset + }; + if ("includeExplanation" in options && options.includeExplanation) mergedToken.explanation = _token.explanation; ++ if (_token.type != null) mergedToken.type = _token.type; + tokens.forEach((t, themeIdx) => { + const { content: _, explanation: __, offset: ___, ...styles } = t[lineIdx][tokenIdx]; + mergedToken.variants[themes[themeIdx].color] = styles; +@@ -6332,7 +6333,8 @@ function flatTokenVariants(merged, variantsOrder, cssVariablePrefix, defaultColo + const token = { + content: merged.content, + explanation: merged.explanation, +- offset: merged.offset ++ offset: merged.offset, ++ type: merged.type + }; + const styles = variantsOrder.map((t) => getTokenStyleObject(merged.variants[t])); + const styleKeys = new Set(styles.flatMap((t) => Object.keys(t))); +@@ -12257,6 +12259,7 @@ function setTokenChar(node, char) { + } + //#endregion + //#region src/utils/createTransformerWithState.ts ++const TOKEN_TYPE_ATTR = { 1: "comment", 2: "string", 3: "regex" }; + function createTransformerWithState(useTokenTransformer = false, useCSSClasses = false) { + const state = { lineInfo: [] }; + const transformers = [{ +@@ -12297,6 +12300,8 @@ function createTransformerWithState(useTokenTransformer = false, useCSSClasses = + if (token?.offset != null && token.content != null) { + const tokenChar = token.__lineChar; + if (tokenChar != null) hast.properties["data-char"] = tokenChar; ++ const tokenType = TOKEN_TYPE_ATTR[token.type]; ++ if (tokenType != null) hast.properties["data-token-type"] = tokenType; + return hast; + } + return hast; +@@ -13345,7 +13350,8 @@ function renderTwoFiles({ deletionFile, additionFile, deletionInfo, additionInfo + decorations: void 0, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + } : { + ...options, + lang: "text", +@@ -13354,7 +13360,8 @@ function renderTwoFiles({ deletionFile, additionFile, deletionInfo, additionInfo + decorations: void 0, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + })(); + return { +@@ -13465,7 +13472,8 @@ function renderFileWithHighlighter(file, highlighter, { theme = DEFAULT_THEMES, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), + tokenizeMaxLineLength, +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + return { + lang, +@@ -13474,7 +13482,8 @@ function renderFileWithHighlighter(file, highlighter, { theme = DEFAULT_THEMES, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), + tokenizeMaxLineLength, +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + })(); + const highlightedLines = getLineNodes(highlighter.codeToHast(isWindowedHighlight ? extractWindowedFileContent(lines ?? linesFromFileContents(file.contents), startingLine, totalLines) : file.contents, hastConfig)); +diff --git a/node_modules/@pierre/diffs/dist/worker/worker.js b/node_modules/@pierre/diffs/dist/worker/worker.js +index 09c283a..5c3b2b1 100644 +--- a/node_modules/@pierre/diffs/dist/worker/worker.js ++++ b/node_modules/@pierre/diffs/dist/worker/worker.js +@@ -571,6 +571,7 @@ function setTokenChar(node, char) { + } + //#endregion + //#region src/utils/createTransformerWithState.ts ++const TOKEN_TYPE_ATTR = { 1: "comment", 2: "string", 3: "regex" }; + function createTransformerWithState(useTokenTransformer = false, useCSSClasses = false) { + const state = { lineInfo: [] }; + const transformers = [{ +@@ -611,6 +612,8 @@ function createTransformerWithState(useTokenTransformer = false, useCSSClasses = + if (token?.offset != null && token.content != null) { + const tokenChar = token.__lineChar; + if (tokenChar != null) hast.properties["data-char"] = tokenChar; ++ const tokenType = TOKEN_TYPE_ATTR[token.type]; ++ if (tokenType != null) hast.properties["data-token-type"] = tokenType; + return hast; + } + return hast; +@@ -1516,7 +1519,8 @@ function renderTwoFiles({ deletionFile, additionFile, deletionInfo, additionInfo + decorations: void 0, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + } : { + ...options, + lang: "text", +@@ -1525,7 +1529,8 @@ function renderTwoFiles({ deletionFile, additionFile, deletionInfo, additionInfo + decorations: void 0, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + })(); + return { +@@ -1636,7 +1641,8 @@ function renderFileWithHighlighter(file, highlighter, { theme = DEFAULT_THEMES, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), + tokenizeMaxLineLength, +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + return { + lang, +@@ -1645,7 +1651,8 @@ function renderFileWithHighlighter(file, highlighter, { theme = DEFAULT_THEMES, + defaultColor: false, + cssVariablePrefix: formatCSSVariablePrefix("token"), + tokenizeMaxLineLength, +- tokenizeTimeLimit: 0 ++ tokenizeTimeLimit: 0, ++ includeExplanation: "tokenType" + }; + })(); + const highlightedLines = getLineNodes(highlighter.codeToHast(isWindowedHighlight ? extractWindowedFileContent(lines ?? linesFromFileContents(file.contents), startingLine, totalLines) : file.contents, hastConfig)); diff --git a/patches/@shikijs+core+4.4.3.patch b/patches/@shikijs+core+4.4.3.patch new file mode 100644 index 0000000..4bf599b --- /dev/null +++ b/patches/@shikijs+core+4.4.3.patch @@ -0,0 +1,14 @@ +diff --git a/node_modules/@shikijs/core/dist/index.mjs b/node_modules/@shikijs/core/dist/index.mjs +index ceab61a..f119171 100644 +--- a/node_modules/@shikijs/core/dist/index.mjs ++++ b/node_modules/@shikijs/core/dist/index.mjs +@@ -165,7 +165,8 @@ function flatTokenVariants(merged, variantsOrder, cssVariablePrefix, defaultColo + const token = { + content: merged.content, + explanation: merged.explanation, +- offset: merged.offset ++ offset: merged.offset, ++ type: merged.type + }; + const styles = variantsOrder.map((t) => getTokenStyleObject(merged.variants[t])); + const styleKeys = new Set(styles.flatMap((t) => Object.keys(t))); diff --git a/patches/@shikijs+primitive+4.4.3.patch b/patches/@shikijs+primitive+4.4.3.patch new file mode 100644 index 0000000..3ba8efd --- /dev/null +++ b/patches/@shikijs+primitive+4.4.3.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/@shikijs/primitive/dist/index.mjs b/node_modules/@shikijs/primitive/dist/index.mjs +index 5c954ae..3bdaa9a 100644 +--- a/node_modules/@shikijs/primitive/dist/index.mjs ++++ b/node_modules/@shikijs/primitive/dist/index.mjs +@@ -774,6 +774,7 @@ function codeToTokensWithThemes(primitive, code, options, codeToTokensBaseFn = c + offset: _token.offset + }; + if ("includeExplanation" in options && options.includeExplanation) mergedToken.explanation = _token.explanation; ++ if (_token.type != null) mergedToken.type = _token.type; + tokens.forEach((t, themeIdx) => { + const { content: _, explanation: __, offset: ___, ...styles } = t[lineIdx][tokenIdx]; + mergedToken.variants[themes[themeIdx].color] = styles; diff --git a/src/client/lsp/target.ts b/src/client/lsp/target.ts index 1f35685..efc0d94 100644 --- a/src/client/lsp/target.ts +++ b/src/client/lsp/target.ts @@ -15,6 +15,23 @@ export interface TokenTarget { /** 0-based UTF-16 offset in the line. */ col: number; text: string; + /** The highlighter's classification of the rendered token; absent where it made none. */ + tokenType?: HighlightTokenType | null; +} + +/** Shiki's TextMate token classes, as `@pierre/diffs` renders them onto token spans. */ +export type HighlightTokenType = 'comment' | 'string' | 'regex'; + +const TOKEN_TYPES: readonly string[] = ['comment', 'string', 'regex']; + +/** + * The highlighter's class for the token `el` renders, read from the span it already + * painted. Synchronous: the diff viewer classified this text when it tokenized the + * file, so a click needs no parse of its own. + */ +export function tokenTypeAt(el: Element | null): HighlightTokenType | null { + const type = el?.closest('[data-token-type]')?.dataset.tokenType; + return type != null && TOKEN_TYPES.includes(type) ? (type as HighlightTokenType) : null; } // Module state, not store state: hover fires constantly and must not re-render. diff --git a/src/client/review/ReviewPane.tsx b/src/client/review/ReviewPane.tsx index 97a4398..e513151 100644 --- a/src/client/review/ReviewPane.tsx +++ b/src/client/review/ReviewPane.tsx @@ -22,7 +22,7 @@ import { HoverTooltip, hoverControl } from '../lsp/HoverTooltip.js'; import { ReferencesList } from '../lsp/ReferencesList.js'; import { SymbolMenu } from '../lsp/SymbolMenu.js'; import { SymbolPicker } from '../lsp/SymbolPicker.js'; -import { lspTarget, schemaHoverOnly, type TokenTarget } from '../lsp/target.js'; +import { lspTarget, schemaHoverOnly, tokenTypeAt, type TokenTarget } from '../lsp/target.js'; import { isCollapsed, itemDeps, @@ -160,9 +160,11 @@ function targetOf( line = alt; } } + // The highlighter already classified this span while rendering it. + const tokenType = tokenTypeAt(props.tokenElement); return word - ? { path, side, line, col: props.lineCharStart + word.start, text: word.text } - : { path, side, line, col: props.lineCharStart, text: props.tokenText }; + ? { path, side, line, col: props.lineCharStart + word.start, text: word.text, tokenType } + : { path, side, line, col: props.lineCharStart, text: props.tokenText, tokenType }; } /** Whether some running language server claims this file's language. */ diff --git a/src/client/store.ts b/src/client/store.ts index 75f79e9..940ca85 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -1400,6 +1400,11 @@ export const useStore = create((set, get) => { // With language servers off every action would only flash why: no menu to offer. if (!get().lsp.enabled) return; get().closeHover(); + // The highlighter classified this span as it rendered it: no parse, no await. + if (target.tokenType) { + get().closeSymbolMenu(); + return; + } const t = ++menuSeq; const generationAtClick = generation; const blocked = await blocksSymbol(target.path, target.side, target.line, target.col, () => @@ -1432,7 +1437,7 @@ export const useStore = create((set, get) => { hover: null, async requestHover(target, anchor) { const t = ++hoverSeq; - if (get().symbolMenu || targetBlocker(target)) return; + if (get().symbolMenu || targetBlocker(target) || target.tokenType) return; const g = generation; try { const blocked = await blocksSymbol(target.path, target.side, target.line, target.col, () => diff --git a/test/client/ReviewPane.test.ts b/test/client/ReviewPane.test.ts index 25b2fcb..b28c65c 100644 --- a/test/client/ReviewPane.test.ts +++ b/test/client/ReviewPane.test.ts @@ -168,7 +168,7 @@ describe('ReviewPane scroller effects', () => { { item: { id: 'diff:config.json@0' } }, ); expect(enter).toHaveBeenCalledWith( - { path: 'config.json', side: 'new', line: 3, col: 7, text: '"runs-on"' }, + { path: 'config.json', side: 'new', line: 3, col: 7, text: '"runs-on"', tokenType: null }, tokenElement, ); expect(tokenElement.classList.contains('lsp-hover')).toBe(false); diff --git a/test/client/tokenType.test.ts b/test/client/tokenType.test.ts new file mode 100644 index 0000000..3c7925a --- /dev/null +++ b/test/client/tokenType.test.ts @@ -0,0 +1,66 @@ +import { DEFAULT_TOKENIZE_MAX_LENGTH, getSharedHighlighter, renderFileWithHighlighter } from '@pierre/diffs'; +import { toHtml } from 'hast-util-to-html'; +import { describe, expect, it } from 'vitest'; +import { tokenTypeAt } from '../../src/client/lsp/target.js'; +import { SHIKI_THEMES } from '../../src/client/theme.js'; + +/** + * Guards the three vendored patches under `patches/`, which together carry Shiki's + * TextMate token class through a dual-theme render and onto the rendered span. Drop any + * one of them and the attribute disappears, so these assertions fail loudly. + */ +/** + * Renders the way the viewer does: two themes at once. A single-theme render takes a + * different path through Shiki and keeps `type` even unpatched, so it would pass here + * while the app stayed blank. + */ +async function render(code: string, lang: string): Promise { + const highlighter = await getSharedHighlighter({ themes: [SHIKI_THEMES.light, SHIKI_THEMES.dark], langs: [lang] }); + const { code: lines } = renderFileWithHighlighter({ name: `file.${lang}`, contents: code, lang }, highlighter, { + theme: SHIKI_THEMES, + useTokenTransformer: true, + tokenizeMaxLineLength: DEFAULT_TOKENIZE_MAX_LENGTH, + }); + return lines.map((line) => toHtml(line)).join('\n'); +} + +describe('rendered token classification', () => { + it('marks comments and string literals on the spans it paints', async () => { + const html = await render('// note\nconst greeting = "hello";', 'typescript'); + expect(html).toContain('data-token-type="comment"'); + expect(html).toContain('data-token-type="string"'); + // Code keeps no attribute, so the symbol menu stays available on it. + expect(html).not.toMatch(/data-token-type="[^"]*">const { + const html = await render(code, lang); + expect(html).toContain('data-token-type="comment"'); + expect(html).toContain('data-token-type="string"'); + }); +}); + +describe('tokenTypeAt', () => { + const span = (attrs: string): Element => { + const el = { + closest: (sel: string) => (sel === '[data-token-type]' && attrs ? { dataset: { tokenType: attrs } } : null), + }; + return el as unknown as Element; + }; + + it('reads a classification the highlighter made', () => { + expect(tokenTypeAt(span('comment'))).toBe('comment'); + expect(tokenTypeAt(span('string'))).toBe('string'); + }); + + it('ignores an absent, empty or unknown classification', () => { + expect(tokenTypeAt(null)).toBeNull(); + expect(tokenTypeAt(span(''))).toBeNull(); + expect(tokenTypeAt(span('keyword'))).toBeNull(); + }); +}); From 80600c4cac8f23046ea03f50a3b0448c26784846 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 08:17:55 +0000 Subject: [PATCH 2/4] fix(nix): refresh npmDepsHash for the patch-package dependency Adding patch-package changed package-lock.json, so the pinned hash no longer matched and `nix build` failed. Value taken from the mismatch the nix job reported, as the comment above it prescribes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01P8ro9sQLfwxHnmbyDDBmJ3 --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index d06a11c..29a83c8 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,7 @@ # docker run --rm -v "$PWD:/src:ro" -w /src nixos/nix nix build 'path:/src#default' --no-link --extra-experimental-features 'nix-command flakes' 2>&1 # or in one: # hash=$(docker run --rm -v "$PWD:/src:ro" -w /src nixos/nix nix build 'path:/src#default' --no-link --extra-experimental-features 'nix-command flakes' 2>&1 | sed -n 's/.*got: *//p' | tail -1) && test -n "$hash" && sed -i "s|npmDepsHash = \".*\";|npmDepsHash = \"$hash\";|" flake.nix - npmDepsHash = "sha256-YiLHOz0zELiDCcglFSYuN1SaEFKFf8T2AJBUVQT81BU="; + npmDepsHash = "sha256-OeLjWnAIZ2beHdsLVfHtNqzLQSykIHxTbNT2hhFXlEQ="; nativeBuildInputs = [ pkgs.makeWrapper ]; postInstall = '' From 2206b4f0ed953a6e5c9bcc6868a5a58ec6cd8ede Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 08:21:13 +0000 Subject: [PATCH 3/4] fix(ci): keep the lockfile check free of install scripts Adding patch-package gave the package a `prepare` script. The lockfile job installs nothing, so npm ran that script against a patch-package binary that was not there and exited 127 before it ever compared lockfiles. The job already documents that it needs no install; say the same to npm. Regenerate package-lock.json on Node 24 as `engines` requires. The lockfile committed with the patch-package dependency came from Node 22, whose npm keeps an optional peer entry this npm drops -- exactly the drift the job's own error message warns about. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01P8ro9sQLfwxHnmbyDDBmJ3 --- .github/workflows/ci.yml | 3 ++- package-lock.json | 11 ----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff48afc..fa52551 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,9 +101,10 @@ jobs: # The `npm ci` in the other jobs only checks the dependency tree, so drift in # the lockfile's own metadata (name, version, bin, engines) passes it silently. # `--package-lock-only` needs no install, so this job does not run `npm ci`. + # It installs nothing, so `prepare` would call a patch-package that is not there. - name: Check package-lock.json is up to date run: | - npm install --package-lock-only --no-audit --no-fund + npm install --package-lock-only --no-audit --no-fund --ignore-scripts git diff --exit-code -- package-lock.json || { echo "::error file=package-lock.json::package-lock.json is out of date; run \`npm install\` and commit the result. Use Node 24 as \`engines\` requires: older npm keeps optional peer entries this npm drops, so the lockfile it writes fails here." exit 1 diff --git a/package-lock.json b/package-lock.json index 704982d..69aed88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1817,17 +1817,6 @@ "react-dom": "^18.3.1 || ^19.0.0" } }, - "node_modules/@pierre/trees/node_modules/@pierre/theme": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pierre/theme/-/theme-1.1.0.tgz", - "integrity": "sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "vscode": "^1.0.0" - } - }, "node_modules/@pierre/trees/node_modules/@pierre/theming": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pierre/theming/-/theming-1.0.0.tgz", From 521706d71f24e8b12b6597dec6cf47cab3d6ef3a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 08:23:06 +0000 Subject: [PATCH 4/4] fix(nix): refresh npmDepsHash for the Node 24 lockfile Regenerating package-lock.json on Node 24 dropped an optional peer entry, which moved the hash again. Value taken from the mismatch the nix job reported. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01P8ro9sQLfwxHnmbyDDBmJ3 --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 29a83c8..b0e0585 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,7 @@ # docker run --rm -v "$PWD:/src:ro" -w /src nixos/nix nix build 'path:/src#default' --no-link --extra-experimental-features 'nix-command flakes' 2>&1 # or in one: # hash=$(docker run --rm -v "$PWD:/src:ro" -w /src nixos/nix nix build 'path:/src#default' --no-link --extra-experimental-features 'nix-command flakes' 2>&1 | sed -n 's/.*got: *//p' | tail -1) && test -n "$hash" && sed -i "s|npmDepsHash = \".*\";|npmDepsHash = \"$hash\";|" flake.nix - npmDepsHash = "sha256-OeLjWnAIZ2beHdsLVfHtNqzLQSykIHxTbNT2hhFXlEQ="; + npmDepsHash = "sha256-HfkDRjgaMo+tOT5JEwS8fDuEAEH+bzb+/fafQyWrtSc="; nativeBuildInputs = [ pkgs.makeWrapper ]; postInstall = ''