From 550a2bb8f1a4261630caf320b6c33c869b648438 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Wed, 18 Mar 2026 12:32:54 +0200 Subject: [PATCH 01/25] chore: update dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates: - css-tree: 3.1.0 → 3.2.1 (bug fixes, new parse options) - node-html-parser: 7.0.1 → 7.1.0 (new parsing options) - @types/node: 24.10.1 → 24.12.0 (type updates) - vscode-languageserver-textdocument: 1.0.11 → 1.0.12 - vscode-uri: 3.0.8 → 3.1.0 --- AGENTS.md | 14 +- MIGRATION.md | 91 ++++++++++ package.json | 12 +- pnpm-lock.yaml | 467 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 577 insertions(+), 7 deletions(-) create mode 100644 MIGRATION.md create mode 100644 pnpm-lock.yaml diff --git a/AGENTS.md b/AGENTS.md index bc13715..321059e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,17 @@ **IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods. +## Portfolio Tracking in Linear (Required) + +For cross-project release management, also mirror status in Linear project `css-lsp`. + +- Every work session must be mapped to a Linear issue. +- When work starts, move the Linear issue to `Started`. +- When work ends, update the issue with outcome and next step, then set `Backlog` or `Done`. +- Pull requests must reference a Linear issue ID. +- Do not run release actions without confirming related Linear issues are current. +- bd remains the source of truth for repo-local dependency tracking; Linear is required as a portfolio mirror. + ### Why bd? - Dependency-aware: Track blockers and relationships between issues @@ -131,8 +142,9 @@ history/ - ✅ Link discovered work with `discovered-from` dependencies - ✅ Check `bd ready` before asking "what should I work on?" - ✅ Store AI planning docs in `history/` directory +- ✅ Mirror delivery/release status in Linear project `css-lsp` - ❌ Do NOT create markdown TODO lists -- ❌ Do NOT use external issue trackers +- ❌ Do NOT replace bd with external trackers for repo-local dependency planning - ❌ Do NOT duplicate tracking systems - ❌ Do NOT clutter repo root with planning documents diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..9965380 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,91 @@ +# Dependency Migration Guide + +## Overview + +This guide covers updating dependencies to their latest compatible versions. + +## Current Outdated Dependencies + +| Package | Current | Wanted | Latest | Type | +|---------|---------|--------|--------|------| +| @types/node | 24.10.1 | 24.12.0 | 25.5.0 | patch | +| css-tree | 3.1.0 | 3.2.1 | 3.2.1 | minor | +| glob | 12.0.0 | 12.0.0 | 13.0.6 | major | +| node-html-parser | 7.0.1 | 7.1.0 | 7.1.0 | minor | + +## Safe Updates (Backward Compatible) + +### @types/node 24.10.1 → 24.12.0 + +**Type:** Patch update + +**Changes:** Minor type definitions updates. + +**Action:** Update with `pnpm update @types/node` + +### css-tree 3.1.0 → 3.2.1 + +**Type:** Minor update + +**New Features (3.2.0):** +- Added `list` option to `parse()` method to control child node format (List vs array) +- Added `onToken` option for advanced token handling +- Added math functions support (`min()`, `max()`, etc.) +- Added `sideEffects: false` for better tree-shaking + +**Bug Fixes (3.2.1):** +- Fixed parsing of nested function in definition syntax + +**Impact:** All changes are additive. Existing code continues to work. + +**Action:** Update with `pnpm update css-tree` + +### node-html-parser 7.0.1 → 7.1.0 + +**Type:** Minor update + +**New Features:** +- Added `closeAllOnClosing` option +- Added `preserveTagNesting` option + +**Impact:** All changes are additive. Existing code continues to work. + +**Action:** Update with `pnpm update node-html-parser` + +## Optional Major Update + +### glob 12.0.0 → 13.0.6 + +**Type:** Major update (breaking) + +**Breaking Changes:** +- CLI moved to separate package `glob-bin` +- `--shell` option removed + +**Impact:** None if used programmatically (API unchanged). CLI users must install `glob-bin` separately. + +**Action:** Update only if needed. Current semver range `^12.0.0` will NOT auto-upgrade to v13. + +## Update Commands + +```bash +# Install pnpm lockfile if missing +pnpm install + +# Update all dependencies to wanted versions +pnpm update + +# Or update specific packages +pnpm update @types/node css-tree node-html-parser + +# For glob v13 (optional, breaking) +pnpm update glob@13 +``` + +## Verification + +After updating, run tests to verify: + +```bash +pnpm test +``` diff --git a/package.json b/package.json index 7194f14..42cf3b5 100644 --- a/package.json +++ b/package.json @@ -19,17 +19,17 @@ }, "devDependencies": { "@types/css-tree": "^2.3.11", - "@types/node": "^24.10.1", - "typescript": "^5.9.3", - "ts-node": "^10.9.2" + "@types/node": "^24.12.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" }, "dependencies": { - "css-tree": "^3.1.0", + "css-tree": "^3.2.1", "glob": "^12.0.0", + "node-html-parser": "^7.1.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", - "vscode-uri": "^3.0.8", - "node-html-parser": "^7.0.1" + "vscode-uri": "^3.0.8" }, "engines": { "node": "*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..69871d3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,467 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + css-tree: + specifier: ^3.2.1 + version: 3.2.1 + glob: + specifier: ^12.0.0 + version: 12.0.0 + node-html-parser: + specifier: ^7.1.0 + version: 7.1.0 + vscode-languageserver: + specifier: ^9.0.1 + version: 9.0.1 + vscode-languageserver-textdocument: + specifier: ^1.0.11 + version: 1.0.12 + vscode-uri: + specifier: ^3.0.8 + version: 3.1.0 + devDependencies: + '@types/css-tree': + specifier: ^2.3.11 + version: 2.3.11 + '@types/node': + specifier: ^24.12.0 + version: 24.12.0 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@24.12.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/css-tree@2.3.11': + resolution: {integrity: sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + glob@12.0.0: + resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==} + engines: {node: 20 || >=22} + hasBin: true + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + node-html-parser@7.1.0: + resolution: {integrity: sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + +snapshots: + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@isaacs/cliui@9.0.0': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/css-tree@2.3.11': {} + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + arg@4.1.3: {} + + balanced-match@4.0.4: {} + + boolbase@1.0.0: {} + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + diff@4.0.4: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + entities@4.5.0: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + glob@12.0.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.4 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + he@1.2.0: {} + + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + lru-cache@11.2.7: {} + + make-error@1.3.6: {} + + mdn-data@2.27.1: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minipass@7.1.3: {} + + node-html-parser@7.1.0: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + ts-node@10.9.2(@types/node@24.12.0)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.12.0 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + v8-compile-cache-lib@3.0.1: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.1.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + yn@3.1.1: {} From 29d2a011ab296ed8b626c7ac3fff7e5040efed45 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 14:54:58 +0200 Subject: [PATCH 02/25] refactor: extract flag parsing into declarative flags.ts module - Move all flag definitions to a single declarative registry in src/flags.ts - Supports flag types: bool, optIn, enum, pathDisplay, list, int - Simplifies adding new flags to 3 steps (add to interface, add to FLAGS, add to return) - Preserve all existing CLI and env var behavior - Add --no-color-replacement-diagnostics flag for Firefox extension --- src/flags.ts | 274 ++++++++++++++++++++++++++++++++++++ src/runtimeConfig.ts | 239 +------------------------------ src/server.ts | 8 +- tests/runtimeConfig.test.ts | 11 ++ 4 files changed, 292 insertions(+), 240 deletions(-) create mode 100644 src/flags.ts diff --git a/src/flags.ts b/src/flags.ts new file mode 100644 index 0000000..9eb6e7f --- /dev/null +++ b/src/flags.ts @@ -0,0 +1,274 @@ +export type PathDisplayMode = "relative" | "absolute" | "abbreviated"; +export type UndefinedVarFallbackMode = "warning" | "info" | "off"; + +export interface RuntimeConfig { + enableColorProvider: boolean; + colorOnlyOnVariables: boolean; + enableColorReplacementDiagnostics: boolean; + lookupFiles: string[] | undefined; + ignoreGlobs: string[] | undefined; + pathDisplayMode: PathDisplayMode; + pathDisplayAbbrevLength: number; + undefinedVarFallback: UndefinedVarFallbackMode; +} + +// ============================================================================ +// Flag Definitions (Declarative) +// ============================================================================ +// To add a new flag: +// 1. Add it to RuntimeConfig interface above +// 2. Add a flag definition here +// 3. Add parsing logic in buildRuntimeConfig() +// 4. Add tests + +interface BoolFlagDef { + kind: "bool"; + positive: string; + negative: string; + default: boolean; + envKey?: string; +} + +interface OptInFlagDef { + kind: "optIn"; + flag: string; + envKey: string; + default: boolean; +} + +interface EnumFlagDef { + kind: "enum"; + flag: string; + envKey: string; + values: readonly T[]; + default: T; + aliases?: Record; +} + +interface PathDisplayFlagDef { + kind: "pathDisplay"; + flag: string; + envKey: string; + defaultMode: PathDisplayMode; + defaultLength: number; +} + +interface IntFlagDef { + kind: "int"; + flag: string; + envKey: string; + default: number; +} + +interface ListFlagDef { + kind: "list"; + primaryFlag: string; + secondaryFlag: string; + envKey: string; +} + +type FlagDef = BoolFlagDef | OptInFlagDef | EnumFlagDef | PathDisplayFlagDef | IntFlagDef | ListFlagDef; + +// ============================================================================ +// Flag Registry +// ============================================================================ + +const FLAGS = { + enableColorProvider: { + kind: "bool" as const, + positive: "--color-preview", + negative: "--no-color-preview", + default: true, + }, + colorOnlyOnVariables: { + kind: "optIn" as const, + flag: "--color-only-variables", + envKey: "CSS_LSP_COLOR_ONLY_VARIABLES", + default: false, + }, + enableColorReplacementDiagnostics: { + kind: "bool" as const, + positive: "--color-replacement-diagnostics", + negative: "--no-color-replacement-diagnostics", + default: true, + }, + lookupFiles: { + kind: "list" as const, + primaryFlag: "--lookup-files", + secondaryFlag: "--lookup-file", + envKey: "CSS_LSP_LOOKUP_FILES", + }, + ignoreGlobs: { + kind: "list" as const, + primaryFlag: "--ignore-globs", + secondaryFlag: "--ignore-glob", + envKey: "CSS_LSP_IGNORE_GLOBS", + }, + pathDisplay: { + kind: "pathDisplay" as const, + flag: "--path-display", + envKey: "CSS_LSP_PATH_DISPLAY", + defaultMode: "relative" as PathDisplayMode, + defaultLength: 1, + }, + pathDisplayLength: { + kind: "int" as const, + flag: "--path-display-length", + envKey: "CSS_LSP_PATH_DISPLAY_LENGTH", + default: 1, + }, + undefinedVarFallback: { + kind: "enum" as const, + flag: "--undefined-var-fallback", + envKey: "CSS_LSP_UNDEFINED_VAR_FALLBACK", + values: ["warning", "info", "off"] as const, + default: "warning" as UndefinedVarFallbackMode, + aliases: { + warn: "warning", + information: "info", + omit: "off", + none: "off", + disable: "off", + disabled: "off", + }, + }, +} as const satisfies Record; + +// ============================================================================ +// Parsing Utilities +// ============================================================================ + +function getArgValue(argv: string[], name: string): string | null { + const flag = `--${name}`; + const directIndex = argv.indexOf(flag); + if (directIndex !== -1) { + const candidate = argv[directIndex + 1]; + if (candidate && !candidate.startsWith("-")) return candidate; + return null; + } + const prefix = `${flag}=`; + const withEquals = argv.find((arg) => arg.startsWith(prefix)); + if (withEquals) return withEquals.slice(prefix.length); + return null; +} + +function parseOptionalInt(value: string | null | undefined): number | null { + if (!value) return null; + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? null : parsed; +} + +function splitList(value: string): string[] { + return value.split(",").map((e) => e.trim()).filter(Boolean); +} + +function parseBool(flag: BoolFlagDef, argv: string[], env: NodeJS.ProcessEnv): boolean { + if (argv.includes(flag.negative)) return false; + if (argv.includes(flag.positive)) return true; + if (flag.envKey && env[flag.envKey] === "1") return true; + return flag.default; +} + +function parseOptIn(flag: OptInFlagDef, argv: string[], env: NodeJS.ProcessEnv): boolean { + if (argv.includes(flag.flag)) return true; + if (env[flag.envKey] === "1") return true; + return flag.default; +} + +function parseEnum(flag: EnumFlagDef, argv: string[], env: NodeJS.ProcessEnv): T { + const argValue = getArgValue(argv, flag.flag.replace("--", "")); + const envValue = env[flag.envKey]; + const raw = argValue ?? envValue ?? null; + if (!raw) return flag.default; + + const normalized = raw.toLowerCase(); + if (flag.aliases) { + const aliased = flag.aliases[normalized]; + if (aliased && flag.values.includes(aliased)) return aliased; + } + const match = flag.values.find((v) => v.toLowerCase() === normalized); + return match ?? flag.default; +} + +function parsePathDisplay(flag: PathDisplayFlagDef, argv: string[], env: NodeJS.ProcessEnv): { mode: PathDisplayMode; combinedLength: number | null } { + const argValue = getArgValue(argv, flag.flag.replace("--", "")); + const envValue = env[flag.envKey]; + const raw = argValue ?? envValue ?? null; + + let mode = flag.defaultMode; + let combinedLength: number | null = null; + + if (raw) { + const [modePart, lengthPart] = raw.split(":", 2); + const normalizedMode = modePart?.toLowerCase(); + if (normalizedMode === "relative") mode = "relative"; + else if (normalizedMode === "absolute") mode = "absolute"; + else if (normalizedMode === "abbreviated" || normalizedMode === "abbr" || normalizedMode === "fish") mode = "abbreviated"; + if (lengthPart) { + const parsed = parseOptionalInt(lengthPart); + if (parsed !== null) combinedLength = parsed; + } + } + + return { mode, combinedLength }; +} + +function parseInt(flag: IntFlagDef, argv: string[], env: NodeJS.ProcessEnv): number { + const argValue = getArgValue(argv, flag.flag.replace("--", "")); + const envValue = env[flag.envKey]; + const raw = argValue ?? envValue ?? null; + const parsed = parseOptionalInt(raw); + return parsed ?? flag.default; +} + +function parseList(flag: ListFlagDef, argv: string[], env: NodeJS.ProcessEnv): string[] | undefined { + const cliValues: string[] = []; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if ((arg === flag.primaryFlag || arg === flag.secondaryFlag) && argv[i + 1] && !argv[i + 1].startsWith("-")) { + cliValues.push(...splitList(argv[i + 1])); + i++; + continue; + } + if (arg.startsWith(`${flag.primaryFlag}=`)) { + cliValues.push(...splitList(arg.slice(flag.primaryFlag.length + 1))); + continue; + } + if (arg.startsWith(`${flag.secondaryFlag}=`)) { + cliValues.push(...splitList(arg.slice(flag.secondaryFlag.length + 1))); + } + } + if (cliValues.length > 0) return cliValues; + const envValue = env[flag.envKey]; + if (envValue) { + const values = splitList(envValue); + if (values.length > 0) return values; + } + return undefined; +} + +// ============================================================================ +// Config Builder +// ============================================================================ + +export function buildRuntimeConfig(argv: string[], env: NodeJS.ProcessEnv): RuntimeConfig { + const pathDisplay = parsePathDisplay(FLAGS.pathDisplay, argv, env); + const pathDisplayLengthArg = getArgValue(argv, "path-display-length"); + const pathDisplayLengthEnv = env.CSS_LSP_PATH_DISPLAY_LENGTH; + const abbrevLengthRaw = parseOptionalInt(pathDisplayLengthArg ?? pathDisplayLengthEnv) ?? pathDisplay.combinedLength; + const pathDisplayAbbrevLength = Math.max(0, abbrevLengthRaw ?? 1); + + return { + enableColorProvider: parseBool(FLAGS.enableColorProvider, argv, env), + colorOnlyOnVariables: parseOptIn(FLAGS.colorOnlyOnVariables, argv, env), + enableColorReplacementDiagnostics: parseBool(FLAGS.enableColorReplacementDiagnostics, argv, env), + lookupFiles: parseList(FLAGS.lookupFiles, argv, env), + ignoreGlobs: parseList(FLAGS.ignoreGlobs, argv, env), + pathDisplayMode: pathDisplay.mode, + pathDisplayAbbrevLength, + undefinedVarFallback: parseEnum(FLAGS.undefinedVarFallback, argv, env), + }; +} + +// Export for testing +export { FLAGS }; diff --git a/src/runtimeConfig.ts b/src/runtimeConfig.ts index ca7dfc9..cb99d18 100644 --- a/src/runtimeConfig.ts +++ b/src/runtimeConfig.ts @@ -4,6 +4,7 @@ export type UndefinedVarFallbackMode = "warning" | "info" | "off"; export interface RuntimeConfig { enableColorProvider: boolean; colorOnlyOnVariables: boolean; + enableColorReplacementDiagnostics: boolean; lookupFiles: string[] | undefined; ignoreGlobs: string[] | undefined; pathDisplayMode: PathDisplayMode; @@ -11,240 +12,4 @@ export interface RuntimeConfig { undefinedVarFallback: UndefinedVarFallbackMode; } -function getArgValue(argv: string[], name: string): string | null { - const flag = `--${name}`; - const directIndex = argv.indexOf(flag); - if (directIndex !== -1) { - const candidate = argv[directIndex + 1]; - if (candidate && !candidate.startsWith("-")) { - return candidate; - } - return null; - } - - const prefix = `${flag}=`; - const withEquals = argv.find((arg) => arg.startsWith(prefix)); - if (withEquals) { - return withEquals.slice(prefix.length); - } - - return null; -} - -function parseOptionalInt(value: string | null | undefined): number | null { - if (!value) { - return null; - } - const parsed = Number.parseInt(value, 10); - if (Number.isNaN(parsed)) { - return null; - } - return parsed; -} - -function normalizePathDisplayMode( - value: string | null | undefined, -): PathDisplayMode | null { - if (!value) { - return null; - } - - switch (value.toLowerCase()) { - case "relative": - return "relative"; - case "absolute": - return "absolute"; - case "abbreviated": - case "abbr": - case "fish": - return "abbreviated"; - default: - return null; - } -} - -function normalizeUndefinedVarFallbackMode( - value: string | null | undefined, -): UndefinedVarFallbackMode | null { - if (!value) { - return null; - } - - switch (value.toLowerCase()) { - case "warn": - case "warning": - return "warning"; - case "info": - case "information": - return "info"; - case "off": - case "omit": - case "none": - case "disable": - case "disabled": - return "off"; - default: - return null; - } -} - -function parsePathDisplay(value: string | null | undefined): { - mode: PathDisplayMode | null; - abbrevLength: number | null; -} { - if (!value) { - return { mode: null, abbrevLength: null }; - } - - const [modePart, lengthPart] = value.split(":", 2); - const mode = normalizePathDisplayMode(modePart); - const abbrevLength = parseOptionalInt(lengthPart); - - return { mode, abbrevLength }; -} - -function splitLookupList(value: string): string[] { - return value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); -} - -function resolveLookupFiles( - argv: string[], - env: NodeJS.ProcessEnv, -): string[] | undefined { - const cliFiles: string[] = []; - - for (let i = 0; i < argv.length; i++) { - const arg = argv[i]; - if ( - arg === "--lookup-files" && - argv[i + 1] && - !argv[i + 1].startsWith("-") - ) { - cliFiles.push(...splitLookupList(argv[i + 1])); - i++; - continue; - } - if (arg.startsWith("--lookup-files=")) { - cliFiles.push(...splitLookupList(arg.slice("--lookup-files=".length))); - continue; - } - if ( - arg === "--lookup-file" && - argv[i + 1] && - !argv[i + 1].startsWith("-") - ) { - cliFiles.push(argv[i + 1]); - i++; - continue; - } - if (arg.startsWith("--lookup-file=")) { - cliFiles.push(arg.slice("--lookup-file=".length)); - } - } - - if (cliFiles.length > 0) { - return cliFiles; - } - - const envValue = env.CSS_LSP_LOOKUP_FILES; - if (envValue) { - const envFiles = splitLookupList(envValue); - if (envFiles.length > 0) { - return envFiles; - } - } - - return undefined; -} - -function resolveIgnoreGlobs( - argv: string[], - env: NodeJS.ProcessEnv, -): string[] | undefined { - const cliGlobs: string[] = []; - - for (let i = 0; i < argv.length; i++) { - const arg = argv[i]; - if ( - arg === "--ignore-globs" && - argv[i + 1] && - !argv[i + 1].startsWith("-") - ) { - cliGlobs.push(...splitLookupList(argv[i + 1])); - i++; - continue; - } - if (arg.startsWith("--ignore-globs=")) { - cliGlobs.push(...splitLookupList(arg.slice("--ignore-globs=".length))); - continue; - } - if ( - arg === "--ignore-glob" && - argv[i + 1] && - !argv[i + 1].startsWith("-") - ) { - cliGlobs.push(argv[i + 1]); - i++; - continue; - } - if (arg.startsWith("--ignore-glob=")) { - cliGlobs.push(arg.slice("--ignore-glob=".length)); - } - } - - if (cliGlobs.length > 0) { - return cliGlobs; - } - - const envValue = env.CSS_LSP_IGNORE_GLOBS; - if (envValue) { - const envGlobs = splitLookupList(envValue); - if (envGlobs.length > 0) { - return envGlobs; - } - } - - return undefined; -} - -export function buildRuntimeConfig( - argv: string[], - env: NodeJS.ProcessEnv, -): RuntimeConfig { - const enableColorProvider = !argv.includes("--no-color-preview"); - const colorOnlyOnVariables = - argv.includes("--color-only-variables") || - env.CSS_LSP_COLOR_ONLY_VARIABLES === "1"; - const lookupFiles = resolveLookupFiles(argv, env); - const ignoreGlobs = resolveIgnoreGlobs(argv, env); - const pathDisplayArg = getArgValue(argv, "path-display"); - const pathDisplayEnv = env.CSS_LSP_PATH_DISPLAY; - const parsedPathDisplay = parsePathDisplay(pathDisplayArg ?? pathDisplayEnv); - const pathDisplayMode: PathDisplayMode = - parsedPathDisplay.mode ?? "relative"; - const pathDisplayLengthArg = getArgValue(argv, "path-display-length"); - const pathDisplayLengthEnv = env.CSS_LSP_PATH_DISPLAY_LENGTH; - const abbrevLengthRaw = - parseOptionalInt(pathDisplayLengthArg ?? pathDisplayLengthEnv) ?? - parsedPathDisplay.abbrevLength; - const pathDisplayAbbrevLength = Math.max(0, abbrevLengthRaw ?? 1); - const undefinedVarFallbackArg = getArgValue(argv, "undefined-var-fallback"); - const undefinedVarFallbackEnv = env.CSS_LSP_UNDEFINED_VAR_FALLBACK; - const undefinedVarFallback = - normalizeUndefinedVarFallbackMode( - undefinedVarFallbackArg ?? undefinedVarFallbackEnv, - ) ?? "warning"; - - return { - enableColorProvider, - colorOnlyOnVariables, - lookupFiles, - ignoreGlobs, - pathDisplayMode, - pathDisplayAbbrevLength, - undefinedVarFallback, - }; -} +export { buildRuntimeConfig } from "./flags"; diff --git a/src/server.ts b/src/server.ts index c4f04ef..faa9578 100644 --- a/src/server.ts +++ b/src/server.ts @@ -265,9 +265,11 @@ async function validateTextDocument(textDocument: TextDocument): Promise { } } - diagnostics.push( - ...collectColorReplacementDiagnostics(textDocument, cssVariableManager), - ); + if (runtimeConfig.enableColorReplacementDiagnostics) { + diagnostics.push( + ...collectColorReplacementDiagnostics(textDocument, cssVariableManager), + ); + } // Send diagnostics to the client connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); diff --git a/tests/runtimeConfig.test.ts b/tests/runtimeConfig.test.ts index ceb8820..442d314 100644 --- a/tests/runtimeConfig.test.ts +++ b/tests/runtimeConfig.test.ts @@ -19,6 +19,7 @@ test("runtime config defaults", () => { assert.equal(config.enableColorProvider, true); assert.equal(config.colorOnlyOnVariables, false); + assert.equal(config.enableColorReplacementDiagnostics, true); assert.equal(config.lookupFiles, undefined); assert.equal(config.ignoreGlobs, undefined); assert.equal(config.pathDisplayMode, "relative"); @@ -229,3 +230,13 @@ test("undefined var fallback mode ignores invalid values", () => { ); assert.equal(config.undefinedVarFallback, "warning"); }); + +test("color replacement diagnostics enabled by default", () => { + const config = buildRuntimeConfig([], makeEnv()); + assert.equal(config.enableColorReplacementDiagnostics, true); +}); + +test("color replacement diagnostics disabled with --no-color-replacement-diagnostics", () => { + const config = buildRuntimeConfig(["--no-color-replacement-diagnostics"], makeEnv()); + assert.equal(config.enableColorReplacementDiagnostics, false); +}); From 8d389d58540765f426d1bd0d4fd5ef7098bc7465 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 14:56:01 +0200 Subject: [PATCH 03/25] docs: document --no-color-replacement-diagnostics flag --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ad16e90..292846b 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ This is a standalone LSP server. Configure it in any LSP client. Command-line flags: - `--no-color-preview` +- `--no-color-replacement-diagnostics` (disable "replace literal with CSS variable" suggestions) - `--color-only-variables` (show colors only on `var(--...)` usages) - `--lookup-files ","` (comma-separated list of glob patterns) - `--lookup-file ""` (repeatable) From a746c3aea46b079ab023d64190058a2068a2bc99 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 14:58:46 +0200 Subject: [PATCH 04/25] add: env var support for color replacement diagnostics - Add CSS_LSP_COLOR_REPLACEMENT_DIAGNOSTICS=0 env var - Update parseBool to handle "0" for disabling via env - Document env var in README --- README.md | 4 ++++ src/flags.ts | 6 +++++- tests/runtimeConfig.test.ts | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 292846b..0bc4f93 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ Command-line flags: Environment variables: +- `CSS_LSP_COLOR_REPLACEMENT_DIAGNOSTICS=0` (disable "replace literal with CSS variable" suggestions) + +Environment variables: + - `CSS_LSP_COLOR_ONLY_VARIABLES=1` (same as `--color-only-variables`) - `CSS_LSP_LOOKUP_FILES` (comma-separated glob patterns; ignored if CLI lookup flags are provided) - `CSS_LSP_IGNORE_GLOBS` (comma-separated glob patterns; ignored if CLI ignore flags are provided) diff --git a/src/flags.ts b/src/flags.ts index 9eb6e7f..bb2df9f 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -91,6 +91,7 @@ const FLAGS = { positive: "--color-replacement-diagnostics", negative: "--no-color-replacement-diagnostics", default: true, + envKey: "CSS_LSP_COLOR_REPLACEMENT_DIAGNOSTICS", }, lookupFiles: { kind: "list" as const, @@ -165,7 +166,10 @@ function splitList(value: string): string[] { function parseBool(flag: BoolFlagDef, argv: string[], env: NodeJS.ProcessEnv): boolean { if (argv.includes(flag.negative)) return false; if (argv.includes(flag.positive)) return true; - if (flag.envKey && env[flag.envKey] === "1") return true; + if (flag.envKey) { + if (env[flag.envKey] === "0") return false; + if (env[flag.envKey] === "1") return true; + } return flag.default; } diff --git a/tests/runtimeConfig.test.ts b/tests/runtimeConfig.test.ts index 442d314..c6ae973 100644 --- a/tests/runtimeConfig.test.ts +++ b/tests/runtimeConfig.test.ts @@ -240,3 +240,8 @@ test("color replacement diagnostics disabled with --no-color-replacement-diagnos const config = buildRuntimeConfig(["--no-color-replacement-diagnostics"], makeEnv()); assert.equal(config.enableColorReplacementDiagnostics, false); }); + +test("color replacement diagnostics disabled with env var", () => { + const config = buildRuntimeConfig([], makeEnv({ CSS_LSP_COLOR_REPLACEMENT_DIAGNOSTICS: "0" })); + assert.equal(config.enableColorReplacementDiagnostics, false); +}); From 747db8fa73f5936608dacf934b018bfdb184f114 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 14:59:46 +0200 Subject: [PATCH 05/25] bump version to 1.0.19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42cf3b5..e649508 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "css-variable-lsp", - "version": "1.0.18", + "version": "1.0.19", "description": "A CSS Language Server for CSS Variables", "license": "GPL-3.0", "repository": { From 4ec90aef5b8eeed2b3e4a05c653c67fee20ae729 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 15:01:02 +0200 Subject: [PATCH 06/25] clear AGENTS.md --- AGENTS.md | 151 ------------------------------------------------------ 1 file changed, 151 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 321059e..e69de29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,151 +0,0 @@ -## Issue Tracking with bd (beads) - -**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods. - -## Portfolio Tracking in Linear (Required) - -For cross-project release management, also mirror status in Linear project `css-lsp`. - -- Every work session must be mapped to a Linear issue. -- When work starts, move the Linear issue to `Started`. -- When work ends, update the issue with outcome and next step, then set `Backlog` or `Done`. -- Pull requests must reference a Linear issue ID. -- Do not run release actions without confirming related Linear issues are current. -- bd remains the source of truth for repo-local dependency tracking; Linear is required as a portfolio mirror. - -### Why bd? - -- Dependency-aware: Track blockers and relationships between issues -- Git-friendly: Auto-syncs to JSONL for version control -- Agent-optimized: JSON output, ready work detection, discovered-from links -- Prevents duplicate tracking systems and confusion - -### Quick Start - -**Check for ready work:** - -```bash -bd ready --json -``` - -**Create new issues:** - -```bash -bd create "Issue title" -t bug|feature|task -p 0-4 --json -bd create "Issue title" -p 1 --deps discovered-from:bd-123 --json -``` - -**Claim and update:** - -```bash -bd update bd-42 --status in_progress --json -bd update bd-42 --priority 1 --json -``` - -**Complete work:** - -```bash -bd close bd-42 --reason "Completed" --json -``` - -### Issue Types - -- `bug` - Something broken -- `feature` - New functionality -- `task` - Work item (tests, docs, refactoring) -- `epic` - Large feature with subtasks -- `chore` - Maintenance (dependencies, tooling) - -### Priorities - -- `0` - Critical (security, data loss, broken builds) -- `1` - High (major features, important bugs) -- `2` - Medium (default, nice-to-have) -- `3` - Low (polish, optimization) -- `4` - Backlog (future ideas) - -### Workflow for AI Agents - -1. **Check ready work**: `bd ready` shows unblocked issues -2. **Claim your task**: `bd update --status in_progress` -3. **Work on it**: Implement, test, document -4. **Discover new work?** Create linked issue: - - `bd create "Found bug" -p 1 --deps discovered-from:` -5. **Complete**: `bd close --reason "Done"` -6. **Commit together**: Always commit the `.beads/issues.jsonl` file together with the code changes so issue state stays in sync with code state - -### Auto-Sync - -bd automatically syncs with git: - -- Exports to `.beads/issues.jsonl` after changes (5s debounce) -- Imports from JSONL when newer (e.g., after `git pull`) -- No manual export/import needed! - -### MCP Server (Recommended) - -If using Claude or MCP-compatible clients, install the beads MCP server: - -```bash -pip install beads-mcp -``` - -Add to MCP config (e.g., `~/.config/claude/config.json`): - -```json -{ - "beads": { - "command": "beads-mcp", - "args": [] - } -} -``` - -Then use `mcp__beads__*` functions instead of CLI commands. - -### Managing AI-Generated Planning Documents - -AI assistants often create planning and design documents during development: - -- PLAN.md, IMPLEMENTATION.md, ARCHITECTURE.md -- DESIGN.md, CODEBASE_SUMMARY.md, INTEGRATION_PLAN.md -- TESTING_GUIDE.md, TECHNICAL_DESIGN.md, and similar files - -**Best Practice: Use a dedicated directory for these ephemeral files** - -**Recommended approach:** - -- Create a `history/` directory in the project root -- Store ALL AI-generated planning/design docs in `history/` -- Keep the repository root clean and focused on permanent project files -- Only access `history/` when explicitly asked to review past planning - -**Example .gitignore entry (optional):** - -``` -# AI planning documents (ephemeral) -history/ -``` - -**Benefits:** - -- ✅ Clean repository root -- ✅ Clear separation between ephemeral and permanent documentation -- ✅ Easy to exclude from version control if desired -- ✅ Preserves planning history for archeological research -- ✅ Reduces noise when browsing the project - -### Important Rules - -- ✅ Use bd for ALL task tracking -- ✅ Always use `--json` flag for programmatic use -- ✅ Link discovered work with `discovered-from` dependencies -- ✅ Check `bd ready` before asking "what should I work on?" -- ✅ Store AI planning docs in `history/` directory -- ✅ Mirror delivery/release status in Linear project `css-lsp` -- ❌ Do NOT create markdown TODO lists -- ❌ Do NOT replace bd with external trackers for repo-local dependency planning -- ❌ Do NOT duplicate tracking systems -- ❌ Do NOT clutter repo root with planning documents - -For more details, see README.md and QUICKSTART.md. From 7aa9066bea3eea55c6f7df4e48c318d904c1504c Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 15:07:44 +0200 Subject: [PATCH 07/25] chore: update package-lock.json to match package.json --- package-lock.json | 396 ++++++++-------------------------------------- 1 file changed, 68 insertions(+), 328 deletions(-) diff --git a/package-lock.json b/package-lock.json index a216ab3..55de435 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "css-variable-lsp", - "version": "1.0.16", + "version": "1.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "css-variable-lsp", - "version": "1.0.16", + "version": "1.0.19", "license": "GPL-3.0", "dependencies": { - "css-tree": "^3.1.0", + "css-tree": "^3.2.1", "glob": "^12.0.0", - "node-html-parser": "^7.0.1", + "node-html-parser": "^7.1.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/css-tree": "^2.3.11", - "@types/node": "^24.10.1", + "@types/node": "^24.12.0", "ts-node": "^10.9.2", "typescript": "^5.9.3" }, @@ -42,42 +42,13 @@ "node": ">=12" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@jridgewell/resolve-uri": { @@ -144,20 +115,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -168,9 +138,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", "dependencies": { @@ -180,30 +150,6 @@ "node": ">=0.4.0" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -211,30 +157,33 @@ "dev": true, "license": "MIT" }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=7.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -273,13 +222,13 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -298,9 +247,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -362,18 +311,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -434,15 +371,6 @@ "he": "bin/he" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -450,12 +378,12 @@ "license": "ISC" }, "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^9.0.0" }, "engines": { "node": "20 || >=22" @@ -465,9 +393,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -481,39 +409,39 @@ "license": "ISC" }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/node-html-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", - "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", "license": "MIT", "dependencies": { "css-select": "^5.1.0", @@ -548,16 +476,16 @@ } }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -605,102 +533,6 @@ "node": ">=0.10.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -751,7 +583,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -838,97 +669,6 @@ "node": ">= 8" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", From 5b13aa47f3ab558c19e746494caf66f9a9c28cae Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 16:41:18 +0200 Subject: [PATCH 08/25] chore: remove MIGRATION.md (updates complete) --- MIGRATION.md | 91 ---------------------------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 MIGRATION.md diff --git a/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index 9965380..0000000 --- a/MIGRATION.md +++ /dev/null @@ -1,91 +0,0 @@ -# Dependency Migration Guide - -## Overview - -This guide covers updating dependencies to their latest compatible versions. - -## Current Outdated Dependencies - -| Package | Current | Wanted | Latest | Type | -|---------|---------|--------|--------|------| -| @types/node | 24.10.1 | 24.12.0 | 25.5.0 | patch | -| css-tree | 3.1.0 | 3.2.1 | 3.2.1 | minor | -| glob | 12.0.0 | 12.0.0 | 13.0.6 | major | -| node-html-parser | 7.0.1 | 7.1.0 | 7.1.0 | minor | - -## Safe Updates (Backward Compatible) - -### @types/node 24.10.1 → 24.12.0 - -**Type:** Patch update - -**Changes:** Minor type definitions updates. - -**Action:** Update with `pnpm update @types/node` - -### css-tree 3.1.0 → 3.2.1 - -**Type:** Minor update - -**New Features (3.2.0):** -- Added `list` option to `parse()` method to control child node format (List vs array) -- Added `onToken` option for advanced token handling -- Added math functions support (`min()`, `max()`, etc.) -- Added `sideEffects: false` for better tree-shaking - -**Bug Fixes (3.2.1):** -- Fixed parsing of nested function in definition syntax - -**Impact:** All changes are additive. Existing code continues to work. - -**Action:** Update with `pnpm update css-tree` - -### node-html-parser 7.0.1 → 7.1.0 - -**Type:** Minor update - -**New Features:** -- Added `closeAllOnClosing` option -- Added `preserveTagNesting` option - -**Impact:** All changes are additive. Existing code continues to work. - -**Action:** Update with `pnpm update node-html-parser` - -## Optional Major Update - -### glob 12.0.0 → 13.0.6 - -**Type:** Major update (breaking) - -**Breaking Changes:** -- CLI moved to separate package `glob-bin` -- `--shell` option removed - -**Impact:** None if used programmatically (API unchanged). CLI users must install `glob-bin` separately. - -**Action:** Update only if needed. Current semver range `^12.0.0` will NOT auto-upgrade to v13. - -## Update Commands - -```bash -# Install pnpm lockfile if missing -pnpm install - -# Update all dependencies to wanted versions -pnpm update - -# Or update specific packages -pnpm update @types/node css-tree node-html-parser - -# For glob v13 (optional, breaking) -pnpm update glob@13 -``` - -## Verification - -After updating, run tests to verify: - -```bash -pnpm test -``` From 73977de84bea07968a41a3396078904eb69f1a3a Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 18:41:43 +0200 Subject: [PATCH 09/25] show matching variable names in color replacement diagnostics --- src/colorVariableFeature.ts | 2 +- tests/colorVariableFeature.test.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/colorVariableFeature.ts b/src/colorVariableFeature.ts index 1b77f92..c6d2df5 100644 --- a/src/colorVariableFeature.ts +++ b/src/colorVariableFeature.ts @@ -39,7 +39,7 @@ export function collectColorReplacementDiagnostics( const message = variableNames.length === 1 ? `Literal color can be replaced with matching CSS variable '${variableNames[0]}'` - : `Literal color can be replaced with ${variableNames.length} matching CSS variables`; + : `Literal color can be replaced with matching CSS variables: ${variableNames.map((n) => `'${n}'`).join(", ")}`; return [ { diff --git a/tests/colorVariableFeature.test.ts b/tests/colorVariableFeature.test.ts index 3137db3..84e3daf 100644 --- a/tests/colorVariableFeature.test.ts +++ b/tests/colorVariableFeature.test.ts @@ -62,7 +62,7 @@ test("diagnostics are informational and preserve multiple variable options", () assert.strictEqual(diagnostics.length, 1); assert.strictEqual(diagnostics[0].severity, DiagnosticSeverity.Information); - assert.match(diagnostics[0].message, /2 matching CSS variables/); + assert.match(diagnostics[0].message, /matching CSS variables: '--paper', '--white'/); }); test("diagnostics exclude self-references inside matching custom property definitions", () => { @@ -82,6 +82,19 @@ test("diagnostics exclude self-references inside matching custom property defini assert.doesNotMatch(diagnostics[0].message, /'--white'/); }); +test("diagnostic message includes variable name when only one match", () => { + const manager = new CssVariableManager(); + manager.parseContent(":root { --white: #fff; }", "file:///vars.css", "css"); + const doc = createDoc("file:///test.css", ".title { color: white; }"); + + manager.parseDocument(doc); + const diagnostics = collectColorReplacementDiagnostics(doc, manager); + + assert.strictEqual(diagnostics.length, 1); + assert.strictEqual(diagnostics[0].severity, DiagnosticSeverity.Information); + assert.match(diagnostics[0].message, /matching CSS variable '--white'/); +}); + test("completion items replace the full literal color token", () => { const manager = new CssVariableManager(); manager.parseContent(":root { --white: #fff; }", "file:///vars.css", "css"); From 80f305de6c2dd61bae2686422c416201309b15cb Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 19:02:13 +0200 Subject: [PATCH 10/25] skip color replacement diagnostics on variable definitions --- src/colorVariableFeature.ts | 6 ++++++ tests/colorVariableFeature.test.ts | 9 ++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/colorVariableFeature.ts b/src/colorVariableFeature.ts index c6d2df5..218599f 100644 --- a/src/colorVariableFeature.ts +++ b/src/colorVariableFeature.ts @@ -30,6 +30,12 @@ export function collectColorReplacementDiagnostics( return cssVariableManager .getDocumentColorLiterals(document.uri) .flatMap((literal) => { + // Skip color literals that are part of variable definitions (--var: color) + // We only want to show diagnostics on usages, not definitions + if (literal.variableName) { + return []; + } + const matches = getMatchingVariables(literal, cssVariableManager); if (matches.length === 0) { return []; diff --git a/tests/colorVariableFeature.test.ts b/tests/colorVariableFeature.test.ts index 84e3daf..3203adb 100644 --- a/tests/colorVariableFeature.test.ts +++ b/tests/colorVariableFeature.test.ts @@ -65,10 +65,10 @@ test("diagnostics are informational and preserve multiple variable options", () assert.match(diagnostics[0].message, /matching CSS variables: '--paper', '--white'/); }); -test("diagnostics exclude self-references inside matching custom property definitions", () => { +test("diagnostics are not shown on variable definitions", () => { const manager = new CssVariableManager(); manager.parseContent( - ":root { --white: #fff; --paper: #fff; }", + ":root { --white: #fff; --paper: rgb(255 255 255); }", "file:///vars.css", "css" ); @@ -77,9 +77,8 @@ test("diagnostics exclude self-references inside matching custom property defini manager.parseDocument(doc); const diagnostics = collectColorReplacementDiagnostics(doc, manager); - assert.strictEqual(diagnostics.length, 1); - assert.match(diagnostics[0].message, /'--paper'/); - assert.doesNotMatch(diagnostics[0].message, /'--white'/); + // No diagnostics on definitions (only on usages) + assert.strictEqual(diagnostics.length, 0); }); test("diagnostic message includes variable name when only one match", () => { From 7b2aa9f372a0db61255199c2d54a117da8603a3e Mon Sep 17 00:00:00 2001 From: lmn451 Date: Thu, 19 Mar 2026 19:20:21 +0200 Subject: [PATCH 11/25] fix: add error handling and optimize color lookup performance - Add try-catch blocks to color replacement functions to prevent LSP crashes - Implement color index (Map>) for O(1) color lookups - Replace O(n*m) iteration with direct HashMap lookup in getVariablesByColor() - Mark index dirty when variables change, rebuild lazily on first access Performance: Color lookup now scales with number of matches (O(k)) instead of total variables (O(n*m)). --- src/colorVariableFeature.ts | 142 +++++++++++++++++++----------------- src/cssVariableManager.ts | 50 ++++++++++--- tsconfig.tsbuildinfo | 2 +- 3 files changed, 117 insertions(+), 77 deletions(-) diff --git a/src/colorVariableFeature.ts b/src/colorVariableFeature.ts index 218599f..f05aa46 100644 --- a/src/colorVariableFeature.ts +++ b/src/colorVariableFeature.ts @@ -27,91 +27,103 @@ export function collectColorReplacementDiagnostics( document: TextDocument, cssVariableManager: CssVariableManager ): Diagnostic[] { - return cssVariableManager - .getDocumentColorLiterals(document.uri) - .flatMap((literal) => { - // Skip color literals that are part of variable definitions (--var: color) - // We only want to show diagnostics on usages, not definitions - if (literal.variableName) { - return []; - } - - const matches = getMatchingVariables(literal, cssVariableManager); - if (matches.length === 0) { - return []; - } - - const variableNames = matches.map((match) => match.name); - const message = - variableNames.length === 1 - ? `Literal color can be replaced with matching CSS variable '${variableNames[0]}'` - : `Literal color can be replaced with matching CSS variables: ${variableNames.map((n) => `'${n}'`).join(", ")}`; - - return [ - { - severity: DiagnosticSeverity.Information, - range: literal.range, - message, - source: "css-variable-lsp", - code: COLOR_REPLACEMENT_DIAGNOSTIC_CODE, - data: { - kind: COLOR_REPLACEMENT_DIAGNOSTIC_CODE, - variableNames, - } satisfies ColorReplacementDiagnosticData, - }, - ]; - }); + try { + return cssVariableManager + .getDocumentColorLiterals(document.uri) + .flatMap((literal) => { + // Skip color literals that are part of variable definitions (--var: color) + // We only want to show diagnostics on usages, not definitions + if (literal.variableName) { + return []; + } + + const matches = getMatchingVariables(literal, cssVariableManager); + if (matches.length === 0) { + return []; + } + + const variableNames = matches.map((match) => match.name); + const message = + variableNames.length === 1 + ? `Literal color can be replaced with matching CSS variable '${variableNames[0]}'` + : `Literal color can be replaced with matching CSS variables: ${variableNames.map((n) => `'${n}'`).join(", ")}`; + + return [ + { + severity: DiagnosticSeverity.Information, + range: literal.range, + message, + source: "css-variable-lsp", + code: COLOR_REPLACEMENT_DIAGNOSTIC_CODE, + data: { + kind: COLOR_REPLACEMENT_DIAGNOSTIC_CODE, + variableNames, + } satisfies ColorReplacementDiagnosticData, + }, + ]; + }); + } catch (error) { + console.error("Error collecting color diagnostics:", error); + return []; + } } - export function getColorReplacementCompletionItems( document: TextDocument, position: Position, cssVariableManager: CssVariableManager, displayOptions: CompletionDisplayOptions ): CompletionItem[] { - const literal = findColorLiteralAtPosition(document, position, cssVariableManager); - if (!literal) { + try { + const literal = findColorLiteralAtPosition(document, position, cssVariableManager); + if (!literal) { + return []; + } + + return getMatchingVariables(literal, cssVariableManager).map((match) => + createColorReplacementCompletionItem(document, literal.range, match, displayOptions) + ); + } catch (error) { + console.error("Error getting color replacement completions:", error); return []; } - - return getMatchingVariables(literal, cssVariableManager).map((match) => - createColorReplacementCompletionItem(document, literal.range, match, displayOptions) - ); } - export function getColorReplacementCodeActions( document: TextDocument, diagnostics: Diagnostic[] ): CodeAction[] { - const actions: CodeAction[] = []; + try { + const actions: CodeAction[] = []; - for (const diagnostic of diagnostics) { - if (diagnostic.code !== COLOR_REPLACEMENT_DIAGNOSTIC_CODE) { - continue; - } + for (const diagnostic of diagnostics) { + if (diagnostic.code !== COLOR_REPLACEMENT_DIAGNOSTIC_CODE) { + continue; + } - const data = diagnostic.data as ColorReplacementDiagnosticData | undefined; - const variableNames = data?.variableNames || []; - - for (const variableName of variableNames) { - actions.push({ - title: `Replace with var(${variableName})`, - kind: CodeActionKind.QuickFix, - diagnostics: [diagnostic], - edit: { - changes: { - [document.uri]: [ - TextEdit.replace(diagnostic.range, `var(${variableName})`), - ], + const data = diagnostic.data as ColorReplacementDiagnosticData | undefined; + const variableNames = data?.variableNames || []; + + for (const variableName of variableNames) { + actions.push({ + title: `Replace with var(${variableName})`, + kind: CodeActionKind.QuickFix, + diagnostics: [diagnostic], + edit: { + changes: { + [document.uri]: [ + TextEdit.replace(diagnostic.range, `var(${variableName})`), + ], + }, }, - }, - }); + }); + } } - } - return actions; + return actions; + } catch (error) { + console.error("Error getting color replacement code actions:", error); + return []; + } } - function findColorLiteralAtPosition( document: TextDocument, position: Position, diff --git a/src/cssVariableManager.ts b/src/cssVariableManager.ts index 02abaa9..902a31d 100644 --- a/src/cssVariableManager.ts +++ b/src/cssVariableManager.ts @@ -128,6 +128,8 @@ export class CssVariableManager { private usages: Map = new Map(); private colorLiterals: Map = new Map(); private domTrees: Map = new Map(); // URI -> DOM tree + private colorIndex: Map> = new Map(); // color key -> variable names + private colorIndexDirty: boolean = true; private logger: Logger; private lookupFiles: string[]; private ignoreGlobs: string[]; @@ -171,6 +173,35 @@ export class CssVariableManager { return extensions; } + /** + * Rebuild the color index for O(1) color lookups. + * Should be called when variables change. + */ + private rebuildColorIndex(): void { + this.colorIndex.clear(); + + for (const name of this.variables.keys()) { + const resolvedColor = this.resolveVariableColor(name); + if (resolvedColor) { + const key = getNormalizedColorKey(resolvedColor); + if (!this.colorIndex.has(key)) { + this.colorIndex.set(key, new Set()); + } + this.colorIndex.get(key)!.add(name); + } + } + + this.colorIndexDirty = false; + } + /** + * Ensure the color index is up to date. + */ + private ensureColorIndex(): void { + if (this.colorIndexDirty) { + this.rebuildColorIndex(); + } + } + private resolveLanguageId(filePath: string): string | null { const ext = path.extname(filePath); if (!ext) { @@ -459,6 +490,7 @@ export class CssVariableManager { this.variables.set(name, []); } this.variables.get(name)?.push(variable); + this.colorIndexDirty = true; } } @@ -629,6 +661,7 @@ export class CssVariableManager { this.variables.set(name, []); } this.variables.get(name)?.push(variable); + this.colorIndexDirty = true; } } @@ -854,8 +887,8 @@ export class CssVariableManager { this.variables.set(name, filtered); } } + this.colorIndexDirty = true; } - public clearDocumentUsages(uri: string): void { const normalizedUri = normalizeUri(uri); for (const [name, usgs] of this.usages.entries()) { @@ -935,28 +968,23 @@ export class CssVariableManager { color: Color, options: { excludeName?: string } = {} ): CssVariable[] { + this.ensureColorIndex(); const key = getNormalizedColorKey(color); + const names = this.colorIndex.get(key) || new Set(); + const matches: CssVariable[] = []; - - for (const name of this.variables.keys()) { + for (const name of names) { if (options.excludeName && name === options.excludeName) { continue; } - - const resolvedColor = this.resolveVariableColor(name); - if (!resolvedColor || getNormalizedColorKey(resolvedColor) !== key) { - continue; - } - const winningDefinition = this.getWinningVariableDefinition(name); if (winningDefinition) { matches.push(winningDefinition); } } - + return matches.toSorted((a, b) => a.name.localeCompare(b.name)); } - /** * Resolve a variable name to a Color if possible. * Handles recursive variable references: var(--a) -> var(--b) -> #fff diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 055e9eb..de288c6 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/colorprovider.ts","./src/colorservice.ts","./src/colorvariablefeature.ts","./src/completioncontext.ts","./src/cssvariablemanager.ts","./src/domtree.ts","./src/initialize.ts","./src/pathdisplay.ts","./src/runtimeconfig.ts","./src/server.ts","./src/specificity.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/colorprovider.ts","./src/colorservice.ts","./src/colorvariablefeature.ts","./src/completioncontext.ts","./src/cssvariablemanager.ts","./src/domtree.ts","./src/flags.ts","./src/initialize.ts","./src/pathdisplay.ts","./src/runtimeconfig.ts","./src/server.ts","./src/specificity.ts"],"version":"5.9.3"} \ No newline at end of file From 5ec0de5c1318634c788daffb24c1a47007e84276 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Fri, 20 Mar 2026 10:22:02 +0200 Subject: [PATCH 12/25] perf: use line-based bucketing for color literal lookups Replace O(n) linear search with O(k) line-bucketed lookup in findColorLiteralAtPosition, where k = colors on target line. Changes: - colorLiterals now uses Map> - Added getDocumentColorLiteralsByLine() method - getDocumentColorLiterals() flattens for backward compatibility - findColorLiteralAtPosition uses bucketed lookup --- src/colorVariableFeature.ts | 64 ++++++++++++++++++++---- src/cssVariableManager.ts | 97 +++++++++++++++---------------------- 2 files changed, 94 insertions(+), 67 deletions(-) diff --git a/src/colorVariableFeature.ts b/src/colorVariableFeature.ts index f05aa46..ef98bbd 100644 --- a/src/colorVariableFeature.ts +++ b/src/colorVariableFeature.ts @@ -11,6 +11,7 @@ import { } from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; import { CssColorLiteral, CssVariable, CssVariableManager } from "./cssVariableManager"; +import { Logger } from "./logger"; export const COLOR_REPLACEMENT_DIAGNOSTIC_CODE = "replace-with-css-variable"; @@ -25,14 +26,13 @@ export interface CompletionDisplayOptions { export function collectColorReplacementDiagnostics( document: TextDocument, - cssVariableManager: CssVariableManager + cssVariableManager: CssVariableManager, + logger: Logger ): Diagnostic[] { try { return cssVariableManager .getDocumentColorLiterals(document.uri) .flatMap((literal) => { - // Skip color literals that are part of variable definitions (--var: color) - // We only want to show diagnostics on usages, not definitions if (literal.variableName) { return []; } @@ -63,7 +63,7 @@ export function collectColorReplacementDiagnostics( ]; }); } catch (error) { - console.error("Error collecting color diagnostics:", error); + logger.error(`Error collecting color diagnostics: ${error}`); return []; } } @@ -71,7 +71,8 @@ export function getColorReplacementCompletionItems( document: TextDocument, position: Position, cssVariableManager: CssVariableManager, - displayOptions: CompletionDisplayOptions + displayOptions: CompletionDisplayOptions, + logger: Logger ): CompletionItem[] { try { const literal = findColorLiteralAtPosition(document, position, cssVariableManager); @@ -83,13 +84,14 @@ export function getColorReplacementCompletionItems( createColorReplacementCompletionItem(document, literal.range, match, displayOptions) ); } catch (error) { - console.error("Error getting color replacement completions:", error); + logger.error(`Error getting color replacement completions: ${error}`); return []; } } export function getColorReplacementCodeActions( document: TextDocument, - diagnostics: Diagnostic[] + diagnostics: Diagnostic[], + logger: Logger ): CodeAction[] { try { const actions: CodeAction[] = []; @@ -100,7 +102,12 @@ export function getColorReplacementCodeActions( } const data = diagnostic.data as ColorReplacementDiagnosticData | undefined; - const variableNames = data?.variableNames || []; + if (!isValidDiagnosticData(data)) { + logger.error(`Invalid diagnostic data for code action: ${JSON.stringify(diagnostic.data)}`); + continue; + } + + const variableNames = data.variableNames; for (const variableName of variableNames) { actions.push({ @@ -120,7 +127,7 @@ export function getColorReplacementCodeActions( return actions; } catch (error) { - console.error("Error getting color replacement code actions:", error); + logger.error(`Error getting color replacement code actions: ${error}`); return []; } } @@ -130,10 +137,19 @@ function findColorLiteralAtPosition( cssVariableManager: CssVariableManager ): CssColorLiteral | null { const offset = document.offsetAt(position); + const targetLine = position.line; + + const lineLiterals = cssVariableManager.getDocumentColorLiteralsByLine( + document.uri, + targetLine + ); - for (const literal of cssVariableManager.getDocumentColorLiterals(document.uri)) { + for (const literal of lineLiterals) { const start = document.offsetAt(literal.range.start); const end = document.offsetAt(literal.range.end); + if (!isRangeValid(literal.range) || start > end) { + continue; + } if (offset >= start && offset <= end) { return literal; } @@ -142,6 +158,34 @@ function findColorLiteralAtPosition( return null; } +function isValidDiagnosticData(data: unknown): data is ColorReplacementDiagnosticData { + if (typeof data !== "object" || data === null) { + return false; + } + const obj = data as Record; + return ( + obj.kind === COLOR_REPLACEMENT_DIAGNOSTIC_CODE && + Array.isArray(obj.variableNames) && + obj.variableNames.every((v) => typeof v === "string") + ); +} + +function isRangeValid(range: Range): boolean { + if (range.start.line < 0 || range.start.character < 0) { + return false; + } + if (range.end.line < 0 || range.end.character < 0) { + return false; + } + if (range.start.line > range.end.line) { + return false; + } + if (range.start.line === range.end.line && range.start.character > range.end.character) { + return false; + } + return true; +} + function getMatchingVariables( literal: CssColorLiteral, cssVariableManager: CssVariableManager diff --git a/src/cssVariableManager.ts b/src/cssVariableManager.ts index 902a31d..6bd944c 100644 --- a/src/cssVariableManager.ts +++ b/src/cssVariableManager.ts @@ -10,6 +10,7 @@ import { Color } from "vscode-languageserver/node"; import { getNormalizedColorKey, parseColor } from "./colorService"; import { calculateSpecificity, compareSpecificity } from "./specificity"; import * as path from "path"; +import { Logger } from "./logger"; export interface CssVariable { name: string; @@ -42,11 +43,6 @@ export interface CssColorLiteral { variableName?: string; } -export interface Logger { - log(message: string): void; - error(message: string): void; -} - const DEFAULT_LOOKUP_FILES = [ "**/*.css", "**/*.scss", @@ -126,7 +122,7 @@ function normalizeUri(uri: string): string { export class CssVariableManager { private variables: Map = new Map(); private usages: Map = new Map(); - private colorLiterals: Map = new Map(); + private colorLiterals: Map> = new Map(); private domTrees: Map = new Map(); // URI -> DOM tree private colorIndex: Map> = new Map(); // color key -> variable names private colorIndexDirty: boolean = true; @@ -135,19 +131,8 @@ export class CssVariableManager { private ignoreGlobs: string[]; private lookupExtensions: Map; - constructor(logger?: Logger, lookupFiles?: string[], ignoreGlobs?: string[]) { - this.logger = logger || { - log: (message: string) => { - // Only log to console in debug mode - if (process.env.CSS_LSP_DEBUG) { - console.log(message); - } - }, - error: (message: string) => { - // Always log errors - console.error(message); - }, - }; + constructor(logger: Logger, lookupFiles?: string[], ignoreGlobs?: string[]) { + this.logger = logger; const normalizedLookupFiles = normalizeGlobList(lookupFiles); const normalizedIgnoreGlobs = normalizeGlobList(ignoreGlobs); @@ -246,9 +231,7 @@ export class CssVariableManager { absolute: true, }); - this.logger.log( - `[css-lsp] Scanned ${folder}: found ${files.length} files` - ); + this.logger.debug("scanFolder", { folder, fileCount: files.length }); allFiles.push(...files); } @@ -268,9 +251,7 @@ export class CssVariableManager { this.parseContent(content, fileUri, languageId); } catch (error) { - this.logger.error( - `[css-lsp] Error scanning file ${filePath}: ${error}` - ); + this.logger.error("scanFileError", { filePath, error: String(error) }); } processedFiles++; @@ -284,9 +265,7 @@ export class CssVariableManager { } } - this.logger.log( - `[css-lsp] Workspace scan complete. Processed ${totalFiles} files.` - ); + this.logger.debug("workspaceScanComplete", { totalFiles }); } public parseDocument(document: TextDocument): void { @@ -308,7 +287,7 @@ export class CssVariableManager { const domTree = new DOMTree(text); this.domTrees.set(uri, domTree); } catch (error) { - this.logger.error(`Error parsing HTML for ${uri}: ${error}`); + this.logger.error("parseHtmlError", { uri, error: String(error) }); } // Use node-html-parser to extract style blocks and inline styles @@ -370,7 +349,7 @@ export class CssVariableManager { } } } catch (error) { - this.logger.error(`Error parsing HTML content for ${uri}: ${error}`); + this.logger.error("parseHtmlContentError", { uri, error: String(error) }); } } else { // CSS, SCSS, SASS, LESS @@ -389,9 +368,7 @@ export class CssVariableManager { const ast = csstree.parse(text, { positions: true, onParseError: (error) => { - this.logger.log( - `[css-lsp] CSS Parse Error in ${uri}: ${error.message}` - ); + this.logger.debug("cssParseError", { uri, error: error.message }); }, }); @@ -562,7 +539,7 @@ export class CssVariableManager { }, }); } catch (e) { - this.logger.error(`Error parsing CSS in ${uri}: ${e}`); + this.logger.error("parseCssError", { uri, error: String(e) }); } } @@ -582,9 +559,7 @@ export class CssVariableManager { context: "declarationList", positions: true, onParseError: (error) => { - this.logger.log( - `[css-lsp] Inline Style Parse Error in ${uri}: ${error.message}` - ); + this.logger.debug("inlineStyleParseError", { uri, error: error.message }); }, }); @@ -728,7 +703,7 @@ export class CssVariableManager { }, }); } catch (e) { - this.logger.error(`Error parsing inline style in ${uri}: ${e}`); + this.logger.error("parseInlineStyleError", { uri, error: String(e) }); } } @@ -739,7 +714,7 @@ export class CssVariableManager { text: string, offset: number ): void { - const literals = this.colorLiterals.get(normalizeUri(uri)) || []; + const lineMap = this.colorLiterals.get(normalizeUri(uri)) || new Map(); this.collectColorLiteralsFromValue( declaration.value, @@ -748,7 +723,7 @@ export class CssVariableManager { text, offset, declaration.property, - literals + lineMap ); if (declaration.value.type === "Raw" && declaration.value.loc) { @@ -764,16 +739,14 @@ export class CssVariableManager { declaration.value.value, offset + declaration.value.loc.start.offset, declaration.property, - literals + lineMap ); } catch (error) { - this.logger.log( - `[css-lsp] Raw value parse error in ${uri}: ${String(error)}` - ); + this.logger.debug("rawValueParseError", { uri, error: String(error) }); } } - this.colorLiterals.set(normalizeUri(uri), literals); + this.colorLiterals.set(normalizeUri(uri), lineMap); } private collectColorLiteralsFromValue( @@ -783,7 +756,7 @@ export class CssVariableManager { sourceText: string, baseOffset: number, propertyName: string, - literals: CssColorLiteral[] + lineMap: Map ): void { csstree.walk(valueNode, { enter: (node: csstree.CssNode) => { @@ -822,12 +795,18 @@ export class CssVariableManager { baseOffset + node.loc.start.offset + leadingWhitespace; const endOffset = baseOffset + node.loc.end.offset - trailingWhitespace; - literals.push({ + const range = Range.create( + document.positionAt(startOffset), + document.positionAt(endOffset) + ); + + const line = range.start.line; + if (!lineMap.has(line)) { + lineMap.set(line, []); + } + lineMap.get(line)!.push({ uri, - range: Range.create( - document.positionAt(startOffset), - document.positionAt(endOffset) - ), + range, value, color, propertyName, @@ -841,9 +820,7 @@ export class CssVariableManager { try { const filePath = URI.parse(uri).fsPath; if (!fs.existsSync(filePath)) { - this.logger.log( - `[css-lsp] File ${uri} does not exist on disk, removing from manager.` - ); + this.logger.debug("fileNotFound", { uri }); this.removeFile(uri); return; } @@ -861,9 +838,9 @@ export class CssVariableManager { } this.parseContent(content, uri, languageId); - this.logger.log(`[css-lsp] Updated file ${uri} from disk.`); + this.logger.debug("fileUpdatedFromDisk", { uri }); } catch (error) { - this.logger.error(`[css-lsp] Error updating file ${uri}: ${error}`); + this.logger.error("fileUpdateError", { uri, error: String(error) }); } } @@ -929,7 +906,13 @@ export class CssVariableManager { } public getDocumentColorLiterals(uri: string): CssColorLiteral[] { - return this.colorLiterals.get(normalizeUri(uri)) || []; + const lineMap = this.colorLiterals.get(normalizeUri(uri)); + if (!lineMap) { return []; } + return Array.from(lineMap.values()).flat(); + } + + public getDocumentColorLiteralsByLine(uri: string, line: number): CssColorLiteral[] { + return this.colorLiterals.get(normalizeUri(uri))?.get(line) ?? []; } /** From 11585c857c4937dd9129d5b18d7cd026fbf4fde5 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Fri, 20 Mar 2026 10:33:19 +0200 Subject: [PATCH 13/25] feat: extract Logger interface to separate module Move Logger interface and createLogger factory from cssVariableManager.ts to dedicated src/logger.ts module for better separation of concerns. --- src/logger.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/logger.ts diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..8a4e430 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,39 @@ +export type LogLevel = "debug" | "info" | "warn" | "error"; + +export interface Logger { + debug(label: string, payload?: unknown): void; + info(label: string, payload?: unknown): void; + warn(label: string, payload?: unknown): void; + error(label: string, payload?: unknown): void; +} + +function formatMessage(level: LogLevel, label: string, payload?: unknown): string { + const prefix = `[css-lsp][${level}]`; + const meta = payload !== undefined ? ` ${JSON.stringify(payload)}` : ""; + return `${prefix} ${label}${meta}`; +} + +export function createLogger(envVar = "CSS_LSP_DEBUG"): Logger { + const isDebug = !!process.env[envVar]; + + return { + debug: (label, payload) => { + if (isDebug) { + console.log(formatMessage("debug", label, payload)); + } + }, + info: (label, payload) => { + if (isDebug) { + console.log(formatMessage("info", label, payload)); + } + }, + warn: (label, payload) => { + if (isDebug) { + console.log(formatMessage("warn", label, payload)); + } + }, + error: (label, payload) => { + console.error(formatMessage("error", label, payload)); + }, + }; +} From 62bad4bf0d6551433b3ca78c82ee4e05f6bb4358 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Fri, 20 Mar 2026 10:44:57 +0200 Subject: [PATCH 14/25] test: update tests to use new Logger interface CssVariableManager now requires a Logger argument. Update all test files to import Logger and pass SilentLogger instance. --- tests/cascadeAndInline.test.ts | 20 +++-- tests/colorCascadeResolution.test.ts | 34 ++++--- tests/colorFormatting.test.ts | 12 ++- tests/colorProvider.test.ts | 16 +++- tests/colorVariableFeature.test.ts | 39 +++++--- tests/contextualCompletion.test.ts | 42 +++++---- tests/cssVariableManager.test.ts | 26 ++++-- tests/debugLogging.test.ts | 130 +++++++++++---------------- tests/diagnostics.test.ts | 40 +++++---- tests/example_files.test.ts | 14 ++- tests/fileLifecycle.test.ts | 22 +++-- tests/fileTypesAndUpdates.test.ts | 18 +++- tests/htmlComments.test.ts | 22 +++-- tests/nestedVarParsing.test.ts | 26 ++++-- tests/perf.test.ts | 12 ++- tests/renameMultiFile.test.ts | 36 +++++--- tests/workspaceScan.test.ts | 11 ++- 17 files changed, 325 insertions(+), 195 deletions(-) diff --git a/tests/cascadeAndInline.test.ts b/tests/cascadeAndInline.test.ts index e8cb4a7..36a4877 100644 --- a/tests/cascadeAndInline.test.ts +++ b/tests/cascadeAndInline.test.ts @@ -2,13 +2,23 @@ import { test } from "node:test"; import { strict as assert } from "node:assert"; import { CssVariableManager } from "../src/cssVariableManager"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { Logger } from "../src/logger"; + +class SilentLogger implements Logger { + debug(_label: string, _payload?: unknown): void {} + info(_label: string, _payload?: unknown): void {} + warn(_label: string, _payload?: unknown): void {} + error(_label: string, _payload?: unknown): void {} +} + +const silentLogger = new SilentLogger(); function createDoc(uri: string, content: string, languageId: string = "css") { return TextDocument.create(uri, languageId, 1, content); } test("!important tracking", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ":root { --color: red !important; } div { --color: blue; }"; const doc = createDoc("file:///test.css", css); manager.parseDocument(doc); @@ -26,7 +36,7 @@ test("!important tracking", () => { }); test("source order tracking", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ":root { --a: first; } :root { --a: second; }"; const doc = createDoc("file:///test.css", css); manager.parseDocument(doc); @@ -37,7 +47,7 @@ test("source order tracking", () => { }); test("inline style parsing", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const html = '
'; const doc = createDoc("file:///test.html", html, "html"); @@ -52,7 +62,7 @@ test("inline style parsing", () => { }); test("inline style definitions are tracked", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const html = '
'; const doc = createDoc("file:///test.html", html, "html"); @@ -65,7 +75,7 @@ test("inline style definitions are tracked", () => { }); test("combined cascade tracking", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --x: root; } div { --x: div; } diff --git a/tests/colorCascadeResolution.test.ts b/tests/colorCascadeResolution.test.ts index 92bafb5..222586c 100644 --- a/tests/colorCascadeResolution.test.ts +++ b/tests/colorCascadeResolution.test.ts @@ -2,13 +2,23 @@ import { test } from "node:test"; import { strict as assert } from "node:assert"; import { CssVariableManager } from "../src/cssVariableManager"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { Logger } from "../src/logger"; + +class SilentLogger implements Logger { + debug(_label: string, _payload?: unknown): void {} + info(_label: string, _payload?: unknown): void {} + warn(_label: string, _payload?: unknown): void {} + error(_label: string, _payload?: unknown): void {} +} + +const silentLogger = new SilentLogger(); function createDoc(uri: string, content: string, languageId: string = "css") { return TextDocument.create(uri, languageId, 1, content); } test("color resolution respects !important flag", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --color: #ff0000; } div { --color: #0000ff !important; } @@ -25,7 +35,7 @@ test("color resolution respects !important flag", () => { }); test("color resolution uses specificity when no !important", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --color: #ff0000; } div { --color: #00ff00; } @@ -42,7 +52,7 @@ test("color resolution uses specificity when no !important", () => { }); test("color resolution uses source order for equal specificity", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --color: #ff0000; } :root { --color: #0000ff; } @@ -58,7 +68,7 @@ test("color resolution uses source order for equal specificity", () => { }); test("color resolution handles variable references", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --primary: #ff0000; @@ -75,7 +85,7 @@ test("color resolution handles variable references", () => { }); test("color resolution detects circular references", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --a: var(--b); @@ -89,7 +99,7 @@ test("color resolution detects circular references", () => { }); test("color resolution handles multi-level variable chains", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --base: #00ff00; @@ -108,7 +118,7 @@ test("color resolution handles multi-level variable chains", () => { }); test("color resolution combines cascade rules correctly", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --color: #ff0000; } div { --color: #00ff00; } @@ -127,7 +137,7 @@ test("color resolution combines cascade rules correctly", () => { }); test("color resolution works across multiple files", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent( ":root { --primary: #ff0000; }", @@ -151,7 +161,7 @@ test("color resolution works across multiple files", () => { }); test("color resolution with rgba values", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --transparent-red: rgba(255, 0, 0, 0.5); @@ -168,7 +178,7 @@ test("color resolution with rgba values", () => { }); test("color resolution with hsl values", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --hsl-color: hsl(120, 100%, 50%); @@ -185,7 +195,7 @@ test("color resolution with hsl values", () => { }); test("color resolution returns null for non-color values", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --spacing: 10px; @@ -205,7 +215,7 @@ test("color resolution returns null for non-color values", () => { }); test("color resolution with !important overrides higher specificity", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` #high-specificity { --color: #ff0000; } div { --color: #0000ff !important; } diff --git a/tests/colorFormatting.test.ts b/tests/colorFormatting.test.ts index 0aac194..65bd264 100644 --- a/tests/colorFormatting.test.ts +++ b/tests/colorFormatting.test.ts @@ -9,6 +9,16 @@ import { import { CssVariableManager } from "../src/cssVariableManager"; import { TextDocument } from "vscode-languageserver-textdocument"; import { Range } from "vscode-languageserver/node"; +import { Logger } from "../src/logger"; + +class SilentLogger implements Logger { + debug(_label: string, _payload?: unknown): void {} + info(_label: string, _payload?: unknown): void {} + warn(_label: string, _payload?: unknown): void {} + error(_label: string, _payload?: unknown): void {} +} + +const silentLogger = new SilentLogger(); test("color formatting", () => { const red = { red: 1, green: 0, blue: 0, alpha: 1 }; @@ -39,7 +49,7 @@ test("parseColor ignores named colors by default", () => { }); test("value range calculation", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const css = ` :root { --simple: red; diff --git a/tests/colorProvider.test.ts b/tests/colorProvider.test.ts index 42d8d91..0d3099f 100644 --- a/tests/colorProvider.test.ts +++ b/tests/colorProvider.test.ts @@ -2,17 +2,27 @@ import { test } from "node:test"; import { strict as assert } from "node:assert"; import { TextDocument } from "vscode-languageserver-textdocument"; import { CssVariableManager } from "../src/cssVariableManager"; +import { Logger } from "../src/logger"; import { collectColorPresentations, collectDocumentColors, } from "../src/colorProvider"; +class SilentLogger implements Logger { + debug(_label: string, _payload?: unknown) {} + info(_label: string, _payload?: unknown) {} + warn(_label: string, _payload?: unknown) {} + error(_label: string, _payload?: unknown) {} +} + +const silentLogger = new SilentLogger(); + function createDoc(uri: string, content: string, languageId: string = "css") { return TextDocument.create(uri, languageId, 1, content); } test("collectDocumentColors returns empty when disabled", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const uri = "file:///colors.css"; const css = ":root { --primary: #ff0000; }"; const doc = createDoc(uri, css); @@ -27,7 +37,7 @@ test("collectDocumentColors returns empty when disabled", () => { }); test("collectDocumentColors respects onlyVariables flag", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const uri = "file:///colors-usage.css"; const css = ` :root { --primary: #ff0000; } @@ -65,7 +75,7 @@ test("collectColorPresentations honors enabled flag", () => { }); test("collectDocumentColors resolves named color variables", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const uri = "file:///colors-named.css"; const css = ` :root { --primary: red; } diff --git a/tests/colorVariableFeature.test.ts b/tests/colorVariableFeature.test.ts index 3203adb..6ed8a40 100644 --- a/tests/colorVariableFeature.test.ts +++ b/tests/colorVariableFeature.test.ts @@ -3,18 +3,28 @@ import { strict as assert } from "node:assert"; import { TextDocument } from "vscode-languageserver-textdocument"; import { DiagnosticSeverity } from "vscode-languageserver/node"; import { CssVariableManager } from "../src/cssVariableManager"; +import { Logger } from "../src/logger"; import { collectColorReplacementDiagnostics, getColorReplacementCodeActions, getColorReplacementCompletionItems, } from "../src/colorVariableFeature"; +class SilentLogger implements Logger { + debug(_label: string, _payload?: unknown) {} + info(_label: string, _payload?: unknown) {} + warn(_label: string, _payload?: unknown) {} + error(_label: string, _payload?: unknown) {} +} + +const silentLogger = new SilentLogger(); + function createDoc(uri: string, content: string, languageId: string = "css") { return TextDocument.create(uri, languageId, 1, content); } test("variables match by normalized color value", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent( ":root { --white: #fff; --paper: rgb(255 255 255); --accent: #0d6efd; }", "file:///vars.css", @@ -33,7 +43,7 @@ test("variables match by normalized color value", () => { }); test("document color literals are collected from compound values and skip var()", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const doc = createDoc( "file:///test.css", ".card { background: linear-gradient(#fff, rgb(255 255 255), var(--paper)); box-shadow: 0 0 2px white; }" @@ -49,7 +59,7 @@ test("document color literals are collected from compound values and skip var()" }); test("diagnostics are informational and preserve multiple variable options", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent( ":root { --white: #fff; --paper: rgb(255 255 255); }", "file:///vars.css", @@ -58,7 +68,7 @@ test("diagnostics are informational and preserve multiple variable options", () const doc = createDoc("file:///test.css", ".title { color: white; }"); manager.parseDocument(doc); - const diagnostics = collectColorReplacementDiagnostics(doc, manager); + const diagnostics = collectColorReplacementDiagnostics(doc, manager, silentLogger); assert.strictEqual(diagnostics.length, 1); assert.strictEqual(diagnostics[0].severity, DiagnosticSeverity.Information); @@ -66,7 +76,7 @@ test("diagnostics are informational and preserve multiple variable options", () }); test("diagnostics are not shown on variable definitions", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent( ":root { --white: #fff; --paper: rgb(255 255 255); }", "file:///vars.css", @@ -75,19 +85,19 @@ test("diagnostics are not shown on variable definitions", () => { const doc = createDoc("file:///test.css", ":root { --white: #fff; }"); manager.parseDocument(doc); - const diagnostics = collectColorReplacementDiagnostics(doc, manager); + const diagnostics = collectColorReplacementDiagnostics(doc, manager, silentLogger); // No diagnostics on definitions (only on usages) assert.strictEqual(diagnostics.length, 0); }); test("diagnostic message includes variable name when only one match", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --white: #fff; }", "file:///vars.css", "css"); const doc = createDoc("file:///test.css", ".title { color: white; }"); manager.parseDocument(doc); - const diagnostics = collectColorReplacementDiagnostics(doc, manager); + const diagnostics = collectColorReplacementDiagnostics(doc, manager, silentLogger); assert.strictEqual(diagnostics.length, 1); assert.strictEqual(diagnostics[0].severity, DiagnosticSeverity.Information); @@ -95,7 +105,7 @@ test("diagnostic message includes variable name when only one match", () => { }); test("completion items replace the full literal color token", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --white: #fff; }", "file:///vars.css", "css"); const source = ".title { color: white; }"; @@ -106,7 +116,8 @@ test("completion items replace the full literal color token", () => { doc, doc.positionAt(source.indexOf("white") + 2), manager, - { formatLocation: (uri) => uri } + { formatLocation: (uri) => uri }, + silentLogger ); assert.strictEqual(completions.length, 1); @@ -121,7 +132,7 @@ test("completion items replace the full literal color token", () => { }); test("code actions return one quick fix per matching variable", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent( ":root { --white: #fff; --paper: rgb(255 255 255); }", "file:///vars.css", @@ -131,8 +142,8 @@ test("code actions return one quick fix per matching variable", () => { const doc = createDoc("file:///test.css", source); manager.parseDocument(doc); - const diagnostics = collectColorReplacementDiagnostics(doc, manager); - const actions = getColorReplacementCodeActions(doc, diagnostics); + const diagnostics = collectColorReplacementDiagnostics(doc, manager, silentLogger); + const actions = getColorReplacementCodeActions(doc, diagnostics, silentLogger); assert.strictEqual(actions.length, 2); assert.deepEqual( @@ -142,7 +153,7 @@ test("code actions return one quick fix per matching variable", () => { }); test("literal detection works in html and less documents", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const htmlDoc = createDoc( "file:///test.html", '
', diff --git a/tests/contextualCompletion.test.ts b/tests/contextualCompletion.test.ts index 7b23b06..a0583e1 100644 --- a/tests/contextualCompletion.test.ts +++ b/tests/contextualCompletion.test.ts @@ -4,6 +4,16 @@ import { CssVariableManager } from "../src/cssVariableManager"; import { TextDocument } from "vscode-languageserver-textdocument"; import { CompletionItem } from "vscode-languageserver/node"; import { getCssCompletionContext } from "../src/completionContext"; +import { Logger } from "../src/logger"; + +class SilentLogger implements Logger { + debug(_label: string, _payload?: unknown): void {} + info(_label: string, _payload?: unknown): void {} + warn(_label: string, _payload?: unknown): void {} + error(_label: string, _payload?: unknown): void {} +} + +const silentLogger = new SilentLogger(); function createDoc(uri: string, content: string, languageId: string = "css") { return TextDocument.create(uri, languageId, 1, content); @@ -41,7 +51,7 @@ function getCompletionsAt( } test("completion suggests variables inside var()", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --primary: red; --secondary: blue; }", "file:///vars.css", "css"); const completions = getCompletionsAt(manager, ".btn { color: var(--|) }"); @@ -52,7 +62,7 @@ test("completion suggests variables inside var()", () => { }); test("no completion after property colon without var()", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --bg-color: white; }", "file:///vars.css", "css"); const completions = getCompletionsAt(manager, ".box { background: | }"); @@ -61,7 +71,7 @@ test("no completion after property colon without var()", () => { }); test("completion works in multi-value properties", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --spacing: 10px; }", "file:///vars.css", "css"); const completions = getCompletionsAt(manager, ".box { padding: 5px var(--|) }"); @@ -71,7 +81,7 @@ test("completion works in multi-value properties", () => { }); test("no completion in property name position", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --color: red; }", "file:///vars.css", "css"); const completions = getCompletionsAt(manager, ".selector { col| }"); @@ -80,7 +90,7 @@ test("no completion in property name position", () => { }); test("no completion in selector position", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --color: red; }", "file:///vars.css", "css"); const completions = getCompletionsAt(manager, ".my-class| { color: red; }"); @@ -89,7 +99,7 @@ test("no completion in selector position", () => { }); test("completion works in HTML style attribute", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --text-color: black; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -103,7 +113,7 @@ test("completion works in HTML style attribute", () => { }); test("completion works in HTML style block", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --text-color: black; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -117,7 +127,7 @@ test("completion works in HTML style block", () => { }); test("no completion in HTML outside style context", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --color: red; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -130,7 +140,7 @@ test("no completion in HTML outside style context", () => { }); test("completion works in non-CSS language when using var()", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --color: red; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -144,7 +154,7 @@ test("completion works in non-CSS language when using var()", () => { }); test("no completion in non-CSS language without var()", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --color: red; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -157,7 +167,7 @@ test("no completion in non-CSS language without var()", () => { }); test("no completion in selector pseudo-class", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --color: red; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -169,7 +179,7 @@ test("no completion in selector pseudo-class", () => { }); test("completion works after semicolon in declaration block", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --primary: red; --secondary: blue; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -181,7 +191,7 @@ test("completion works after semicolon in declaration block", () => { }); test("completion works in nested var() fallback", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --fallback: gray; }", "file:///vars.css", "css"); const completions = getCompletionsAt(manager, ".box { color: var(--primary, var(--|)) }"); @@ -190,7 +200,7 @@ test("completion works in nested var() fallback", () => { }); test("no completion in var() fallback value", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --fallback: gray; }", "file:///vars.css", "css"); const completions = getCompletionsAt( @@ -202,7 +212,7 @@ test("no completion in var() fallback value", () => { }); test("completion works across multiple lines", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --margin: 20px; }", "file:///vars.css", "css"); const content = `.container { @@ -217,7 +227,7 @@ test("completion works across multiple lines", () => { }); test("completion shows variable values in detail", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); manager.parseContent(":root { --theme-color: #ff5500; }", "file:///vars.css", "css"); const completions = getCompletionsAt(manager, ".btn { color: var(--|) }"); diff --git a/tests/cssVariableManager.test.ts b/tests/cssVariableManager.test.ts index 644d958..2596fc8 100644 --- a/tests/cssVariableManager.test.ts +++ b/tests/cssVariableManager.test.ts @@ -2,13 +2,23 @@ import { test } from "node:test"; import { strict as assert } from "node:assert"; import { CssVariableManager } from "../src/cssVariableManager"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { Logger } from "../src/logger"; + +class SilentLogger implements Logger { + debug(_label: string, _payload?: unknown): void {} + info(_label: string, _payload?: unknown): void {} + warn(_label: string, _payload?: unknown): void {} + error(_label: string, _payload?: unknown): void {} +} + +const silentLogger = new SilentLogger(); function createDoc(uri: string, content: string, languageId: string = "css") { return TextDocument.create(uri, languageId, 1, content); } test("basic CSS extraction", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const doc = createDoc("file:///test.css", ":root { --main-color: red; }"); manager.parseDocument(doc); const vars = manager.getAllVariables(); @@ -18,7 +28,7 @@ test("basic CSS extraction", () => { }); test("HTML style extraction", () => { - const manager = new CssVariableManager(); + const manager = new CssVariableManager(silentLogger); const htmlContent = ` +`; + const document = TextDocument.create("file:///test.html", "html", 1, html); + manager.parseDocument(document); + + const simple = manager.getVariables("--simple")[0]; + const spaced = manager.getVariables("--spaced")[0]; + const complex = manager.getVariables("--complex")[0]; + + const getText = (range: Range) => { + const start = document.offsetAt(range.start); + const end = document.offsetAt(range.end); + return html.substring(start, end); + }; + + if (!simple.valueRange) { + throw new Error("Expected valueRange for --simple"); + } + assert.strictEqual(getText(simple.valueRange).trim(), "red"); + + if (!spaced.valueRange) { + throw new Error("Expected valueRange for --spaced"); + } + assert.strictEqual(getText(spaced.valueRange).trim(), "blue"); + + if (!complex.valueRange) { + throw new Error("Expected valueRange for --complex"); + } + assert.strictEqual(getText(complex.valueRange).trim(), "rgb(0,0,0)"); +}); + From 50a7c15af27fb4bbfc9c9856920ce1ecb468c8bb Mon Sep 17 00:00:00 2001 From: lmn451 Date: Sun, 22 Mar 2026 16:54:49 +0200 Subject: [PATCH 23/25] Support explicit stdio transport for CLI usage Use --stdio flag to force stdio transport. Without the flag, the library auto-detects transport from argv (--node-ipc for VS Code extension, --socket, --pipe for other clients). This keeps both CLI and VS Code extension working correctly. --- src/server.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/server.ts b/src/server.ts index 2fd717f..88a2ef0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -47,9 +47,16 @@ import { const runtimeConfig = buildRuntimeConfig(process.argv.slice(2), process.env); -// Create a connection for the server, using Node's IPC as a transport. +// Create a connection for the server. +// Use stdio transport when --stdio flag is passed (CLI usage). +// Otherwise, let the library auto-detect transport from argv: +// --node-ipc for IPC (VS Code extension), +// --socket or --pipe for other transports. // Also include all preview / proposed LSP features. -const connection = createConnection(ProposedFeatures.all); +const useStdio = process.argv.includes('--stdio'); +const connection = useStdio + ? createConnection(ProposedFeatures.all, process.stdin, process.stdout) + : createConnection(ProposedFeatures.all); const logger = createLogger(); @@ -294,7 +301,11 @@ async function validateTextDocument(textDocument: TextDocument): Promise { if (runtimeConfig.enableColorReplacementDiagnostics) { diagnostics.push( - ...collectColorReplacementDiagnostics(textDocument, cssVariableManager, logger), + ...collectColorReplacementDiagnostics( + textDocument, + cssVariableManager, + logger, + ), ); } @@ -821,7 +832,11 @@ connection.onCodeAction((params): CodeAction[] => { return []; } - return getColorReplacementCodeActions(document, params.context.diagnostics, logger); + return getColorReplacementCodeActions( + document, + params.context.diagnostics, + logger, + ); }); // Document symbols handler From 5277576d74d50cbc93c4a45ca170e2d1a6460244 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Mon, 23 Mar 2026 10:36:42 +0200 Subject: [PATCH 24/25] fix: remove dead code and clean up minor issues - Remove unused htmlText field from DOMTree class - Rename unused document param to _document in createColorReplacementCompletionItem - Remove dead parseInt function from flags.ts (never called) - Add eslint-disable comments for deprecated rootUri/rootPath usage - Remove deprecated fields from debug logging in onInitialize - Export isPositionOnDefinition helper and add tests --- src/colorVariableFeature.ts | 16 +++++++++++- src/domTree.ts | 2 -- src/flags.ts | 8 ------ src/server.ts | 14 +++++++--- tests/colorVariableFeature.test.ts | 41 ++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/colorVariableFeature.ts b/src/colorVariableFeature.ts index 997cc16..a7cc574 100644 --- a/src/colorVariableFeature.ts +++ b/src/colorVariableFeature.ts @@ -196,7 +196,7 @@ function getMatchingVariables( } function createColorReplacementCompletionItem( - document: TextDocument, + _document: TextDocument, range: Range, match: CssVariable, displayOptions: CompletionDisplayOptions @@ -211,3 +211,17 @@ function createColorReplacementCompletionItem( sortText: match.name, }; } + +// Check if cursor position is on a CSS variable definition +export function isPositionOnDefinition( + document: TextDocument, + definitions: CssVariable[], + position: Position, +): boolean { + const cursorOffset = document.offsetAt(position); + return definitions.some((def) => { + const start = document.offsetAt(def.range.start); + const end = document.offsetAt(def.range.end); + return cursorOffset >= start && cursorOffset <= end; + }); +} diff --git a/src/domTree.ts b/src/domTree.ts index c9e8f7f..b67aa0b 100644 --- a/src/domTree.ts +++ b/src/domTree.ts @@ -17,10 +17,8 @@ export interface DOMNodeInfo { export class DOMTree { private root: ParsedHTMLElement; - private htmlText: string; constructor(htmlText: string) { - this.htmlText = htmlText; this.root = parse(htmlText, { lowerCaseTagName: true, comment: false, diff --git a/src/flags.ts b/src/flags.ts index bb2df9f..14c2d86 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -217,14 +217,6 @@ function parsePathDisplay(flag: PathDisplayFlagDef, argv: string[], env: NodeJS. return { mode, combinedLength }; } -function parseInt(flag: IntFlagDef, argv: string[], env: NodeJS.ProcessEnv): number { - const argValue = getArgValue(argv, flag.flag.replace("--", "")); - const envValue = env[flag.envKey]; - const raw = argValue ?? envValue ?? null; - const parsed = parseOptionalInt(raw); - return parsed ?? flag.default; -} - function parseList(flag: ListFlagDef, argv: string[], env: NodeJS.ProcessEnv): string[] | undefined { const cliValues: string[] = []; for (let i = 0; i < argv.length; i++) { diff --git a/src/server.ts b/src/server.ts index 88a2ef0..6339eb6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -33,6 +33,7 @@ import { collectColorReplacementDiagnostics, getColorReplacementCodeActions, getColorReplacementCompletionItems, + isPositionOnDefinition, } from "./colorVariableFeature"; import { buildInitializeResult } from "./initialize"; import { formatUriForDisplay, toNormalizedFsPath } from "./pathDisplay"; @@ -88,9 +89,6 @@ let rootFolderPath: string | null = null; connection.onInitialize((params: InitializeParams) => { logger.debug("initialize", { - rootUri: params.rootUri, - // rootPath is deprecated and optional in InitializeParams - rootPath: params.rootPath, workspaceFolders: params.workspaceFolders, capabilities: params.capabilities, }); @@ -105,13 +103,17 @@ connection.onInitialize((params: InitializeParams) => { capabilities.textDocument.publishDiagnostics && capabilities.textDocument.publishDiagnostics.relatedInformation ); + // eslint-disable-next-line @typescript-eslint/no-deprecated if (params.rootUri) { try { + // eslint-disable-next-line @typescript-eslint/no-deprecated rootFolderPath = path.normalize(URI.parse(params.rootUri).fsPath); } catch { rootFolderPath = null; } + // eslint-disable-next-line @typescript-eslint/no-deprecated } else if (params.rootPath) { + // eslint-disable-next-line @typescript-eslint/no-deprecated rootFolderPath = path.normalize(params.rootPath); } updateWorkspaceFolderPaths(params.workspaceFolders || undefined); @@ -832,6 +834,12 @@ connection.onCodeAction((params): CodeAction[] => { return []; } + // Skip replacement suggestions when cursor is on a CSS variable definition + const definitions = cssVariableManager.getDocumentDefinitions(document.uri); + if (isPositionOnDefinition(document, definitions, params.range.start)) { + return []; + } + return getColorReplacementCodeActions( document, params.context.diagnostics, diff --git a/tests/colorVariableFeature.test.ts b/tests/colorVariableFeature.test.ts index 02846b9..3d7f4b4 100644 --- a/tests/colorVariableFeature.test.ts +++ b/tests/colorVariableFeature.test.ts @@ -7,6 +7,7 @@ import { collectColorReplacementDiagnostics, getColorReplacementCodeActions, getColorReplacementCompletionItems, + isPositionOnDefinition, } from "../src/colorVariableFeature"; import { silentLogger } from "./helpers/silentLogger"; @@ -162,3 +163,43 @@ test("literal detection works in html and less documents", () => { assert.ok(manager.getDocumentColorLiterals(htmlDoc.uri).length >= 2); assert.strictEqual(manager.getDocumentColorLiterals(lessDoc.uri).length, 2); }); + +test("isPositionOnDefinition returns true when cursor is on definition", () => { + const manager = new CssVariableManager(silentLogger); + manager.parseContent(":root { --white: #fff; }", "file:///vars.css", "css"); + + const doc = createDoc("file:///test.css", ":root { --white: #fff; }"); + manager.parseDocument(doc); + + const definitions = manager.getDocumentDefinitions(doc.uri); + + // Position at start of --white (after ":root { ") + const pos = doc.positionAt(8); + assert.strictEqual(isPositionOnDefinition(doc, definitions, pos), true); +}); + +test("isPositionOnDefinition returns false when cursor is not on definition", () => { + const manager = new CssVariableManager(silentLogger); + manager.parseContent(":root { --white: #fff; }", "file:///vars.css", "css"); + + const doc = createDoc("file:///test.css", ".title { color: white; }"); + manager.parseDocument(doc); + + const definitions = manager.getDocumentDefinitions(doc.uri); + + // Position at "white" in color: white + const pos = doc.positionAt(16); + assert.strictEqual(isPositionOnDefinition(doc, definitions, pos), false); +}); + +test("isPositionOnDefinition returns false for empty definitions", () => { + const manager = new CssVariableManager(silentLogger); + const doc = createDoc("file:///test.css", ".title { color: white; }"); + manager.parseDocument(doc); + + const definitions = manager.getDocumentDefinitions(doc.uri); + assert.strictEqual(definitions.length, 0); + + const pos = doc.positionAt(5); + assert.strictEqual(isPositionOnDefinition(doc, definitions, pos), false); +}); From 66bf2103256bf2bc6e9d8f40cede88d09c80f9f5 Mon Sep 17 00:00:00 2001 From: lmn451 Date: Mon, 23 Mar 2026 10:51:55 +0200 Subject: [PATCH 25/25] docs: add AGENTS.md project knowledge base --- AGENTS.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index e69de29..cc7e0e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -0,0 +1,87 @@ +# CSS Variable LSP - Project Knowledge + +**Generated:** 2026-03-23 +**Commit:** 5277576 +**Branch:** chore/update-deps + +## Overview + +CSS Language Server for CSS custom properties. Indexes variables across workspace, provides completions, hover, diagnostics, go-to-definition, rename, and color support. + +**Stack:** TypeScript + vscode-languageserver + css-tree + node-html-parser + +## Structure + +``` +css-lsp/ +├── src/ # 13 source files +│ ├── server.ts # LSP entry + all handlers +│ ├── cssVariableManager.ts # Core: variable index, cascade resolution +│ ├── colorProvider.ts # Color decorations/picker +│ ├── colorService.ts # Color parsing/formatting +│ ├── colorVariableFeature.ts # Color replacement suggestions +│ ├── completionContext.ts # Property-aware completions +│ ├── specificity.ts # CSS cascade ordering +│ ├── domTree.ts # HTML DOM for selector matching +│ ├── flags.ts # CLI flag definitions +│ ├── initialize.ts # LSP capability builder +│ ├── logger.ts # Structured logging +│ ├── pathDisplay.ts # Path formatting (relative/absolute) +│ └── runtimeConfig.ts # Runtime config builder +├── tests/ # 27 test files (*.test.ts) +├── out/ # Compiled output +├── docs/ # Example files +└── package.json # bin: css-variable-lsp +``` + +## Where to Look + +| Task | Location | Notes | +|------|----------|-------| +| LSP protocol handlers | `src/server.ts` | 900+ lines, all onXxx handlers | +| Variable parsing/indexing | `src/cssVariableManager.ts` | Core state manager | +| Color features | `src/colorService.ts`, `colorProvider.ts`, `colorVariableFeature.ts` | 3-file domain | +| Cascade/hover logic | `src/server.ts:611-635` | Sort by !important > specificity > source order | +| CLI/config | `src/flags.ts` | Declarative flag registry | +| Tests | `tests/*.test.ts` | Node.js native test runner | + +## CODE MAP + +| Symbol | Type | Location | Role | +|--------|------|----------|------| +| `connection` | const | server.ts:58 | LSP connection instance | +| `CssVariableManager` | class | cssVariableManager.ts:122 | Central state + parsing | +| `onCompletion` | handler | server.ts:489 | Var completions + color suggestions | +| `onHover` | handler | server.ts:564 | Cascade-aware hover | +| `resolveVariableColor` | method | cssVariableManager.ts:975 | Recursive color resolution | +| `calculateSpecificity` | function | specificity.ts:25 | CSS cascade ordering | +| `parseColor` | function | colorService.ts:11 | Hex/rgb/hsl/named parsing | + +## Conventions + +- **Strict TypeScript** — `strict: true` in tsconfig +- **Named exports only** — No default exports +- **Structured logging** — Logger interface with `debug/info/warn/error` +- **Error handling** — Log via `logger.error()`, never throw in production +- **Tests** — Node.js native `node:test`, `strict as assert` from `node:assert` + +## Anti-Patterns (This Project) + +- **No forbidden comments** — Project relies on TypeScript strict mode +- **No ESLint** — Empty `.prettierrc` uses defaults only +- **Flat src/ structure** — All 13 files at root level + +## Commands + +```bash +npm run compile # tsc -b → out/ +npm test # All 27 test files +npm run perf # Performance tests (CSS_LSP_PERF=1) +``` + +## Notes + +- `server.ts` is monolithic (900+ lines) — contains ALL LSP handlers +- Color index (`colorIndexDirty` flag) provides O(1) color lookups +- Cascade resolution: !important > inline > specificity > source order +- `cssVariableManager.ts` is the brain — imported by all feature modules