diff --git a/docs/_includes/generated-docs/commands.md b/docs/_includes/generated-docs/commands.md index 95fca5deb9..18603692e6 100644 --- a/docs/_includes/generated-docs/commands.md +++ b/docs/_includes/generated-docs/commands.md @@ -46,3 +46,4 @@ | `cSpell.toggleEnableForGlobal` | Toggle Spell Checking in User Settings | | `cSpell.toggleEnableForWorkspace` | Toggle Spell Checking for Workspace | | `cSpell.toggleEnableSpellChecker` | Toggle Spell Checking | +| `cSpell.toggleTraceMode` | Toggle Trace Mode | diff --git a/package-lock.json b/package-lock.json index 06406459ac..5ced86a886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,10 +28,10 @@ "packages/_integrationTests" ], "dependencies": { - "@cspell/cspell-bundled-dicts": "^8.2.3", - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-bundled-dicts": "^8.2.4", + "@cspell/cspell-types": "^8.2.4", "@types/react": "^17.0.73", - "cspell": "^8.2.3", + "cspell": "^8.2.4", "regexp-worker": "^2.0.2" }, "devDependencies": { @@ -727,14 +727,14 @@ "license": "MIT" }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.2.3.tgz", - "integrity": "sha512-AmKr/laSnmuTlECsIkf71N8FPd/ualJx13OdIJNIvUjIE741x/EACITIWLnTK9qFbsefOYp7bUeo9Xtbdw5JSA==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.2.4.tgz", + "integrity": "sha512-HASk7BBR9p2Q1+2mD/WHwOEORkECfwYNbCVzFMJYzw37fTMDClZABypGe1Y5AqFcY9JKlR9YvwlYVzg09vllXg==", "dependencies": { "@cspell/dict-ada": "^4.0.2", "@cspell/dict-aws": "^4.0.1", "@cspell/dict-bash": "^4.1.3", - "@cspell/dict-companies": "^3.0.28", + "@cspell/dict-companies": "^3.0.29", "@cspell/dict-cpp": "^5.0.10", "@cspell/dict-cryptocurrencies": "^4.0.0", "@cspell/dict-csharp": "^4.0.2", @@ -752,7 +752,7 @@ "@cspell/dict-fsharp": "^1.0.1", "@cspell/dict-fullstack": "^3.1.5", "@cspell/dict-gaming-terms": "^1.0.4", - "@cspell/dict-git": "^2.0.0", + "@cspell/dict-git": "^3.0.0", "@cspell/dict-golang": "^6.0.5", "@cspell/dict-haskell": "^4.0.1", "@cspell/dict-html": "^4.0.5", @@ -765,15 +765,15 @@ "@cspell/dict-makefile": "^1.0.0", "@cspell/dict-node": "^4.0.3", "@cspell/dict-npm": "^5.0.14", - "@cspell/dict-php": "^4.0.4", + "@cspell/dict-php": "^4.0.5", "@cspell/dict-powershell": "^5.0.3", "@cspell/dict-public-licenses": "^2.0.5", - "@cspell/dict-python": "^4.1.10", + "@cspell/dict-python": "^4.1.11", "@cspell/dict-r": "^2.0.1", "@cspell/dict-ruby": "^5.0.2", "@cspell/dict-rust": "^4.0.1", "@cspell/dict-scala": "^5.0.0", - "@cspell/dict-software-terms": "^3.3.14", + "@cspell/dict-software-terms": "^3.3.15", "@cspell/dict-sql": "^2.1.3", "@cspell/dict-svelte": "^1.0.2", "@cspell/dict-swift": "^2.0.1", @@ -785,28 +785,28 @@ } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.2.3.tgz", - "integrity": "sha512-603qzkEQZueKauvzCeAMKZqcTBEEJEfs3yBsDKx1jYqyMPuTXnh3vmxkPy0paiJuE625BjzlCuvok225u6x9Qw==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.2.4.tgz", + "integrity": "sha512-aFOoIQqlmIzO+/7sQSVIK23gKUHQl2HNRBpIr+8+BKkKvLXxpQfJoXMc9YzkL3+Tfhd4yN4WK1OLtmMMAKJY8w==", "dependencies": { - "@cspell/cspell-types": "8.2.3" + "@cspell/cspell-types": "8.2.4" }, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-pipe": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.2.3.tgz", - "integrity": "sha512-ga39z+K2ZaSQczaRayNUTrz10z7umEdFiK7AdWOQpGmym5JTtTK0ntnKvKKsdSJ9F5I7TZVxgZH6r4CCEPlEEg==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.2.4.tgz", + "integrity": "sha512-s3LcmpqFHE0L4UHbFe+i9WknInBeWkTcMrwuI8Cf6AioOHfOZS8ch1SmwPIPQ703uuoER100AEyNoex+92G/6g==", "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-resolver": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.2.3.tgz", - "integrity": "sha512-H0855Lg0DxWDcT0FtJyqLvUqOJuE1qSg9X3ENs/ltZntQeaU8wZc+B34bXJrGpJVMuiiqHp4w6rcNN3lsOcshQ==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.2.4.tgz", + "integrity": "sha512-0CI180FKFiSuZElbA45XUtBqTXcQ9wrKwl4x9SraTGZkI7Z1EaZR/saCGwE5JEcHvfQOQK8C1dB40d5XlB0RlQ==", "dependencies": { "global-directory": "^4.0.1" }, @@ -815,17 +815,17 @@ } }, "node_modules/@cspell/cspell-service-bus": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.2.3.tgz", - "integrity": "sha512-hMLEzE2MkFir3kii046RecR1JAAfA6RQhLddjwQTq1c8YCWJ4lQEKUdM5x7nr/UpJtsMj8eYZ7CtbbnxQyn7Zg==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.2.4.tgz", + "integrity": "sha512-rdpFR88m4Pes49ADO6YAUU3xKkRDjSzZdKAgsLgXTMWzC3xbN8IFBGXd7ChPgBf6TTHjQsYggt29NVJQIAh21g==", "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-types": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.2.3.tgz", - "integrity": "sha512-AZIC1n7veQSylp9ZAcVDvIaY+oS/vpzFNJ77rzuhEy/B6X/9jzeI8wg/+vWkmhO59q4iF/ZlswWK3UXfeSnUFg==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.2.4.tgz", + "integrity": "sha512-dq/T10oJx7XdLQkfzZOuLCGJwd3Apzf7O+KlFnxZd/BZ0gCuCynxOZ7GTK23d07EObJg986yhWTjXWgfmLMEVw==", "engines": { "node": ">=18" } @@ -845,9 +845,9 @@ "integrity": "sha512-tOdI3QVJDbQSwPjUkOiQFhYcu2eedmX/PtEpVWg0aFps/r6AyjUQINtTgpqMYnYuq8O1QUIQqnpx21aovcgZCw==" }, "node_modules/@cspell/dict-companies": { - "version": "3.0.28", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.0.28.tgz", - "integrity": "sha512-UinHkMYB/1pUkLKm1PGIm9PBFYxeAa6YvbB1Rq/RAAlrs0WDwiDBr3BAYdxydukG1IqqwT5z9WtU+8D/yV/5lw==" + "version": "3.0.29", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.0.29.tgz", + "integrity": "sha512-F/8XnkqjU7jmSDAcD3LSSX+WxCVUWPssqlO4lzGMIK3MNIUt+d48eSIt3pFAIB/Z9y0ojoLHUtWX9HJ1ZtGrXQ==" }, "node_modules/@cspell/dict-cpp": { "version": "5.0.10", @@ -928,8 +928,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-git": { - "version": "2.0.0", - "license": "MIT" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-git/-/dict-git-3.0.0.tgz", + "integrity": "sha512-simGS/lIiXbEaqJu9E2VPoYW1OTC2xrwPPXNXFMa2uo/50av56qOuaxDrZ5eH1LidFXwoc8HROCHYeKoNrDLSw==" }, "node_modules/@cspell/dict-golang": { "version": "6.0.5", @@ -987,9 +988,9 @@ "integrity": "sha512-k0kC7/W2qG5YII+SW6s+JtvKrkZg651vizi5dv/5G2HmJaeLNgDqBVeeDk/uV+ntBorM66XG4BPMjSxoaIlC5w==" }, "node_modules/@cspell/dict-php": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-php/-/dict-php-4.0.4.tgz", - "integrity": "sha512-fRlLV730fJbulDsLIouZxXoxHt3KIH6hcLFwxaupHL+iTXDg0lo7neRpbqD5MScr/J3idEr7i9G8XWzIikKFug==" + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-php/-/dict-php-4.0.5.tgz", + "integrity": "sha512-9r8ao7Z/mH9Z8pSB7yLtyvcCJWw+/MnQpj7xGVYzIV7V2ZWDRjXZAMgteHMJ37m8oYz64q5d4tiipD300QSetQ==" }, "node_modules/@cspell/dict-powershell": { "version": "5.0.3", @@ -1002,9 +1003,9 @@ "integrity": "sha512-91HK4dSRri/HqzAypHgduRMarJAleOX5NugoI8SjDLPzWYkwZ1ftuCXSk+fy8DLc3wK7iOaFcZAvbjmnLhVs4A==" }, "node_modules/@cspell/dict-python": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.1.10.tgz", - "integrity": "sha512-ErF/Ohcu6Xk4QVNzFgo8p7CxkxvAKAmFszvso41qOOhu8CVpB35ikBRpGVDw9gsCUtZzi15Yl0izi4do6WcLkA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.1.11.tgz", + "integrity": "sha512-XG+v3PumfzUW38huSbfT15Vqt3ihNb462ulfXifpQllPok5OWynhszCLCRQjQReV+dgz784ST4ggRxW452/kVg==", "dependencies": { "@cspell/dict-data-science": "^1.0.11" } @@ -1027,9 +1028,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-software-terms": { - "version": "3.3.14", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-3.3.14.tgz", - "integrity": "sha512-xLUtqrrvMgMEOn8grsl7p2vwf3qh4+0rZLIU0kpKSkMA8tKK3JJxJFfDyoQpZeoUGdpofhrishx3ZfIdpN/RSg==" + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-3.3.15.tgz", + "integrity": "sha512-1qqMGFi1TUNq9gQj4FTLPTlqVzQLXrj80MsKoXVpysr+823kMWesQAjqHiPg+MYsQ3DlTcpGWcjq/EbYonqueQ==" }, "node_modules/@cspell/dict-sql": { "version": "2.1.3", @@ -1054,9 +1055,9 @@ "license": "MIT" }, "node_modules/@cspell/dynamic-import": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.2.3.tgz", - "integrity": "sha512-udJF+88F4UMH2eVKe3Utsh4X1PyNwqPJclIeD3/MDMFWm16lLkFYMqqrdr51tNLKVi4cXceGrUEapmGwf87l/w==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.2.4.tgz", + "integrity": "sha512-j8ZedGPPDrWBDdJd4IQuDF6XDqVhGpXjQtiERWm2IhnoDl2XcgwkrU0ojqwwEEyRjbTBsH6RyNP8FO9wMmDaxQ==", "dependencies": { "import-meta-resolve": "^4.0.0" }, @@ -1065,9 +1066,9 @@ } }, "node_modules/@cspell/strong-weak-map": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.2.3.tgz", - "integrity": "sha512-/0gQZw87MqGX8f28E+LhFfrsWdRdQEL8EEQeMXrrzSoPnfSz+ItHMhhrwPF+bMePPjaaUNYoRXvX7hxiDsGm0w==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.2.4.tgz", + "integrity": "sha512-o0kEAG+EyMGMsfEX/fzAeQMP/7sX3XJ5suiRcgAknaHNAohK63ZtLYLOkX2lNOI0S2b9INrQ2J0+H513rN0EQQ==", "engines": { "node": ">=18" } @@ -5932,21 +5933,21 @@ } }, "node_modules/cspell": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.2.3.tgz", - "integrity": "sha512-lJEIglmBINLW4Jwn+5W1k6Zb5EjyRFLnTvc1uQ268/9pcsB+GWUZruplIe5+erR3AxZ+N7Tqp7IY9j2Jf1+/Fg==", - "dependencies": { - "@cspell/cspell-json-reporter": "8.2.3", - "@cspell/cspell-pipe": "8.2.3", - "@cspell/cspell-types": "8.2.3", - "@cspell/dynamic-import": "8.2.3", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.2.4.tgz", + "integrity": "sha512-dmhyGelq7P5Mnu8hrx+Zqbpag9Afz8dDEK1kcG6bwcu9ROSTL4/l23P9M/L4Zh4EDDsX9pdO03z864jpHtUwfw==", + "dependencies": { + "@cspell/cspell-json-reporter": "8.2.4", + "@cspell/cspell-pipe": "8.2.4", + "@cspell/cspell-types": "8.2.4", + "@cspell/dynamic-import": "8.2.4", "chalk": "^5.3.0", "chalk-template": "^1.1.0", "commander": "^11.1.0", - "cspell-gitignore": "8.2.3", - "cspell-glob": "8.2.3", - "cspell-io": "8.2.3", - "cspell-lib": "8.2.3", + "cspell-gitignore": "8.2.4", + "cspell-glob": "8.2.4", + "cspell-io": "8.2.4", + "cspell-lib": "8.2.4", "fast-glob": "^3.3.2", "fast-json-stable-stringify": "^2.1.0", "file-entry-cache": "^8.0.0", @@ -5967,11 +5968,11 @@ } }, "node_modules/cspell-config-lib": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.2.3.tgz", - "integrity": "sha512-ATbOR06GKBIFM5SPKMF4fgo5G2qmOfdV8TbpyzNtw1AGL7PoOgDNFiKSutEzO5EHyZuXE71ZFxH3rVr2gIV7Dw==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.2.4.tgz", + "integrity": "sha512-+w0edfI5yI+f7t6a7AI0gY6ZdLKfqnZbvqN2ZpruJ+eXODVK3eS4KIqkJ6lBv+O969B/WTqJedi1UkeYqkHIIQ==", "dependencies": { - "@cspell/cspell-types": "8.2.3", + "@cspell/cspell-types": "8.2.4", "comment-json": "^4.2.3", "yaml": "^2.3.4" }, @@ -5980,13 +5981,13 @@ } }, "node_modules/cspell-dictionary": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.2.3.tgz", - "integrity": "sha512-M/idc3TLjYMpT4+8PlIg7kzoeGkR7o6h6pTwRfy/ZkBkEaV+U/35ZtVLO4qjxnuX6wrmawYmHhYqgzyKLEJIhw==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.2.4.tgz", + "integrity": "sha512-ymcwea6c6VXFGL9DihGiSYP25QCnyDCrpB9gtOpTRubqsRmWDM80N4DxyhlZmMgBtSFW/ODChcDydMEK4zZLug==", "dependencies": { - "@cspell/cspell-pipe": "8.2.3", - "@cspell/cspell-types": "8.2.3", - "cspell-trie-lib": "8.2.3", + "@cspell/cspell-pipe": "8.2.4", + "@cspell/cspell-types": "8.2.4", + "cspell-trie-lib": "8.2.4", "fast-equals": "^5.0.1", "gensequence": "^6.0.0" }, @@ -5995,11 +5996,11 @@ } }, "node_modules/cspell-gitignore": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.2.3.tgz", - "integrity": "sha512-tPUI+Aoq1b1shD04CLprrS8wEriiF4G1J+qBiCZK2KWOh6IcufuuDhP1Jtkzz9uONgGWFPF6jj/9TXRFlQexbQ==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.2.4.tgz", + "integrity": "sha512-ywe9q/OWA/GlmKDsAwmK38lISb5noMCQyIEtYZNuHUIr+gi1gdJXDMeDP5t+ua5WUmpCkjGtzBZJj97l94g60Q==", "dependencies": { - "cspell-glob": "8.2.3", + "cspell-glob": "8.2.4", "find-up-simple": "^1.0.0" }, "bin": { @@ -6010,9 +6011,9 @@ } }, "node_modules/cspell-glob": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.2.3.tgz", - "integrity": "sha512-byP2kBblO5d9rZr73MPor+KfoFdry4uu/MQmwLiK5mxgmokZYv5GVDX2DrO16Ni4yJ6/2rBPWLfq+DfCXSWqyw==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.2.4.tgz", + "integrity": "sha512-MamScae3CdWYfdYwrE99pvXp43+ieWp1+HwPdUBbyboO65/ID0lCear3lXdmArgAjnosNcCRZVl0b4xxfFLn1A==", "dependencies": { "micromatch": "^4.0.5" }, @@ -6021,12 +6022,12 @@ } }, "node_modules/cspell-grammar": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.2.3.tgz", - "integrity": "sha512-z57Qyu24BsHHp/nZ9ftN377cSCgSJg+6oywIglau7ws7vRpUgYKVoKxn+ZJfOrIZpXfZUqgph5IwAGFI+aRN6w==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.2.4.tgz", + "integrity": "sha512-FATCZqQz0++d1mydyMjbqT9xXar8xsSSVLnDBaQRRVQ8jWa+vAv4rFu5d5Emk+XfhkqKBpaVIQlUKDB57CogCw==", "dependencies": { - "@cspell/cspell-pipe": "8.2.3", - "@cspell/cspell-types": "8.2.3" + "@cspell/cspell-pipe": "8.2.4", + "@cspell/cspell-types": "8.2.4" }, "bin": { "cspell-grammar": "bin.mjs" @@ -6036,36 +6037,36 @@ } }, "node_modules/cspell-io": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.2.3.tgz", - "integrity": "sha512-mPbLXiIje9chncy/Xb9C6AxqjJm9AFHz/nmIIP5bc6gd4w/yaGlQNyO8jjHF1u2JBVbIxPQSMjFgEuqasPy4Sg==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.2.4.tgz", + "integrity": "sha512-OxqZiLCP12hApFH5FhpNTzOgFUcVkODzKrgGA2HjWvMkLkI3ov4MaS7VPl9BgfP1ptgPmLT2D0AUroQYeSH7Jg==", "dependencies": { - "@cspell/cspell-service-bus": "8.2.3" + "@cspell/cspell-service-bus": "8.2.4" }, "engines": { "node": ">=18" } }, "node_modules/cspell-lib": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.2.3.tgz", - "integrity": "sha512-NA4FsGomGPNp15TWbXx13bfknLGU8B66j0QlU3i4oDrWBj/t5m7O1nmiQqcaDSKd9s5HtdTHfxLc83hdzmmizg==", - "dependencies": { - "@cspell/cspell-bundled-dicts": "8.2.3", - "@cspell/cspell-pipe": "8.2.3", - "@cspell/cspell-resolver": "8.2.3", - "@cspell/cspell-types": "8.2.3", - "@cspell/dynamic-import": "8.2.3", - "@cspell/strong-weak-map": "8.2.3", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.2.4.tgz", + "integrity": "sha512-6E3uYTfGm0Zp4D3tyvAPIEU8jaX0afqXINkgLdMxB1RXXoRUdU8oMc97mtZ6xLmsvJOExsqad9oG9OT76tbPBA==", + "dependencies": { + "@cspell/cspell-bundled-dicts": "8.2.4", + "@cspell/cspell-pipe": "8.2.4", + "@cspell/cspell-resolver": "8.2.4", + "@cspell/cspell-types": "8.2.4", + "@cspell/dynamic-import": "8.2.4", + "@cspell/strong-weak-map": "8.2.4", "clear-module": "^4.1.2", "comment-json": "^4.2.3", "configstore": "^6.0.0", - "cspell-config-lib": "8.2.3", - "cspell-dictionary": "8.2.3", - "cspell-glob": "8.2.3", - "cspell-grammar": "8.2.3", - "cspell-io": "8.2.3", - "cspell-trie-lib": "8.2.3", + "cspell-config-lib": "8.2.4", + "cspell-dictionary": "8.2.4", + "cspell-glob": "8.2.4", + "cspell-grammar": "8.2.4", + "cspell-io": "8.2.4", + "cspell-trie-lib": "8.2.4", "fast-equals": "^5.0.1", "gensequence": "^6.0.0", "import-fresh": "^3.3.0", @@ -6078,12 +6079,12 @@ } }, "node_modules/cspell-trie-lib": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.2.3.tgz", - "integrity": "sha512-yN2PwceN9ViCjXUhhi3MTWfi15Rpc9CsSFFPV3A6cOWoB0qBnuTXk8hBSx+427UGYjtlXPP6EZKY8w8OK6PweA==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.2.4.tgz", + "integrity": "sha512-SYSe518EgOpNjVcKj3MMKO9RnM9mgRufAZkt8qA7PXAXkCeWieLJY22mDEvjPS4C9f0kkfs8UZ5szuLnxHH66w==", "dependencies": { - "@cspell/cspell-pipe": "8.2.3", - "@cspell/cspell-types": "8.2.3", + "@cspell/cspell-pipe": "8.2.4", + "@cspell/cspell-types": "8.2.4", "gensequence": "^6.0.0" }, "engines": { @@ -21876,8 +21877,8 @@ "name": "@internal/cspell-helper", "version": "1.0.0", "dependencies": { - "@cspell/cspell-types": "^8.2.3", - "cspell-lib": "^8.2.3" + "@cspell/cspell-types": "^8.2.4", + "cspell-lib": "^8.2.4" }, "bin": { "build": "build.mjs" @@ -21928,7 +21929,7 @@ "mocha": "^10.2.0" }, "devDependencies": { - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-types": "^8.2.4", "@types/chai": "^4.3.11", "@types/decompress": "^4.2.7", "@types/glob": "^8.1.0", @@ -22313,14 +22314,14 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "@cspell/cspell-bundled-dicts": "^8.2.3", - "@cspell/cspell-pipe": "^8.2.3", - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-bundled-dicts": "^8.2.4", + "@cspell/cspell-pipe": "^8.2.4", + "@cspell/cspell-types": "^8.2.4", "@internal/common-utils": "file:../__utils", - "cspell-config-lib": "^8.2.3", - "cspell-gitignore": "^8.2.3", - "cspell-glob": "^8.2.3", - "cspell-lib": "^8.2.3", + "cspell-config-lib": "^8.2.4", + "cspell-gitignore": "^8.2.4", + "cspell-glob": "^8.2.4", + "cspell-lib": "^8.2.4", "gensequence": "^6.0.0", "json-rpc-api": "file:../json-rpc-api", "rxjs": "^7.8.1", @@ -22352,9 +22353,9 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-types": "^8.2.4", "@internal/common-utils": "file:../__utils", - "cspell-lib": "^8.2.3", + "cspell-lib": "^8.2.4", "regexp-worker": "^2.0.2", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", @@ -22425,7 +22426,7 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-types": "^8.2.4", "@internal/common-utils": "file:../__utils", "@internal/settings-webview": "file:../_settingsViewer", "code-spell-checker-server": "file:../_server", @@ -22450,7 +22451,7 @@ "@types/kefir": "^3.8.11", "@types/source-map-support": "^0.5.10", "cross-env": "^7.0.3", - "cspell-lib": "^8.2.3", + "cspell-lib": "^8.2.4", "lorem-ipsum": "^2.0.8", "rfdc": "^1.3.0", "source-map-support": "^0.5.21", diff --git a/package.json b/package.json index 75843c603d..88f3d3f686 100644 --- a/package.json +++ b/package.json @@ -460,6 +460,12 @@ "category": "Spell", "title": "Insert Words Directive", "icon": "$(comment-discussion)" + }, + { + "command": "cSpell.toggleTraceMode", + "category": "Spell", + "title": "Toggle Trace Mode", + "icon": "$(search)" } ], "languages": [ @@ -3386,10 +3392,10 @@ "vitest": "^1.1.0" }, "dependencies": { - "@cspell/cspell-bundled-dicts": "^8.2.3", - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-bundled-dicts": "^8.2.4", + "@cspell/cspell-types": "^8.2.4", "@types/react": "^17.0.73", - "cspell": "^8.2.3", + "cspell": "^8.2.4", "regexp-worker": "^2.0.2" }, "comment-resolutions": { diff --git a/packages/__cspell-helper/package.json b/packages/__cspell-helper/package.json index 8d7c97abd3..6bece1b7b6 100644 --- a/packages/__cspell-helper/package.json +++ b/packages/__cspell-helper/package.json @@ -22,8 +22,8 @@ "yargs": "^17.7.2" }, "dependencies": { - "@cspell/cspell-types": "^8.2.3", - "cspell-lib": "^8.2.3" + "@cspell/cspell-types": "^8.2.4", + "cspell-lib": "^8.2.4" }, "engines": { "node": ">18.0.0" diff --git a/packages/_integrationTests/package.json b/packages/_integrationTests/package.json index 3114eb9f05..a71299bbc1 100644 --- a/packages/_integrationTests/package.json +++ b/packages/_integrationTests/package.json @@ -23,7 +23,7 @@ "author": "", "license": "MIT", "devDependencies": { - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-types": "^8.2.4", "@types/chai": "^4.3.11", "@types/decompress": "^4.2.7", "@types/glob": "^8.1.0", diff --git a/packages/_server/package.json b/packages/_server/package.json index f7d4d47fe6..a4868c9a61 100644 --- a/packages/_server/package.json +++ b/packages/_server/package.json @@ -35,14 +35,14 @@ "yargs": "^17.7.2" }, "dependencies": { - "@cspell/cspell-bundled-dicts": "^8.2.3", - "@cspell/cspell-pipe": "^8.2.3", - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-bundled-dicts": "^8.2.4", + "@cspell/cspell-pipe": "^8.2.4", + "@cspell/cspell-types": "^8.2.4", "@internal/common-utils": "file:../__utils", - "cspell-config-lib": "^8.2.3", - "cspell-gitignore": "^8.2.3", - "cspell-glob": "^8.2.3", - "cspell-lib": "^8.2.3", + "cspell-config-lib": "^8.2.4", + "cspell-gitignore": "^8.2.4", + "cspell-glob": "^8.2.4", + "cspell-lib": "^8.2.4", "gensequence": "^6.0.0", "json-rpc-api": "file:../json-rpc-api", "rxjs": "^7.8.1", diff --git a/packages/_server/src/DocumentValidationController.mts b/packages/_server/src/DocumentValidationController.mts new file mode 100644 index 0000000000..a2556f7a15 --- /dev/null +++ b/packages/_server/src/DocumentValidationController.mts @@ -0,0 +1,98 @@ +import { createTextDocument, DocumentValidator } from 'cspell-lib'; +import { DisposableList } from 'utils-disposables'; +import type { TextDocumentChangeEvent, TextDocuments } from 'vscode-languageserver'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; + +import type { TextDocumentInfoWithText, TextDocumentRef } from './api.js'; +import type { CSpellUserSettings } from './config/cspellConfig/index.mjs'; +import type { DocumentSettings } from './config/documentSettings.mjs'; +import { defaultCheckLimit } from './constants.mjs'; + +interface DocValEntry { + uri: string; + settings: Promise; + docVal: Promise; +} + +export class DocumentValidationController { + private docValMap = new Map(); + private disposables = new DisposableList(); + + constructor( + readonly documentSettings: DocumentSettings, + readonly documents: TextDocuments, + ) { + this.disposables.push( + documents.onDidClose((e) => this.handleOnDidClose(e)), + documents.onDidChangeContent((e) => this.handleOnDidChangeContent(e)), + ); + } + + get(doc: TextDocumentRef) { + return this.docValMap.get(doc.uri); + } + + getDocumentValidator(docInfo: TextDocumentInfoWithText | TextDocument) { + const uri = docInfo.uri; + const docValEntry = this.docValMap.get(uri); + if (docValEntry) return docValEntry.docVal; + + const entry = this.createDocValEntry(docInfo); + this.docValMap.set(uri, entry); + return entry.docVal; + } + + private createDocValEntry(docInfo: TextDocumentInfoWithText | TextDocument) { + const uri = docInfo.uri; + const settings = this.documentSettings.getSettings(docInfo); + const docVal = createDocumentValidator(docInfo, settings); + const entry: DocValEntry = { uri, settings, docVal }; + return entry; + } + + clear() { + this.docValMap.clear(); + } + + dispose() { + this.clear(); + this.disposables.dispose(); + } + + private handleOnDidClose(e: TextDocumentChangeEvent) { + this.docValMap.delete(e.document.uri); + } + + private handleOnDidChangeContent(e: TextDocumentChangeEvent) { + this._handleOnDidChangeContent(e).catch(() => undefined); + } + + private async _handleOnDidChangeContent(e: TextDocumentChangeEvent) { + const { document } = e; + const entry = this.docValMap.get(document.uri); + if (!entry) return; + const { settings, docVal } = entry; + const updatedSettings = await this.documentSettings.getSettings(document); + const [_settings, _docVal, _curSettings] = await Promise.all([settings, docVal, updatedSettings] as const); + if (_settings !== _curSettings) { + this.docValMap.set(document.uri, this.createDocValEntry(document)); + return; + } + await _docVal.updateDocumentText(document.getText()); + } +} + +export async function createDocumentValidator( + textDocument: TextDocument | TextDocumentInfoWithText, + pSettings: Promise | CSpellUserSettings, +): Promise { + const settings = await pSettings; + const limit = (settings.checkLimit || defaultCheckLimit) * 1024; + const content = ('getText' in textDocument ? textDocument.getText() : textDocument.text).slice(0, limit); + const { uri, languageId, version } = textDocument; + const docInfo = { uri, content, languageId, version }; + const doc = createTextDocument(docInfo); + const docVal = new DocumentValidator(doc, { noConfigSearch: true }, settings); + await docVal.prepare(); + return docVal; +} diff --git a/packages/_server/src/api/api.ts b/packages/_server/src/api/api.ts index e677617f24..9d980f87d2 100644 --- a/packages/_server/src/api/api.ts +++ b/packages/_server/src/api/api.ts @@ -15,12 +15,16 @@ import { createClientApi, createServerApi } from 'json-rpc-api'; import type { GetConfigurationForDocumentRequest, GetConfigurationForDocumentResult, + GetSpellCheckingOffsetsResult, IsSpellCheckEnabledResult, OnSpellCheckDocumentStep, PublishDiagnostics, SpellingSuggestionsResult, SplitTextIntoWordsResult, TextDocumentInfo, + TextDocumentRef, + TraceWordRequest, + TraceWordResult, WorkspaceConfigForDocumentRequest, WorkspaceConfigForDocumentResponse, } from './apiModels.js'; @@ -34,6 +38,12 @@ export interface ServerRequestsAPI { isSpellCheckEnabled(req: TextDocumentInfo): IsSpellCheckEnabledResult; splitTextIntoWords(req: string): SplitTextIntoWordsResult; spellingSuggestions(word: string, doc?: TextDocumentInfo): SpellingSuggestionsResult; + /** + * Calculate the text ranges that should be spell checked. + * @param doc The document to be spell checked. + */ + getSpellCheckingOffsets(doc: TextDocumentRef): GetSpellCheckingOffsetsResult; + traceWord(req: TraceWordRequest): TraceWordResult; } /** Notifications that can be sent to the server */ diff --git a/packages/_server/src/api/apiModels.ts b/packages/_server/src/api/apiModels.ts index 3606937694..9c7384a6d3 100644 --- a/packages/_server/src/api/apiModels.ts +++ b/packages/_server/src/api/apiModels.ts @@ -67,6 +67,14 @@ export interface SpellingSuggestionsResult { suggestions: Suggestion[]; } +export interface GetSpellCheckingOffsetsResult { + /** + * The text offsets of the text in the document that should be spell checked. + * The offsets are start/end pairs. + */ + offsets: number[]; +} + export interface GetConfigurationForDocumentRequest extends Partial { /** used to calculate configTargets, configTargets will be empty if undefined. */ workspaceConfig?: WorkspaceConfigForDocument; @@ -205,3 +213,23 @@ export type VSCodeSettingsCspell = { }; export type PublishDiagnostics = Required; + +export interface TraceWordRequest { + uri: DocumentUri; + word: string; +} +export interface WordTrace { + word: string; + found: boolean; + foundWord: string | undefined; + forbidden: boolean; + noSuggest: boolean; + dictName: string; + dictSource: string; + errors: string | undefined; +} + +export interface TraceWordResult { + traces?: WordTrace[] | undefined; + errors?: string | undefined; +} diff --git a/packages/_server/src/config/documentSettings.mts b/packages/_server/src/config/documentSettings.mts index d3fd0f907f..700f8a6e90 100644 --- a/packages/_server/src/config/documentSettings.mts +++ b/packages/_server/src/config/documentSettings.mts @@ -46,6 +46,7 @@ import { handleSpecialUri } from './docUriHelper.mjs'; import type { TextDocumentUri } from './vscode.config.mjs'; import { getConfiguration, getWorkspaceFolders } from './vscode.config.mjs'; import { createWorkspaceNamesResolver, resolveSettings } from './WorkspacePathResolver.mjs'; +import { findMatchingFoldersForUri } from '../utils/matchingFoldersForUri.mjs'; // The settings interface describe the server relevant settings part export type SettingsCspell = VSCodeSettingsCspell; @@ -474,7 +475,7 @@ export class DocumentSettings { public async matchingFoldersForUri(docUri: string): Promise { const folders = await this.folders; - return _matchingFoldersForUri(folders, docUri); + return findMatchingFoldersForUri(folders, docUri); } private createCache(loader: (key: K) => T): AutoLoadCache { @@ -554,10 +555,6 @@ export function isLanguageEnabled(languageId: string, settings: CSpellUserSettin return checkOnly && starEnabled !== true ? !!enabled : enabled !== false; } -function _matchingFoldersForUri(folders: WorkspaceFolder[], docUri: string): WorkspaceFolder[] { - return folders.filter(({ uri }) => docUri.startsWith(uri)).sort((a, b) => b.uri.length - a.uri.length); -} - function _bestMatchingFolderForUri(folders: WorkspaceFolder[], docUri: string | undefined, defaultFolder: WorkspaceFolder): WorkspaceFolder; function _bestMatchingFolderForUri( folders: WorkspaceFolder[], @@ -570,7 +567,7 @@ function _bestMatchingFolderForUri( defaultFolder?: WorkspaceFolder, ): WorkspaceFolder | undefined { if (!docUri) return defaultFolder; - const matches = _matchingFoldersForUri(folders, docUri); + const matches = findMatchingFoldersForUri(folders, docUri); return matches[0] || defaultFolder; } diff --git a/packages/_server/src/constants.mts b/packages/_server/src/constants.mts index de5aad08a7..e60fa89793 100644 --- a/packages/_server/src/constants.mts +++ b/packages/_server/src/constants.mts @@ -2,3 +2,4 @@ import type { DiagnosticSource, ExtensionId } from './api.js'; export const extensionId: ExtensionId = 'cSpell'; export const diagnosticSource: DiagnosticSource = extensionId; +export const defaultCheckLimit = 500; diff --git a/packages/_server/src/progressNotifier.test.mts b/packages/_server/src/progressNotifier.test.mts index 7f0a1e1890..7e34e5401a 100644 --- a/packages/_server/src/progressNotifier.test.mts +++ b/packages/_server/src/progressNotifier.test.mts @@ -1,11 +1,10 @@ -import type { ExcludeDisposableHybrid } from 'utils-disposables'; -import { injectDisposable } from 'utils-disposables'; import { describe, expect, test, vi } from 'vitest'; import { TextDocument } from 'vscode-languageserver-textdocument'; import type { MessageConnection, ServerSideApi } from './api.js'; import { createProgressNotifier } from './progressNotifier.mjs'; import { createServerApi } from './serverApi.mjs'; +import { createMockServerSideApi } from './test/test.api.js'; vi.mock('./serverApi'); @@ -13,31 +12,7 @@ const mockedCreateClientApi = vi.mocked(createServerApi); // const mockedCreateConnection = jest.mocked(createConnection); mockedCreateClientApi.mockImplementation(() => { - const mock: ServerSideApi = injectDisposable>( - { - clientRequest: { - onWorkspaceConfigForDocumentRequest: vi.fn(), - vfsReadDirectory: vi.fn(() => Promise.resolve([])), - vfsReadFile: vi.fn(() => Promise.resolve({ uri: '', content: '' })), - vfsStat: vi.fn(() => Promise.resolve({ type: 0, size: 0, mtime: 0 })), - }, - clientNotification: { - onSpellCheckDocument: vi.fn(), - onDiagnostics: vi.fn(), - }, - serverRequest: { - getConfigurationForDocument: { subscribe: vi.fn() }, - isSpellCheckEnabled: { subscribe: vi.fn() }, - splitTextIntoWords: { subscribe: vi.fn() }, - spellingSuggestions: { subscribe: vi.fn() }, - }, - serverNotification: { - notifyConfigChange: { subscribe: vi.fn() }, - registerConfigurationFile: { subscribe: vi.fn() }, - }, - }, - () => undefined, - ); + const mock: ServerSideApi = createMockServerSideApi(); return mock; }); diff --git a/packages/_server/src/server.mts b/packages/_server/src/server.mts index 9036279bea..2a1d288e17 100644 --- a/packages/_server/src/server.mts +++ b/packages/_server/src/server.mts @@ -45,7 +45,10 @@ import { } from './config/documentSettings.mjs'; import { isScmUri } from './config/docUriHelper.mjs'; import type { TextDocumentUri } from './config/vscode.config.mjs'; +import { defaultCheckLimit } from './constants.mjs'; +import { DocumentValidationController } from './DocumentValidationController.mjs'; import { createProgressNotifier } from './progressNotifier.mjs'; +import type { PartialServerSideHandlers } from './serverApi.mjs'; import { createServerApi } from './serverApi.mjs'; import { createOnSuggestionsHandler } from './suggestionsServer.mjs'; import { defaultIsTextLikelyMinifiedOptions, isTextLikelyMinified } from './utils/analysis.mjs'; @@ -58,8 +61,6 @@ import { bindFileSystemProvider } from './vfs/CSpellFileSystemProvider.mjs'; log('Starting Spell Checker Server'); -const defaultCheckLimit = Validator.defaultCheckLimit; - const overRideDefaults: CSpellUserSettings = { id: 'Extension overrides', patterns: [], @@ -103,31 +104,30 @@ export function run(): void { // Create a simple text document manager. const documents = new TextDocuments(TextDocument); - const clientServerApi: Api.ServerSideApi = dd( - createServerApi( - connection, - { - serverNotifications: { - notifyConfigChange: (...p) => (logInfo('notifyConfigChange'), onConfigChange(...p)), - registerConfigurationFile, - }, - serverRequests: { - getConfigurationForDocument: handleGetConfigurationForDocument, - isSpellCheckEnabled: handleIsSpellCheckEnabled, - splitTextIntoWords: handleSplitTextIntoWords, - spellingSuggestions: createOnSuggestionsHandler(documents, { - fetchSettings: getBaseSettings, - getSettingsVersion: () => documentSettings.version, - }), - }, - }, - _logger, - ), - ); + const handlers: PartialServerSideHandlers = { + serverNotifications: { + notifyConfigChange: (...p) => (logInfo('notifyConfigChange'), onConfigChange(...p)), + registerConfigurationFile, + }, + serverRequests: { + getConfigurationForDocument: handleGetConfigurationForDocument, + getSpellCheckingOffsets: simpleDebounce(_handleGetSpellCheckingOffsets, 100, ({ uri }) => uri), + traceWord: simpleDebounce(_handleGetWordTrace, 100, ({ uri, word }) => uri + '|' + word), + isSpellCheckEnabled: handleIsSpellCheckEnabled, + splitTextIntoWords: handleSplitTextIntoWords, + spellingSuggestions: createOnSuggestionsHandler(documents, { + fetchSettings: getBaseSettings, + getSettingsVersion: () => documentSettings.version, + }), + }, + }; - dd(bindFileSystemProvider(clientServerApi, documents)); + const clientServerApi: Api.ServerSideApi = dd(createServerApi(connection, handlers, _logger)); + + dd(bindFileSystemProvider(connection, clientServerApi, documents)); const documentSettings = new DocumentSettings(connection, clientServerApi, defaultSettings); + const docValidationController = dd(new DocumentValidationController(documentSettings, documents)); const progressNotifier = createProgressNotifier(clientServerApi); @@ -224,7 +224,10 @@ export function run(): void { tap(() => log('Update Config Triggered')), mergeMap(updateActiveSettings), ) - .subscribe(() => {}), + .subscribe(() => { + docValidationController.clear(); + log('Update Config Completed'); + }), ); ds( @@ -392,6 +395,26 @@ export function run(): void { }; } + async function _handleGetSpellCheckingOffsets(docRef: Api.TextDocumentRef): Promise { + log('handleGetSpellCheckingOffsets', docRef.uri); + const { uri } = docRef; + const doc = documents.get(uri); + if (!doc) return { offsets: [] }; + const docVal = await docValidationController.getDocumentValidator(doc); + const offsets = docVal.getCheckedTextRanges().flatMap((r) => [r.startPos, r.endPos]); + return { offsets }; + } + + async function _handleGetWordTrace(req: Api.TraceWordRequest): Promise { + const { word, uri } = req; + log(`_handleGetWordTrace "${word}"`, uri); + const doc = documents.get(uri); + if (!doc) return { errors: 'Document Not Found.' }; + const docVal = await docValidationController.getDocumentValidator(doc); + const traces = docVal.traceWord(word).map((t) => ({ ...t, errors: errorsToString(t.errors) })); + return { traces }; + } + async function getExcludedBy(uri: string): Promise { function globToString(g: Glob): string { if (typeof g === 'string') return g; @@ -513,7 +536,7 @@ export function run(): void { async function getBaseSettings(doc: TextDocumentUri | undefined) { const settings = await getActiveSettings(doc); - return { ...CSpell.mergeSettings(await defaultSettings, settings), enabledLanguageIds: settings.enabledLanguageIds }; + return { ...settings, enabledLanguageIds: settings.enabledLanguageIds }; } async function getSettingsToUseForDocument(doc: TextDocument) { @@ -708,3 +731,8 @@ interface DocSettingPair { doc: TextDocument; settings: CSpellUserSettings; } + +function errorsToString(errors: Error[] | undefined): string | undefined { + if (!errors || !errors.length) return undefined; + return errors.map((e) => e.message).join('\n'); +} diff --git a/packages/_server/src/serverApi.mts b/packages/_server/src/serverApi.mts index d75804c856..0a336674b3 100644 --- a/packages/_server/src/serverApi.mts +++ b/packages/_server/src/serverApi.mts @@ -9,9 +9,11 @@ export function createServerApi(connection: MessageConnection, handlers: Partial const api: ServerSideApiDef = { serverRequests: { getConfigurationForDocument: true, + getSpellCheckingOffsets: true, isSpellCheckEnabled: true, splitTextIntoWords: true, spellingSuggestions: true, + traceWord: true, ...handlers.serverRequests, }, serverNotifications: { diff --git a/packages/_server/src/test/test.api.ts b/packages/_server/src/test/test.api.ts index af1fb3a378..764f4597c9 100644 --- a/packages/_server/src/test/test.api.ts +++ b/packages/_server/src/test/test.api.ts @@ -14,7 +14,9 @@ export function createMockServerSideApi() { getConfigurationForDocument: { subscribe: vi.fn() }, isSpellCheckEnabled: { subscribe: vi.fn() }, splitTextIntoWords: { subscribe: vi.fn() }, + getSpellCheckingOffsets: { subscribe: vi.fn() }, spellingSuggestions: { subscribe: vi.fn() }, + traceWord: { subscribe: vi.fn() }, }, clientNotification: { onSpellCheckDocument: vi.fn(), @@ -62,6 +64,8 @@ export function mockHandlers(): ServerSideHandlers { isSpellCheckEnabled: vi.fn(() => ({ ...sampleIsSpellCheckEnabledResult })), splitTextIntoWords: vi.fn(() => ({ words: [] })), spellingSuggestions: vi.fn(() => ({ suggestions: [] })), + getSpellCheckingOffsets: vi.fn(() => ({ offsets: [] })), + traceWord: vi.fn(() => ({ traces: [] })), }, }; } diff --git a/packages/_server/src/utils/matchingFoldersForUri.mts b/packages/_server/src/utils/matchingFoldersForUri.mts new file mode 100644 index 0000000000..8d303e1a08 --- /dev/null +++ b/packages/_server/src/utils/matchingFoldersForUri.mts @@ -0,0 +1,17 @@ +import type { WorkspaceFolder } from 'vscode-languageserver/node.js'; + +const endUrlOfPath: Record = { + '/': true, + '?': true, + '#': true, +}; + +export function findMatchingFoldersForUri(folders: WorkspaceFolder[], docUri: string): WorkspaceFolder[] { + return folders + .filter(({ uri }) => docUri.startsWith(uri) && (endUrlOfPath[docUri[uri.length]] || uri.endsWith('/') || docUri === uri)) + .sort((a, b) => b.uri.length - a.uri.length); +} + +export function findMatchingFolderForUri(folders: WorkspaceFolder[], docUri: string): WorkspaceFolder | undefined { + return findMatchingFoldersForUri(folders, docUri)[0]; +} diff --git a/packages/_server/src/validator.mts b/packages/_server/src/validator.mts index 9d4d63a333..4ed428cec0 100644 --- a/packages/_server/src/validator.mts +++ b/packages/_server/src/validator.mts @@ -1,4 +1,4 @@ -import { createTextDocument, DocumentValidator, Text as TextUtil } from 'cspell-lib'; +import { Text as TextUtil } from 'cspell-lib'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import type { Diagnostic } from 'vscode-languageserver-types'; import { DiagnosticSeverity } from 'vscode-languageserver-types'; @@ -7,12 +7,12 @@ import type { SpellCheckerDiagnosticData, SpellingDiagnostic, Suggestion } from import type { CSpellUserSettings } from './config/cspellConfig/index.mjs'; import { isScmUri } from './config/docUriHelper.mjs'; import { diagnosticSource } from './constants.mjs'; +import { createDocumentValidator } from './DocumentValidationController.mjs'; export { createTextDocument, validateText } from 'cspell-lib'; export const diagnosticCollectionName = diagnosticSource; export const diagSource = diagnosticCollectionName; -export const defaultCheckLimit = 500; const diagSeverityMap = new Map([ ['error', DiagnosticSeverity.Error], @@ -24,17 +24,7 @@ const diagSeverityMap = new Map([ export async function validateTextDocument(textDocument: TextDocument, options: CSpellUserSettings): Promise { const { severity, severityFlaggedWords } = calcSeverity(textDocument.uri, options); - const limit = (options.checkLimit || defaultCheckLimit) * 1024; - const content = textDocument.getText().slice(0, limit); - const docInfo = { - uri: textDocument.uri, - content, - languageId: textDocument.languageId, - version: textDocument.version, - }; - const doc = createTextDocument(docInfo); - const docVal = new DocumentValidator(doc, { noConfigSearch: true }, options); - await docVal.prepare(); + const docVal = await createDocumentValidator(textDocument, options); const r = await docVal.checkDocumentAsync(true); const diags = r // Convert the offset into a position diff --git a/packages/_server/src/vfs/CSpellFileSystemProvider.mts b/packages/_server/src/vfs/CSpellFileSystemProvider.mts index 3a89ec9c5c..f1776cee14 100644 --- a/packages/_server/src/vfs/CSpellFileSystemProvider.mts +++ b/packages/_server/src/vfs/CSpellFileSystemProvider.mts @@ -3,11 +3,13 @@ import type { VProviderFileSystem } from 'cspell-io'; import { FSCapabilityFlags, urlOrReferenceToUrl, VFileType } from 'cspell-io'; import type { VFileSystemProvider } from 'cspell-lib'; import { getVirtualFS } from 'cspell-lib'; -import type { Disposable, TextDocuments } from 'vscode-languageserver/node.js'; +import { DisposableList } from 'utils-disposables'; +import type { Connection, Disposable, TextDocuments, WorkspaceFolder } from 'vscode-languageserver/node.js'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import type { ServerSideApi } from '../api.js'; import { FileType } from '../api.js'; +import { findMatchingFolderForUri } from '../utils/matchingFoldersForUri.mjs'; const debugFileProtocol = false; @@ -19,10 +21,34 @@ const NotHandledProtocols: Record = { class CSpellFileSystemProvider implements VFileSystemProvider { readonly name = 'VSCode'; + private pFolders: Promise | undefined; + private folders: WorkspaceFolder[] | undefined; + private disposables = new DisposableList(); constructor( + private connection: Connection, private api: ServerSideApi, private documents: TextDocuments, - ) {} + ) { + this.init(); + } + + private async updateWorkspaceFolders() { + this.pFolders = this._updateWorkspaceFolders(); + return this.pFolders; + } + + private async _updateWorkspaceFolders() { + try { + const folders = await this.connection.workspace.getWorkspaceFolders(); + logDebug(`Workspace folders: ${JSON.stringify(folders)}`); + this.folders = folders ?? []; + this.pFolders = undefined; + return this.folders; + } catch (e) { + logDebug(`Error getting workspace folders: ${e}`); + return []; + } + } getFileSystem(url: URL): VProviderFileSystem | undefined { if (NotHandledProtocols[url.protocol.toLowerCase()]) return undefined; @@ -45,10 +71,19 @@ class CSpellFileSystemProvider implements VFileSystemProvider { : VFileType.Unknown, }; }, - readDirectory: async (_url) => { - const url = urlOrReferenceToUrl(_url); + readDirectory: async (urlRef) => { + const url = urlOrReferenceToUrl(urlRef); logDebug(`readDirectory: ${url.href}`); - const entries = await this.api.clientRequest.vfsReadDirectory(url.href); + const href = url.href; + const folders = (await this.pFolders) ?? this.folders ?? (await this.updateWorkspaceFolders()); + const folder = findMatchingFolderForUri(folders, href); + // Do not read directories outside of the workspace. + if (!folder) { + logDebug(`readDirectory: ${url.href} not in workspace: ${JSON.stringify(folders)}`); + logDebug(`readDirectory: ${url.href} not in workspace`); + return []; + } + const entries = await this.api.clientRequest.vfsReadDirectory(href); return entries .map(([name, type]) => [name, type & FileType.Directory ? VFileType.Directory : VFileType.File] as const) .map(([name, type]) => ({ name, dir: url, fileType: type })); @@ -76,15 +111,29 @@ class CSpellFileSystemProvider implements VFileSystemProvider { providerInfo: { name: this.name, }, - dispose: () => {}, + dispose: () => { + this.disposables.dispose(); + }, }; return vfs; } + + private init() { + this.disposables.push( + this.connection.onInitialized(() => { + this.disposables.push( + this.connection.workspace.onDidChangeWorkspaceFolders(() => { + this.updateWorkspaceFolders(); + }), + ); + }), + ); + } } -export function bindFileSystemProvider(api: ServerSideApi, documents: TextDocuments): Disposable { - const provider = new CSpellFileSystemProvider(api, documents); +export function bindFileSystemProvider(connection: Connection, api: ServerSideApi, documents: TextDocuments): Disposable { + const provider = new CSpellFileSystemProvider(connection, api, documents); const vfs = getVirtualFS(); if (debugFileProtocol) { vfs.enableLogging(true); diff --git a/packages/_serverPatternMatcher/package.json b/packages/_serverPatternMatcher/package.json index 960ee3197b..1cf6e9a7ed 100644 --- a/packages/_serverPatternMatcher/package.json +++ b/packages/_serverPatternMatcher/package.json @@ -47,9 +47,9 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-types": "^8.2.4", "@internal/common-utils": "file:../__utils", - "cspell-lib": "^8.2.3", + "cspell-lib": "^8.2.4", "regexp-worker": "^2.0.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-languageserver": "^9.0.1", diff --git a/packages/client/package.json b/packages/client/package.json index 30703a93fa..3b0bfd2503 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -45,7 +45,7 @@ "@types/kefir": "^3.8.11", "@types/source-map-support": "^0.5.10", "cross-env": "^7.0.3", - "cspell-lib": "^8.2.3", + "cspell-lib": "^8.2.4", "lorem-ipsum": "^2.0.8", "rfdc": "^1.3.0", "source-map-support": "^0.5.21", @@ -55,7 +55,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@cspell/cspell-types": "^8.2.3", + "@cspell/cspell-types": "^8.2.4", "@internal/common-utils": "file:../__utils", "@internal/settings-webview": "file:../_settingsViewer", "code-spell-checker-server": "file:../_server", diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index 2ae1effbb6..85ae103398 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -58,7 +58,7 @@ export class CSpellClient implements Disposable { readonly languageIds: Set; readonly allowedSchemas: Set; - private serverApi: ServerApi; + serverApi: ServerApi; private disposables: Set = new Set(); private broadcasterOnSpellCheckDocument = createBroadcaster(); private ready: Resolvable = new Resolvable(); diff --git a/packages/client/src/client/server/server.ts b/packages/client/src/client/server/server.ts index 408f37c6de..a0a91ea681 100644 --- a/packages/client/src/client/server/server.ts +++ b/packages/client/src/client/server/server.ts @@ -42,10 +42,12 @@ export type GetConfigurationForDocumentResult = Partial clientNotification.onSpellCheckDocument.subscribe(log2Cfn(fn, 'onSpellCheckDocument')), onDiagnostics: (fn) => clientNotification.onDiagnostics.subscribe(log2Cfn(fn, 'onDiagnostics')), onWorkspaceConfigForDocumentRequest: (fn) => clientRequest.onWorkspaceConfigForDocumentRequest.subscribe(log2Cfn(fn, 'onWorkspaceConfigForDocumentRequest')), - dispose: rpcApi.dispose, }; diff --git a/packages/client/src/commands.ts b/packages/client/src/commands.ts index d07befaba3..546b642295 100644 --- a/packages/client/src/commands.ts +++ b/packages/client/src/commands.ts @@ -156,18 +156,22 @@ export const commandHandlers = { 'cSpell.insertDisableLineDirective': handleInsertDisableLineDirective, 'cSpell.insertIgnoreWordsDirective': handleInsertIgnoreWordsDirective, 'cSpell.insertWordsDirective': handleInsertWordsDirective, + + 'cSpell.toggleTraceMode': handlerResolvedLater, } as const satisfies CommandHandler; type ImplementedCommandHandlers = typeof commandHandlers; type ImplementedCommandNames = keyof ImplementedCommandHandlers; +export type InjectableCommandHandlers = Partial; + export const knownCommands = Object.fromEntries( Object.keys(commandHandlers).map((key) => [key, key] as [ImplementedCommandNames, ImplementedCommandNames]), ) as Record; -export function registerCommands(): Disposable[] { +export function registerCommands(injectCommands: InjectableCommandHandlers): Disposable[] { const skipRegister = new Set(); - const registeredHandlers = Object.entries(commandHandlers) + const registeredHandlers = Object.entries({ ...commandHandlers, ...injectCommands }) .filter(([cmd]) => !skipRegister.has(cmd)) .map(([cmd, fn]) => registerCmd(cmd, fn)); const registeredFromServer = Object.entries(commandsFromServer).map(([cmd, fn]) => registerCmd(cmd, fn)); diff --git a/packages/client/src/decorate.ts b/packages/client/src/decorate.ts index 18e2da92f1..9b2a6f154e 100644 --- a/packages/client/src/decorate.ts +++ b/packages/client/src/decorate.ts @@ -1,176 +1,2 @@ -import { createDisposableList } from 'utils-disposables'; -import type { DecorationOptions, DiagnosticChangeEvent, TextDocument, TextEditor, TextEditorDecorationType, Uri } from 'vscode'; -import vscode, { ColorThemeKind, DiagnosticSeverity, MarkdownString } from 'vscode'; - -import type { CSpellUserSettings } from './client'; -import { commandUri, createTextEditCommand } from './commands'; -import type { Disposable } from './disposable'; -import type { IssueTracker, SpellingDiagnostic } from './issueTracker'; - -export class SpellingIssueDecorator implements Disposable { - private decorationTypeForIssues: TextEditorDecorationType | undefined; - private decorationTypeForFlagged: TextEditorDecorationType | undefined; - private disposables = createDisposableList(); - public dispose = this.disposables.dispose; - - constructor(readonly issueTracker: IssueTracker) { - const decorators = this.createDecorators(); - - this.decorationTypeForIssues = decorators?.decoratorIssues; - this.decorationTypeForFlagged = decorators?.decoratorFlagged; - this.disposables.push( - () => this.clearDecoration(), - vscode.workspace.onDidChangeConfiguration((e) => e.affectsConfiguration('cSpell') && this.resetDecorator()), - vscode.window.onDidChangeActiveColorTheme(() => this.resetDecorator()), - issueTracker.onDidChangeDiagnostics((e) => this.handleOnDidChangeDiagnostics(e)), - vscode.window.onDidChangeActiveTextEditor((e) => this.refreshEditor(e)), - ); - } - - private handleOnDidChangeDiagnostics(event: DiagnosticChangeEvent) { - this.refreshDiagnostics(event.uris); - } - - refreshEditor(e: vscode.TextEditor | undefined) { - e ??= vscode.window.activeTextEditor; - if (!e) return; - return this.refreshDiagnostics([e.document.uri]); - } - - refreshDiagnostics(docUris?: readonly Uri[]) { - docUris ??= vscode.window.visibleTextEditors.map((e) => e.document.uri); - const updated = new Set(docUris.map((uri) => uri.toString())); - const editors = vscode.window.visibleTextEditors.filter((editor) => updated.has(editor.document.uri.toString())); - editors.forEach((editor) => this.refreshDiagnosticsInEditor(editor)); - } - - refreshDiagnosticsInEditor(editor: TextEditor) { - if (!this.decorationTypeForIssues || !this.decorationTypeForFlagged) return; - const doc = editor.document; - const diags = this.issueTracker.getDiagnostics(doc.uri) || []; - - const decorationsIssues: DecorationOptions[] = diags - .filter((diag) => diag.severity === DiagnosticSeverity.Hint) - .filter((diag) => !diag.data?.isFlagged) - .map((diag) => diagToDecorationOptions(diag, doc)); - editor.setDecorations(this.decorationTypeForIssues, decorationsIssues); - - const decorationsFlagged: DecorationOptions[] = diags - .filter((diag) => diag.severity === DiagnosticSeverity.Hint) - .filter((diag) => diag.data?.isFlagged) - .map((diag) => diagToDecorationOptions(diag, doc)); - editor.setDecorations(this.decorationTypeForFlagged, decorationsFlagged); - } - - private clearDecoration() { - this.decorationTypeForIssues?.dispose(); - this.decorationTypeForIssues = undefined; - this.decorationTypeForFlagged?.dispose(); - this.decorationTypeForFlagged = undefined; - } - - private resetDecorator() { - const decorators = this.createDecorators(); - this.decorationTypeForIssues = decorators?.decoratorIssues; - this.decorationTypeForFlagged = decorators?.decoratorFlagged; - this.refreshDiagnostics(); - } - - private createDecorators(): { decoratorIssues: TextEditorDecorationType; decoratorFlagged: TextEditorDecorationType } | undefined { - this.clearDecoration(); - const decorateIssues = vscode.workspace.getConfiguration('cSpell').get('decorateIssues'); - if (!decorateIssues) return undefined; - - const mode = calcMode(vscode.window.activeColorTheme.kind); - const cfg = vscode.workspace.getConfiguration('cSpell') as CSpellUserSettings; - - const overviewRulerColor: string | undefined = cfg[mode]?.overviewRulerColor || cfg.overviewRulerColor || undefined; - - const decoratorIssues = vscode.window.createTextEditorDecorationType({ - isWholeLine: false, - rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, - overviewRulerLane: vscode.OverviewRulerLane.Right, - overviewRulerColor: overviewRulerColor, - textDecoration: calcTextDecoration(cfg, mode, 'textDecorationColor'), - }); - - const decoratorFlagged = vscode.window.createTextEditorDecorationType({ - isWholeLine: false, - rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, - overviewRulerLane: vscode.OverviewRulerLane.Right, - overviewRulerColor: overviewRulerColor, - textDecoration: calcTextDecoration(cfg, mode, 'textDecorationColorFlagged'), - }); - - return { decoratorIssues, decoratorFlagged }; - } -} - -function calcTextDecoration(cfg: CSpellUserSettings, mode: ColorMode, colorField: 'textDecorationColor' | 'textDecorationColorFlagged') { - const textDecoration = cfg[mode]?.textDecoration || cfg.textDecoration || ''; - const line = cfg[mode]?.textDecorationLine || cfg.textDecorationLine || 'underline'; - const style = cfg[mode]?.textDecorationStyle || cfg.textDecorationStyle || 'wavy'; - const thickness = cfg[mode]?.textDecorationThickness || cfg.textDecorationThickness || 'auto'; - const color = cfg[mode]?.[colorField] || cfg[colorField] || '#fc4'; - return textDecoration || `${line} ${style} ${color} ${thickness}`; -} - -function diagToDecorationOptions(diag: SpellingDiagnostic, doc: TextDocument): DecorationOptions { - const { range } = diag; - const { suggestions, isFlagged, isSuggestion } = diag.data || {}; - const text = doc.getText(range); - - const commandSuggest = commandUri('cSpell.suggestSpellingCorrections', doc.uri, range, text); - // const commandAdd = commandUri('cSpell.addWordToDictionary', text); - - const mdShowSuggestions = markdownLink('Suggestions $(chevron-right)', commandSuggest, 'Show suggestions.'); - - const icon = isFlagged ? '$(error)' : isSuggestion ? '$(info)' : '$(warning)'; - - const hoverMessage = new MarkdownString(icon + ' ', true); - - hoverMessage.appendMarkdown('***').appendText(text).appendMarkdown('***: '); - - if (isSuggestion) { - hoverMessage.appendText('Has Suggestions.'); - if (!suggestions?.length) { - hoverMessage.appendText(' ').appendMarkdown(mdShowSuggestions); - } - } else { - if (isFlagged) { - hoverMessage.appendText('Forbidden word.'); - } else { - hoverMessage.appendText('Unknown word.'); - } - hoverMessage.appendText(' ').appendMarkdown(mdShowSuggestions); - } - - if (suggestions?.length) { - for (const suggestion of suggestions) { - const { word } = suggestion; - const cmd = createTextEditCommand('fix', doc.uri, doc.version, [{ range, newText: word }]); - hoverMessage.appendMarkdown('\n- ' + markdownLink(word, commandUri(cmd), `Fix with ${word}`) + '\n'); - } - } - - hoverMessage.isTrusted = true; - return { range, hoverMessage }; -} - -function markdownLink(text: string, uri: string, hover?: string) { - const hoverText = hover ? ` "${hover}"` : ''; - return `[${text}](${uri}${hoverText})`; -} - -type ColorMode = 'dark' | 'light'; - -function calcMode(kind: ColorThemeKind): ColorMode { - switch (kind) { - case ColorThemeKind.Dark: - case ColorThemeKind.HighContrast: - return 'dark'; - case ColorThemeKind.HighContrastLight: - case ColorThemeKind.Light: - return 'light'; - } -} +export { SpellingExclusionsDecorator } from './decorators/decorateExclusions.js'; +export { SpellingIssueDecorator } from './decorators/decorateIssues.js'; diff --git a/packages/client/src/decorators/decorateExclusions.ts b/packages/client/src/decorators/decorateExclusions.ts new file mode 100644 index 0000000000..1928e4e8eb --- /dev/null +++ b/packages/client/src/decorators/decorateExclusions.ts @@ -0,0 +1,195 @@ +import { createDisposableList } from 'utils-disposables'; +import type { TextEditorDecorationType } from 'vscode'; +import vscode from 'vscode'; + +import type { CSpellClient } from '../client'; +import type { Disposable } from '../disposable'; +import { createEmitter, map, pipe, throttle } from '../Subscribables'; + +const ignoreSchemes: Record = { + output: true, +}; + +export class SpellingExclusionsDecorator implements Disposable { + private decorationType: TextEditorDecorationType | undefined; + private disposables = createDisposableList(); + public dispose = this.disposables.dispose; + private eventEmitter = createEmitter(); + private _enabled = false; + private _pendingUpdates = new Set(); + + constructor( + readonly context: vscode.ExtensionContext, + readonly client: CSpellClient, + ) { + this.disposables.push( + () => this.clearDecoration(), + vscode.window.onDidChangeActiveTextEditor((e) => this.refreshEditor(e)), + vscode.workspace.onDidChangeConfiguration((e) => e.affectsConfiguration('cSpell') && this.refreshEditor(undefined)), + vscode.workspace.onDidChangeTextDocument((e) => this.refreshDocument(e.document)), + vscode.languages.registerHoverProvider('*', this.getHoverProvider()), + pipe( + this.eventEmitter, + map((e) => (e && this._pendingUpdates.add(e), e)), + throttle(100), + ).subscribe(() => this.handlePendingUpdates()), + ); + this._enabled = context.workspaceState.get(SpellingExclusionsDecorator.workspaceStateKey, false); + } + + get enabled() { + return this._enabled; + } + + set enabled(value: boolean) { + if (this._enabled === value) return; + this._enabled = value; + this.resetDecorator(); + } + + toggleEnabled() { + this.enabled = !this.enabled; + this.context.workspaceState.update(SpellingExclusionsDecorator.workspaceStateKey, this.enabled); + } + + private refreshEditor(editor?: vscode.TextEditor | undefined) { + editor ??= vscode.window.activeTextEditor; + if (!editor) return; + this.eventEmitter.notify(editor); + } + + private refreshDocument(doc: vscode.TextDocument) { + if (!this.enabled) return; + const editor = vscode.window.visibleTextEditors.find((e) => e.document === doc); + if (!editor) return; + this.refreshEditor(editor); + } + + private clearDecoration() { + this.decorationType?.dispose(); + this.decorationType = undefined; + } + + private resetDecorator() { + this.clearDecoration(); + if (!this.enabled) return; + this.createDecorator(); + this.refreshEditor(); + } + + private createDecorator() { + this.decorationType?.dispose(); + this.decorationType = vscode.window.createTextEditorDecorationType({ + light: { + // this color will be used in light color themes + backgroundColor: '#8884', + }, + dark: { + // this color will be used in dark color themes + backgroundColor: '#8884', + }, + }); + } + + private async getOffsets(editor: vscode.TextEditor | undefined): Promise { + const doc = editor?.document; + if (!doc) return []; + if (doc.uri.scheme in ignoreSchemes) return []; + const exclusions = await this.client.serverApi.getSpellCheckingOffsets({ uri: doc.uri.toString() }); + return exclusions.offsets; + } + + private handlePendingUpdates() { + const editors = [...this._pendingUpdates]; + this._pendingUpdates.clear(); + for (const editor of editors) { + this.updateDecorations(editor); + } + } + + private async updateDecorations(editor: vscode.TextEditor | undefined) { + if (!this.decorationType || !editor) return; + const hoverMessage = new vscode.MarkdownString('Excluded from spell checking'); + try { + const doc = editor.document; + const decorations: vscode.DecorationOptions[] = []; + const offsets = await this.getOffsets(editor); + if (offsets.length < 2) return; + let lastPos = 0; + for (let i = 0; i < offsets.length - 1; i += 2) { + const end = offsets[i]; + if (end <= lastPos) { + lastPos = offsets[i + 1]; + continue; + } + const range = new vscode.Range(doc.positionAt(lastPos), doc.positionAt(end)); + decorations.push({ range, hoverMessage }); + lastPos = offsets[i + 1]; + } + const textLen = doc.getText().length; + if (lastPos < textLen) { + const range = new vscode.Range(doc.positionAt(lastPos), doc.positionAt(textLen)); + decorations.push({ range }); + } + editor.setDecorations(this.decorationType, decorations); + } catch (err) { + editor.setDecorations(this.decorationType, []); + console.error(err); + } + } + + private getHoverProvider(): vscode.HoverProvider { + return { + provideHover: async (doc, pos) => { + if (!this.enabled) return undefined; + if (doc.uri.scheme in ignoreSchemes) return undefined; + const range = doc.getWordRangeAtPosition(pos); + if (!range) return undefined; + const word = doc.getText(range); + const traceResult = await this.client.serverApi.traceWord({ uri: doc.uri.toString(), word }); + const hoverMessage = new vscode.MarkdownString(); + hoverMessage.appendMarkdown('**Trace:** ').appendText(word + '\n'); + hoverMessage.isTrusted = true; + hoverMessage.baseUri = doc.uri; + hoverMessage.supportThemeIcons = true; + if (traceResult.errors) { + hoverMessage.appendMarkdown('**Errors:** ').appendText(traceResult.errors + '\n'); + } + if (traceResult.traces) { + const found = traceResult.traces.filter((t) => t.found); + if (!found.length) { + hoverMessage.appendMarkdown('**Not Found**\n'); + } + + for (const trace of found) { + if (isUrlLike(trace.dictSource || '') && !trace.dictSource.includes('node_modules')) { + hoverMessage + .appendMarkdown('- $(book) [') + .appendText(trace.dictName) + .appendMarkdown('](') + .appendText(trace.dictSource) + .appendMarkdown(')'); + } else { + hoverMessage.appendMarkdown('- $(book) _').appendText(trace.dictName).appendMarkdown('_'); + } + if (trace.foundWord && trace.foundWord !== word) { + hoverMessage.appendMarkdown(' **').appendText(trace.foundWord.trim()).appendMarkdown('**'); + } + hoverMessage.appendMarkdown('\n'); + } + } + hoverMessage.appendMarkdown('\n[Disable Trace Mode](command:cSpell.toggleTraceMode)'); + const hover = new vscode.Hover(hoverMessage, range); + return hover; + }, + }; + } + + static workspaceStateKey = 'showTrace'; +} + +const regExpUri = /^([a-z-]{2,}):\/\//i; + +function isUrlLike(uri: string): boolean { + return regExpUri.test(uri); +} diff --git a/packages/client/src/decorators/decorateIssues.ts b/packages/client/src/decorators/decorateIssues.ts new file mode 100644 index 0000000000..ae1199b5df --- /dev/null +++ b/packages/client/src/decorators/decorateIssues.ts @@ -0,0 +1,176 @@ +import { createDisposableList } from 'utils-disposables'; +import type { DecorationOptions, DiagnosticChangeEvent, TextDocument, TextEditor, TextEditorDecorationType, Uri } from 'vscode'; +import vscode, { ColorThemeKind, DiagnosticSeverity, MarkdownString } from 'vscode'; + +import type { CSpellUserSettings } from '../client'; +import { commandUri, createTextEditCommand } from '../commands'; +import type { Disposable } from '../disposable'; +import type { IssueTracker, SpellingDiagnostic } from '../issueTracker'; + +export class SpellingIssueDecorator implements Disposable { + private decorationTypeForIssues: TextEditorDecorationType | undefined; + private decorationTypeForFlagged: TextEditorDecorationType | undefined; + private disposables = createDisposableList(); + public dispose = this.disposables.dispose; + + constructor(readonly issueTracker: IssueTracker) { + const decorators = this.createDecorators(); + + this.decorationTypeForIssues = decorators?.decoratorIssues; + this.decorationTypeForFlagged = decorators?.decoratorFlagged; + this.disposables.push( + () => this.clearDecoration(), + vscode.workspace.onDidChangeConfiguration((e) => e.affectsConfiguration('cSpell') && this.resetDecorator()), + vscode.window.onDidChangeActiveColorTheme(() => this.resetDecorator()), + issueTracker.onDidChangeDiagnostics((e) => this.handleOnDidChangeDiagnostics(e)), + vscode.window.onDidChangeActiveTextEditor((e) => this.refreshEditor(e)), + ); + } + + private handleOnDidChangeDiagnostics(event: DiagnosticChangeEvent) { + this.refreshDiagnostics(event.uris); + } + + refreshEditor(e: vscode.TextEditor | undefined) { + e ??= vscode.window.activeTextEditor; + if (!e) return; + return this.refreshDiagnostics([e.document.uri]); + } + + refreshDiagnostics(docUris?: readonly Uri[]) { + docUris ??= vscode.window.visibleTextEditors.map((e) => e.document.uri); + const updated = new Set(docUris.map((uri) => uri.toString())); + const editors = vscode.window.visibleTextEditors.filter((editor) => updated.has(editor.document.uri.toString())); + editors.forEach((editor) => this.refreshDiagnosticsInEditor(editor)); + } + + refreshDiagnosticsInEditor(editor: TextEditor) { + if (!this.decorationTypeForIssues || !this.decorationTypeForFlagged) return; + const doc = editor.document; + const diags = this.issueTracker.getDiagnostics(doc.uri) || []; + + const decorationsIssues: DecorationOptions[] = diags + .filter((diag) => diag.severity === DiagnosticSeverity.Hint) + .filter((diag) => !diag.data?.isFlagged) + .map((diag) => diagToDecorationOptions(diag, doc)); + editor.setDecorations(this.decorationTypeForIssues, decorationsIssues); + + const decorationsFlagged: DecorationOptions[] = diags + .filter((diag) => diag.severity === DiagnosticSeverity.Hint) + .filter((diag) => diag.data?.isFlagged) + .map((diag) => diagToDecorationOptions(diag, doc)); + editor.setDecorations(this.decorationTypeForFlagged, decorationsFlagged); + } + + private clearDecoration() { + this.decorationTypeForIssues?.dispose(); + this.decorationTypeForIssues = undefined; + this.decorationTypeForFlagged?.dispose(); + this.decorationTypeForFlagged = undefined; + } + + private resetDecorator() { + const decorators = this.createDecorators(); + this.decorationTypeForIssues = decorators?.decoratorIssues; + this.decorationTypeForFlagged = decorators?.decoratorFlagged; + this.refreshDiagnostics(); + } + + private createDecorators(): { decoratorIssues: TextEditorDecorationType; decoratorFlagged: TextEditorDecorationType } | undefined { + this.clearDecoration(); + const decorateIssues = vscode.workspace.getConfiguration('cSpell').get('decorateIssues'); + if (!decorateIssues) return undefined; + + const mode = calcMode(vscode.window.activeColorTheme.kind); + const cfg = vscode.workspace.getConfiguration('cSpell') as CSpellUserSettings; + + const overviewRulerColor: string | undefined = cfg[mode]?.overviewRulerColor || cfg.overviewRulerColor || undefined; + + const decoratorIssues = vscode.window.createTextEditorDecorationType({ + isWholeLine: false, + rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, + overviewRulerLane: vscode.OverviewRulerLane.Right, + overviewRulerColor: overviewRulerColor, + textDecoration: calcTextDecoration(cfg, mode, 'textDecorationColor'), + }); + + const decoratorFlagged = vscode.window.createTextEditorDecorationType({ + isWholeLine: false, + rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, + overviewRulerLane: vscode.OverviewRulerLane.Right, + overviewRulerColor: overviewRulerColor, + textDecoration: calcTextDecoration(cfg, mode, 'textDecorationColorFlagged'), + }); + + return { decoratorIssues, decoratorFlagged }; + } +} + +function calcTextDecoration(cfg: CSpellUserSettings, mode: ColorMode, colorField: 'textDecorationColor' | 'textDecorationColorFlagged') { + const textDecoration = cfg[mode]?.textDecoration || cfg.textDecoration || ''; + const line = cfg[mode]?.textDecorationLine || cfg.textDecorationLine || 'underline'; + const style = cfg[mode]?.textDecorationStyle || cfg.textDecorationStyle || 'wavy'; + const thickness = cfg[mode]?.textDecorationThickness || cfg.textDecorationThickness || 'auto'; + const color = cfg[mode]?.[colorField] || cfg[colorField] || '#fc4'; + return textDecoration || `${line} ${style} ${color} ${thickness}`; +} + +function diagToDecorationOptions(diag: SpellingDiagnostic, doc: TextDocument): DecorationOptions { + const { range } = diag; + const { suggestions, isFlagged, isSuggestion } = diag.data || {}; + const text = doc.getText(range); + + const commandSuggest = commandUri('cSpell.suggestSpellingCorrections', doc.uri, range, text); + // const commandAdd = commandUri('cSpell.addWordToDictionary', text); + + const mdShowSuggestions = markdownLink('Suggestions $(chevron-right)', commandSuggest, 'Show suggestions.'); + + const icon = isFlagged ? '$(error)' : isSuggestion ? '$(info)' : '$(warning)'; + + const hoverMessage = new MarkdownString(icon + ' ', true); + + hoverMessage.appendMarkdown('***').appendText(text).appendMarkdown('***: '); + + if (isSuggestion) { + hoverMessage.appendText('Has Suggestions.'); + if (!suggestions?.length) { + hoverMessage.appendText(' ').appendMarkdown(mdShowSuggestions); + } + } else { + if (isFlagged) { + hoverMessage.appendText('Forbidden word.'); + } else { + hoverMessage.appendText('Unknown word.'); + } + hoverMessage.appendText(' ').appendMarkdown(mdShowSuggestions); + } + + if (suggestions?.length) { + for (const suggestion of suggestions) { + const { word } = suggestion; + const cmd = createTextEditCommand('fix', doc.uri, doc.version, [{ range, newText: word }]); + hoverMessage.appendMarkdown('\n- ' + markdownLink(word, commandUri(cmd), `Fix with ${word}`) + '\n'); + } + } + + hoverMessage.isTrusted = true; + return { range, hoverMessage }; +} + +function markdownLink(text: string, uri: string, hover?: string) { + const hoverText = hover ? ` "${hover}"` : ''; + return `[${text}](${uri}${hoverText})`; +} + +type ColorMode = 'dark' | 'light'; + +function calcMode(kind: ColorThemeKind): ColorMode { + switch (kind) { + case ColorThemeKind.Dark: + case ColorThemeKind.HighContrast: + return 'dark'; + case ColorThemeKind.HighContrastLight: + case ColorThemeKind.Light: + return 'light'; + } +} diff --git a/packages/client/src/extension.ts b/packages/client/src/extension.ts index 7b1cc44d83..efdafe890e 100644 --- a/packages/client/src/extension.ts +++ b/packages/client/src/extension.ts @@ -6,9 +6,10 @@ import * as addWords from './addWords'; import { registerCspellInlineCompletionProviders } from './autocomplete'; import { CSpellClient } from './client'; import { registerSpellCheckerCodeActionProvider } from './codeAction'; +import type { InjectableCommandHandlers } from './commands'; import * as commands from './commands'; import { updateDocumentRelatedContext } from './context'; -import { SpellingIssueDecorator } from './decorate'; +import { SpellingExclusionsDecorator, SpellingIssueDecorator } from './decorate'; import * as di from './di'; import type { ExtensionApi } from './extensionApi'; import * as ExtensionRegEx from './extensionRegEx'; @@ -69,8 +70,14 @@ export async function activate(context: ExtensionContext): Promise const configWatcher = vscode.workspace.createFileSystemWatcher(settings.configFileLocationGlob); const decorator = new SpellingIssueDecorator(issueTracker); + const decoratorExclusions = new SpellingExclusionsDecorator(context, client); + decoratorExclusions.enabled = true; activateIssueViewer(context, issueTracker, client); + const extensionCommand: InjectableCommandHandlers = { + 'cSpell.toggleTraceMode': () => decoratorExclusions.toggleEnabled(), + }; + // Push the disposable to the context's subscriptions so that the // client can be deactivated on extension deactivation context.subscriptions.push( @@ -88,9 +95,10 @@ export async function activate(context: ExtensionContext): Promise vscode.window.onDidChangeVisibleTextEditors(handleOnDidChangeVisibleTextEditors), vscode.languages.onDidChangeDiagnostics(handleOnDidChangeDiagnostics), decorator, + decoratorExclusions, registerSpellCheckerCodeActionProvider(issueTracker), - ...commands.registerCommands(), + ...commands.registerCommands(extensionCommand), /* * We need to listen for all change events and see of `cSpell` section changed.