diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.MD new file mode 100644 index 00000000..52ead98c --- /dev/null +++ b/CONTRIBUTING.MD @@ -0,0 +1,41 @@ + +# TypeScript Essential Plugins Contributing Guide + +Hi! Thank you so much for contributing to TypeScript Essential Plugins VS Code extension! We are really excited to bring high quality features and stability and we really appreciate any interest in it! +Let us give you some high-level overview for you. + +## Repo Setup + +> Quick Tip: You can use [ni](https://github.com/antfu/ni) to help switching between repos using different package managers. +> `ni` is equivalent to `pnpm install` and `nr script` is equivalent to `pnpm script` + +### Start in Development + +To start the VS Code plugin extension locally for developing: + +0. Ensure you have pnpm installed (minimum v6): `npm i -g pnpm` + +1. Run `pnpm install` in root folder + +2. Run `pnpm start` to build extension and typescript plugin in watch mode. After initial build you can open VS Code window in development by pressing F5 to **start debugging session** (or by running `> Debug: Select and Start Debugging` and selecting *Extension + TS Plugin*). + +- Note, that window will *be reloaded after each change in `src/*` automatically*. Note that each development window reload most probably cause erase of unsaved files/data. Also if you frequently change files in `src/*` you can uncomment `--disable-extensions` in launch.json for faster window reloads. + +### Files Structure Overview + +- `src/*` - VS Code extension code, that is specific to VS Code extension API only. Most probably you don't need to change it. (For now there is a limitation from vscode-framework so folder name cannot be changed to something like `extension` or `vscode`.) +- `src/configurationType.ts` - Extension configuration live here. Add / change settings here. It is used to generate `out/package.json`'s `contributes.configuration`. +- `typescript/*` - TypeScript plugin code, that integrates into TypeScript language service. After you change code in it, you need run to `> TypeScript: Restart TS server` to see changes (or `> Volar: Restart Vue server` for Vue files). Thats why it is useful to bind it to a shortcut. + +### Running Tests + +#### Unit Tests + +They are in `typescript/test` and using vitest, so they faster than integration. Feel free to add new tests here. But note that most of tests are completion tests, but I do hope to add more types tests in the future. + +To launch them run `pnpm test-plugin`. + +#### Integration Tests + +They are in `integration`. This type of tests launches VSCode. For now I don't recommend either running or adding new tests here, use unit tests. +> Note that while running this script, you must also keep `pnpm start` running in the background. However, changing a file in `src/`, won't relaunch integration tests. If this is your case, you should edit the script. diff --git a/README.MD b/README.MD index 404f70c6..0f2ee72e 100644 --- a/README.MD +++ b/README.MD @@ -102,9 +102,9 @@ This also makes plugin work in Volar's takeover mode! ### Web Support -> Note: when you open TS/JS file in the web for the first time you currently need to switch editors to make everything work! +> Note: when you open TS/JS file in the web for the first time you currently need to switch between editors to make plugin work. -Web-only feature: `import` path resolution +There is web-only feature: fix clicking on relative `import` paths. ### `in` Keyword Suggestions @@ -149,9 +149,9 @@ Appends *space* to almost all keywords e.g. `const `, like WebStorm does. (*enabled by default*) -Patches `toString()` insert function snippet on number types to remove tabStop. +Patches `toString()` insert function snippet on number types to remove annoying tab stop. -### Enforce Properties Sorting +### Restore Properties Sorting (*disabled by default*) enable with `tsEssentialPlugins.fixSuggestionsSorting` @@ -162,11 +162,7 @@ Try to restore [original](https://github.com/microsoft/TypeScript/issues/49012) We extend completion list with extensions from module augmentation (e.g. `.css` files if you have `declare module '*.css'`). But for unchecked contexts list of extensions can be extended with `tsEssentialPlugins.additionalIncludeExtensions` setting. -### Switch Exclude Covered Cases - -(*enabled by default*) - -Exclude already covered strings / enums from suggestions ([TS repo issue](https://github.com/microsoft/TypeScript/issues/13711)). + ### Mark Code Actions @@ -226,7 +222,58 @@ Some settings examples: ``` > Note: changeSorting might not preserve sorting of other existing suggestions which not defined by rules, there is WIP -> Also I'm thinking of making it learn and syncing of most-used imports automatically +> Also I'm thinking of making it learn and sync most-used imports automatically + +### Namespace Imports + +If you always want some modules to be imported automatically as namespace import, you're lucky as there is `autoImport.changeToNamespaceImport` setting for this. + +Example: + +You're completing following Node.js code in empty file: + +```ts +readFileSync +``` + +Default completion and code fix will change it to: + +```ts +import { readFileSync } from 'fs' + +readFileSync +``` + +But if you setup this setting: + +```json +"tsEssentialPlugins.autoImport.changeToNamespaceImport": { + "fs": {}, +}, +``` + +Completing the same code or accepting import code fix will result: + +```ts +import * as fs from 'fs' + +fs.readFileSync +``` + +There is also a way to specify different name for namespace or use default import instead. + +However there are cases where you have some modules injected globally in your application (e.g. global `React` variable), then you can specify *auto imports feature* to use them instead of adding an import: + +```json +"tsEssentialPlugins.autoImport.changeToNamespaceImport": { + "react": { + "namespace": "React", + "addImport": false + }, +}, +``` + +`useState` -> `React.useState` ## Rename Features diff --git a/package.json b/package.json index 8400c331..11003b4f 100644 --- a/package.json +++ b/package.json @@ -94,13 +94,14 @@ "onLanguage:vue" ], "scripts": { - "start": "vscode-framework start --skip-launching", + "start": "run-p watch-extension watch-plugin", + "watch-extension": "vscode-framework start --skip-launching", + "watch-plugin": "node buildTsPlugin.mjs --watch", "build": "tsc && tsc -p typescript --noEmit && vscode-framework build && pnpm build-plugin", "build-plugin": "node buildTsPlugin.mjs && node buildTsPlugin.mjs --browser", - "watch-plugin": "node buildTsPlugin.mjs --watch", "lint": "eslint src/**", "test": "pnpm test-plugin --run && pnpm integration-test", - "test-plugin": "vitest --globals --dir typescript/test/", + "test-plugin": "vitest --globals --dir typescript/test/ --environment ts-plugin", "integration-test": "node integration/prerun.mjs && tsc -p tsconfig.test.json && node testsOut/runTests.js", "integration-test:watch": "chokidar \"integration/**\" -c \"pnpm integration-test\" --initial", "postinstall": "patch-package" @@ -118,7 +119,9 @@ "type-fest": "^2.13.1", "typed-jsonfile": "^0.2.1", "typescript": "^4.9.3", + "vite": "^4.1.1", "vitest": "^0.26.0", + "vitest-environment-ts-plugin": "./vitest-environment-ts-plugin", "vscode-manifest": "^0.0.4" }, "pnpm": { @@ -139,6 +142,7 @@ "@zardoy/utils": "^0.0.9", "@zardoy/vscode-utils": "^0.0.47", "chai": "^4.3.6", + "change-case": "^4.1.2", "chokidar": "^3.5.3", "chokidar-cli": "^3.0.0", "delay": "^5.0.0", @@ -151,6 +155,7 @@ "lodash.throttle": "^4.1.1", "mocha": "^10.0.0", "modify-json-file": "^1.2.2", + "npm-run-all": "^4.1.5", "path-browserify": "^1.0.1", "pluralize": "github:plurals/pluralize#36f03cd2d573fa6d23e12e1529fa4627e2af74b4", "rambda": "^7.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dde3fa7..8a6cab3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,7 @@ importers: '@zardoy/utils': ^0.0.9 '@zardoy/vscode-utils': ^0.0.47 chai: ^4.3.6 + change-case: ^4.1.2 chokidar: ^3.5.3 chokidar-cli: ^3.0.0 delay: ^5.0.0 @@ -40,6 +41,7 @@ importers: lodash.throttle: ^4.1.1 mocha: ^10.0.0 modify-json-file: ^1.2.2 + npm-run-all: ^4.1.5 path-browserify: ^1.0.1 pluralize: github:plurals/pluralize#36f03cd2d573fa6d23e12e1529fa4627e2af74b4 rambda: ^7.2.1 @@ -52,7 +54,9 @@ importers: typed-jsonfile: ^0.2.1 typescript: ^4.9.3 unleashed-typescript: ^1.3.0 + vite: ^4.1.1 vitest: ^0.26.0 + vitest-environment-ts-plugin: ./vitest-environment-ts-plugin vscode-framework: ^0.0.18 vscode-manifest: ^0.0.4 vscode-uri: ^3.0.6 @@ -69,6 +73,7 @@ importers: '@zardoy/utils': 0.0.9 '@zardoy/vscode-utils': 0.0.47_ai5wishe5ovkyp5mm2oyhrbtcu chai: 4.3.6 + change-case: 4.1.2 chokidar: 3.5.3 chokidar-cli: 3.0.0 delay: 5.0.0 @@ -81,6 +86,7 @@ importers: lodash.throttle: 4.1.1 mocha: 10.0.0 modify-json-file: 1.2.2 + npm-run-all: 4.1.5 path-browserify: 1.0.1 pluralize: github.com/plurals/pluralize/36f03cd2d573fa6d23e12e1529fa4627e2af74b4 rambda: 7.2.1 @@ -105,7 +111,9 @@ importers: type-fest: 2.13.1 typed-jsonfile: 0.2.1 typescript: 4.9.3 + vite: 4.1.1_@types+node@16.11.21 vitest: 0.26.3 + vitest-environment-ts-plugin: link:vitest-environment-ts-plugin vscode-manifest: 0.0.4 typescript: @@ -404,7 +412,7 @@ packages: ignore: 4.0.6 import-fresh: 3.3.0 js-yaml: 4.1.0 - minimatch: 3.0.4 + minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -416,7 +424,7 @@ packages: dependencies: '@humanwhocodes/object-schema': 1.2.1 debug: 4.3.4 - minimatch: 3.0.4 + minimatch: 3.1.2 transitivePeerDependencies: - supports-color dev: false @@ -566,7 +574,7 @@ packages: resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} dependencies: fast-glob: 3.2.11 - minimatch: 3.0.4 + minimatch: 3.1.2 mkdirp: 1.0.4 path-browserify: 1.0.1 dev: false @@ -663,7 +671,6 @@ packages: /@types/node/16.18.3: resolution: {integrity: sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==} - dev: true /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -688,7 +695,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 16.11.21 + '@types/node': 16.18.3 dev: false optional: true @@ -1117,8 +1124,8 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.2 - get-intrinsic: 1.1.3 + es-abstract: 1.21.1 + get-intrinsic: 1.2.0 is-string: 1.0.7 dev: false @@ -1133,7 +1140,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.2 + es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 dev: false @@ -1145,6 +1152,11 @@ packages: /assertion-error/1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + /available-typed-arrays/1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1210,7 +1222,7 @@ packages: hasBin: true dependencies: quote-stream: 1.0.2 - resolve: 1.21.0 + resolve: 1.22.1 static-module: 2.2.5 through2: 2.0.5 dev: false @@ -1371,7 +1383,7 @@ packages: check-error: 1.0.2 deep-eql: 4.1.3 get-func-name: 2.0.0 - loupe: 2.3.4 + loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 dev: true @@ -1602,6 +1614,17 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: false + /cross-spawn/6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.1 + shebang-command: 1.2.0 + which: 1.3.1 + dev: false + /cross-spawn/7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1917,33 +1940,52 @@ packages: is-arrayish: 0.2.1 dev: false - /es-abstract/1.20.2: - resolution: {integrity: sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==} + /es-abstract/1.21.1: + resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} engines: {node: '>= 0.4'} dependencies: + available-typed-arrays: 1.0.5 call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 function-bind: 1.1.1 function.prototype.name: 1.1.5 get-intrinsic: 1.1.3 get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 has: 1.0.3 has-property-descriptors: 1.0.0 + has-proto: 1.0.1 has-symbols: 1.0.3 - internal-slot: 1.0.3 - is-callable: 1.2.6 + internal-slot: 1.0.5 + is-array-buffer: 3.0.1 + is-callable: 1.2.7 is-negative-zero: 2.0.2 is-regex: 1.1.4 is-shared-array-buffer: 1.0.2 is-string: 1.0.7 + is-typed-array: 1.1.10 is-weakref: 1.0.2 object-inspect: 1.12.2 object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.4.3 - string.prototype.trimend: 1.0.5 - string.prototype.trimstart: 1.0.5 + safe-regex-test: 1.0.0 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 + dev: false + + /es-set-tostringtag/2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.1.3 + has: 1.0.3 + has-tostringtag: 1.0.0 dev: false /es-shim-unscopables/1.0.0: @@ -1956,7 +1998,7 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} dependencies: - is-callable: 1.2.6 + is-callable: 1.2.7 is-date-object: 1.0.5 is-symbol: 1.0.4 dev: false @@ -2748,6 +2790,12 @@ packages: resolution: {integrity: sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==} dev: false + /for-each/0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + /foreach/2.0.5: resolution: {integrity: sha1-C+4AUBiusmDQo6865ljdATbsG5k=} dev: false @@ -2803,7 +2851,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.2 + es-abstract: 1.21.1 functions-have-names: 1.2.3 dev: false @@ -2851,6 +2899,14 @@ packages: has-symbols: 1.0.3 dev: false + /get-intrinsic/1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + dev: false + /get-stream/2.3.1: resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==} engines: {node: '>=0.10.0'} @@ -2917,7 +2973,7 @@ packages: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.0.4 + minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 @@ -2944,6 +3000,13 @@ packages: type-fest: 0.20.2 dev: false + /globalthis/1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.1.4 + dev: false + /globby/11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -2956,6 +3019,12 @@ packages: slash: 3.0.0 dev: false + /gopd/1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.1.3 + dev: false + /got/12.5.3: resolution: {integrity: sha512-8wKnb9MGU8IPGRIo+/ukTy9XLJBwDiCpIf5TVzQ9Cpol50eMTpBq2GAuDsuDIz7hTYmZgMgC1e9ydr6kSDWs3w==} engines: {node: '>=14.16'} @@ -2998,8 +3067,8 @@ packages: get-intrinsic: 1.1.3 dev: false - /has-symbols/1.0.2: - resolution: {integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==} + /has-proto/1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} dev: false @@ -3012,7 +3081,7 @@ packages: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} dependencies: - has-symbols: 1.0.2 + has-symbols: 1.0.3 dev: false /has/1.0.3: @@ -3160,17 +3229,25 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false - /internal-slot/1.0.3: - resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + /internal-slot/1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 has: 1.0.3 side-channel: 1.0.4 dev: false + /is-array-buffer/3.0.1: + resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + is-typed-array: 1.1.10 + dev: false + /is-arrayish/0.2.1: - resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: false /is-bigint/1.0.4: @@ -3201,8 +3278,8 @@ packages: builtin-modules: 3.3.0 dev: false - /is-callable/1.2.6: - resolution: {integrity: sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==} + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} dev: false @@ -3213,12 +3290,6 @@ packages: ci-info: 3.3.0 dev: true - /is-core-module/2.8.1: - resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==} - dependencies: - has: 1.0.3 - dev: false - /is-core-module/2.9.0: resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==} dependencies: @@ -3339,6 +3410,17 @@ packages: has-symbols: 1.0.3 dev: false + /is-typed-array/1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + /is-unicode-supported/0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -3408,6 +3490,10 @@ packages: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true + /json-parse-better-errors/1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: false + /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: false @@ -3594,6 +3680,16 @@ packages: resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} dev: false + /load-json-file/4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.10 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: false + /local-pkg/0.4.2: resolution: {integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==} engines: {node: '>=14'} @@ -3656,6 +3752,13 @@ packages: resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==} dependencies: get-func-name: 2.0.0 + dev: false + + /loupe/2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true /lower-case/2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3696,6 +3799,11 @@ packages: engines: {node: '>= 0.6'} dev: false + /memorystream/0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + dev: false + /merge-source-map/1.0.4: resolution: {integrity: sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=} dependencies: @@ -3765,12 +3873,12 @@ packages: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: brace-expansion: 1.1.11 + dev: false /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: false /minimatch/5.0.1: resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} @@ -3917,6 +4025,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /nice-try/1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: false + /no-case/3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: @@ -3954,6 +4066,22 @@ packages: engines: {node: '>=14.16'} dev: true + /npm-run-all/4.1.5: + resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} + engines: {node: '>= 4'} + hasBin: true + dependencies: + ansi-styles: 3.2.1 + chalk: 2.4.2 + cross-spawn: 6.0.5 + memorystream: 0.3.1 + minimatch: 3.1.2 + pidtree: 0.3.1 + read-pkg: 3.0.0 + shell-quote: 1.8.0 + string.prototype.padend: 3.1.4 + dev: false + /npm-run-path/4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -3995,7 +4123,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.2 + es-abstract: 1.21.1 dev: false /on-finished/2.3.0: @@ -4133,6 +4261,14 @@ packages: callsites: 3.1.0 dev: false + /parse-json/4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: false + /parse-json/5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -4180,6 +4316,11 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + /path-key/2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: false + /path-key/3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -4191,6 +4332,13 @@ packages: resolution: {integrity: sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==} dev: false + /path-type/3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: false + /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4218,13 +4366,19 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + /pidtree/0.3.1: + resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} + engines: {node: '>=0.10'} + hasBin: true + dev: false + /pify/2.3.0: - resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=} + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} dev: false /pify/3.0.0: - resolution: {integrity: sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=} + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} dev: false @@ -4296,8 +4450,8 @@ packages: engines: {node: '>=10.13.0'} dev: false - /postcss/8.4.19: - resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==} + /postcss/8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.4 @@ -4427,6 +4581,15 @@ packages: type-fest: 0.8.1 dev: false + /read-pkg/3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + dev: false + /read-pkg/5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} @@ -4522,15 +4685,6 @@ packages: path-is-absolute: 1.0.1 dev: false - /resolve/1.21.0: - resolution: {integrity: sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==} - hasBin: true - dependencies: - is-core-module: 2.8.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: false - /resolve/1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -4569,9 +4723,9 @@ packages: dependencies: glob: 7.2.0 - /rollup/2.79.1: - resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} - engines: {node: '>=10.0.0'} + /rollup/3.15.0: + resolution: {integrity: sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: fsevents: 2.3.2 @@ -4591,6 +4745,14 @@ packages: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: false + /safe-regex-test/1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + is-regex: 1.1.4 + dev: false + /safe-regex/2.1.1: resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} dependencies: @@ -4659,16 +4821,32 @@ packages: resolution: {integrity: sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=} dev: false + /shebang-command/1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: false + /shebang-command/2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 + /shebang-regex/1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: false + /shebang-regex/3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + /shell-quote/1.8.0: + resolution: {integrity: sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==} + dev: false + /shlex/2.1.2: resolution: {integrity: sha512-Nz6gtibMVgYeMEhUjp2KuwAgqaJA1K155dU/HuDaEJUGgnmYfVtVZah+uerVWdH8UGnyahhDCgABbYTbs254+w==} dev: true @@ -4677,7 +4855,7 @@ packages: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 object-inspect: 1.12.2 dev: false @@ -4715,7 +4893,7 @@ packages: dev: true /source-map/0.5.7: - resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=} + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} dev: false @@ -4806,20 +4984,29 @@ packages: strip-ansi: 6.0.1 dev: false - /string.prototype.trimend/1.0.5: - resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} + /string.prototype.padend/3.1.4: + resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + dev: false + + /string.prototype.trimend/1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.2 + es-abstract: 1.21.1 dev: false - /string.prototype.trimstart/1.0.5: - resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} + /string.prototype.trimstart/1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.2 + es-abstract: 1.21.1 dev: false /string_decoder/1.0.3: @@ -4952,8 +5139,8 @@ packages: resolution: {integrity: sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==} dev: true - /tinypool/0.3.0: - resolution: {integrity: sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==} + /tinypool/0.3.1: + resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} engines: {node: '>=14.0.0'} dev: true @@ -5125,6 +5312,14 @@ packages: mime-types: 2.1.34 dev: false + /typed-array-length/1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + dev: false + /typed-jsonfile/0.2.1: resolution: {integrity: sha512-8G2jqDOjg2reeq0Af3LYb1hY9bbmDYdnleAPQ6o74IPBJ5OxTjbG89TNoXGVGhy1+M16oFHmLIFfMEthQo3lYA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5315,7 +5510,7 @@ packages: pathe: 0.2.0 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 3.2.4_@types+node@16.18.3 + vite: 4.1.1_@types+node@16.18.3 transitivePeerDependencies: - '@types/node' - less @@ -5326,8 +5521,42 @@ packages: - terser dev: true - /vite/3.2.4_@types+node@16.18.3: - resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} + /vite/4.1.1_@types+node@16.11.21: + resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 16.11.21 + esbuild: 0.15.18 + postcss: 8.4.21 + resolve: 1.22.1 + rollup: 3.15.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vite/4.1.1_@types+node@16.18.3: + resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -5353,9 +5582,9 @@ packages: dependencies: '@types/node': 16.18.3 esbuild: 0.15.18 - postcss: 8.4.19 + postcss: 8.4.21 resolve: 1.22.1 - rollup: 2.79.1 + rollup: 3.15.0 optionalDependencies: fsevents: 2.3.2 dev: true @@ -5393,9 +5622,9 @@ packages: source-map: 0.6.1 strip-literal: 1.0.0 tinybench: 2.3.1 - tinypool: 0.3.0 + tinypool: 0.3.1 tinyspy: 1.0.2 - vite: 3.2.4_@types+node@16.18.3 + vite: 4.1.1_@types+node@16.18.3 vite-node: 0.26.3_@types+node@16.18.3 transitivePeerDependencies: - less @@ -5589,6 +5818,25 @@ packages: resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==} dev: false + /which-typed-array/1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + dev: false + + /which/1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/src/configurationType.ts b/src/configurationType.ts index 6af105d7..866c1d9f 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -114,6 +114,12 @@ export type Configuration = { * @default space */ 'suggestions.keywordsInsertText': 'none' | 'space' + /** + * Will be `format-short` by default in future as super useful! + * Requires symbol patch + * @default disable + */ + 'suggestions.displayImportedInfo': 'disable' | 'short-format' | 'long-format' // TODO! corrent watching! /** * Wether to enable snippets for array methods like `items.map(item => )` @@ -198,12 +204,9 @@ export type Configuration = { /** * Patterns to exclude from workspace symbol search * Example: `["**\/node_modules/**"]` - * Can gradually improve performance, will be set to node_modules by default in future * @uniqueItems true - * @default [] - * @defaultTODO ["**\/node_modules/**"] + * @default ["**\/node_modules/**"] */ - // TODO enable node_modules default when cancellationToken is properly used workspaceSymbolSearchExcludePatterns: string[] /** * @default ["fixMissingFunctionDeclaration"] @@ -322,11 +325,16 @@ export type Configuration = { * Extend TypeScript outline! * Extend outline with: * - JSX Elements - * more coming soon... - * Should be stable enough! + * - Type Alias Declarations + * Should be stable! * @default false */ patchOutline: boolean + /** + * Recommended to enable if you use `patchOutline` + * @default false + */ + 'outline.arraysTuplesNumberedItems': boolean /** * Exclude covered strings/enum cases in switch in completions * @default true @@ -435,19 +443,51 @@ export type Configuration = { /** * Advanced. Use `suggestions.ignoreAutoImports` setting if possible. * - * Packages to ignore in import all fix. + * Specify packages to ignore in *add all missing imports* fix, to ensure these packages never get imported automatically. * * TODO syntaxes /* and module#symbol unsupported (easy) * @default [] */ 'autoImport.alwaysIgnoreInImportAll': string[] + /** + * Specify here modules should be imported as namespace import. But note that imports gets processed first by `suggestions.ignoreAutoImports` anyway. + * + * @default {} + */ + 'autoImport.changeToNamespaceImport': { + [module: string]: { + /** + * Defaults to key + */ + namespace?: string + /** + * @default false + */ + useDefaultImport?: boolean + /** + * Set to `false` if module is acessible from global variable + * For now not supported in add all missing imports code action + * @default true */ + addImport?: boolean + } + } /** * Enable to display additional information about source declaration in completion's documentation * For now only displays function's body - * Requires symbol patching + * Requires symbol patch * @default false */ displayAdditionalInfoInCompletions: boolean + /** + * Wether to try to infer name for extract type / interface code action + * e.g. `let foo: { a: number }` -> `type FooType = { a: number }` + * + * Experimental and *will be enabled by default* once its: + * 1. More configurable and rename is called + * 2. Logic to avoid name conflicts + * @default false + */ + 'codeActions.extractTypeInferName': boolean } // scrapped using search editor. config: caseInsesetive, context lines: 0, regex: const fix\w+ = "[^ ]+" diff --git a/src/extension.ts b/src/extension.ts index aece59aa..7ab25dca 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,7 +4,6 @@ import { defaultJsSupersetLangs } from '@zardoy/vscode-utils/build/langs' import { extensionCtx, getExtensionSetting, getExtensionSettingId } from 'vscode-framework' import { pickObj } from '@zardoy/utils' import { watchExtensionSettings } from '@zardoy/vscode-utils/build/settings' -import { Configuration } from './configurationType' import webImports from './webImports' import { sendCommand } from './sendCommand' import { registerEmmet } from './emmet' @@ -15,6 +14,7 @@ import onCompletionAccepted from './onCompletionAccepted' import specialCommands from './specialCommands' import vueVolarSupport from './vueVolarSupport' import moreCompletions from './moreCompletions' +import { mergeSettingsFromScopes } from './mergeSettings' let isActivated = false // let erroredStatusBarItem: vscode.StatusBarItem | undefined @@ -27,7 +27,9 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted const syncConfig = () => { if (!tsApi) return console.log('sending configure request for typescript-essential-plugins') - const config = vscode.workspace.getConfiguration().get(process.env.IDS_PREFIX!) + const config: any = { ...vscode.workspace.getConfiguration().get(process.env.IDS_PREFIX!) } + // todo implement language-specific settings + mergeSettingsFromScopes(config, 'typescript', extensionCtx.extension.packageJSON) tsApi.configurePlugin('typescript-essential-plugins', config) @@ -35,7 +37,7 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted // see comment in plugin require('fs').writeFileSync( require('path').join(extensionCtx.extensionPath, './plugin-config.json'), - JSON.stringify(pickObj(config as Configuration, 'patchOutline')), + JSON.stringify(pickObj(config, 'patchOutline', 'outline')), ) } @@ -47,7 +49,11 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted vscode.workspace.onDidChangeConfiguration(async ({ affectsConfiguration }) => { if (affectsConfiguration(process.env.IDS_PREFIX!)) { syncConfig() - if (process.env.PLATFORM === 'node' && affectsConfiguration(getExtensionSettingId('patchOutline'))) { + if ( + process.env.PLATFORM === 'node' && + (affectsConfiguration(getExtensionSettingId('patchOutline')) || + affectsConfiguration(getExtensionSettingId('outline.arraysTuplesNumberedItems'))) + ) { await vscode.commands.executeCommand('typescript.restartTsServer') void vscode.window.showWarningMessage('Outline will be updated after text changes or window reload') } diff --git a/src/mergeSettings.ts b/src/mergeSettings.ts new file mode 100644 index 00000000..06bc4d0e --- /dev/null +++ b/src/mergeSettings.ts @@ -0,0 +1,42 @@ +import * as vscode from 'vscode' +import lodash from 'lodash' +import { getExtensionContributionsPrefix } from 'vscode-framework' +import { Configuration } from './configurationType' + +const settingsToIgnore = [] as Array + +export const mergeSettingsFromScopes = ( + settings: Record, + language: string, + packageJson: { contributes: { configuration: { properties: Record } } }, +) => { + const { + contributes: { + configuration: { properties }, + }, + } = packageJson + for (const [fullKey, item] of Object.entries(properties)) { + const key = fullKey.slice(getExtensionContributionsPrefix().length) + const isObject = item.type === 'object' + if ((!isObject && item.type !== 'array') || settingsToIgnore.includes(key as keyof Configuration)) { + continue + } + + const value = getConfigValueFromAllScopes(key as keyof Configuration, language, isObject ? 'object' : 'array') + lodash.set(settings, key, value) + } +} + +const getConfigValueFromAllScopes = (configKey: T, language: string, type: 'array' | 'object'): Configuration[T] => { + const values = { ...vscode.workspace.getConfiguration(process.env.IDS_PREFIX, { languageId: language }).inspect(configKey)! } + const userValueKeys = Object.keys(values).filter(key => key.endsWith('Value') && !key.startsWith('default')) + for (const key of userValueKeys) { + if (values[key] !== undefined) { + continue + } + + values[key] = type === 'array' ? [] : {} + } + + return type === 'array' ? userValueKeys.flatMap(key => values[key]) : Object.assign({}, ...userValueKeys.map(key => values[key])) +} diff --git a/src/onCompletionAccepted.ts b/src/onCompletionAccepted.ts index 6a5520fc..a4faff58 100644 --- a/src/onCompletionAccepted.ts +++ b/src/onCompletionAccepted.ts @@ -22,8 +22,11 @@ export default (tsApi: { onCompletionAccepted }) => { return } - // todo: use cleaner detection - if (typeof insertText === 'object' && typeof label === 'object' && label.detail && [': [],', ': {},', ': "",', ": '',"].includes(label.detail)) { + const isJsxAttributeStringCompletion = typeof insertText === 'object' && insertText.value.endsWith("='$1'") + const isOurObjectLiteralCompletion = + typeof insertText === 'object' && typeof label === 'object' && label.detail && [': [],', ': {},', ': "",', ": '',"].includes(label.detail) + if (isJsxAttributeStringCompletion || isOurObjectLiteralCompletion) { + // todo most probably should be controlled by quickSuggestions setting void vscode.commands.executeCommand('editor.action.triggerSuggest') return } diff --git a/typescript/src/adjustAutoImports.ts b/typescript/src/adjustAutoImports.ts index 685a04ca..0e95f965 100644 --- a/typescript/src/adjustAutoImports.ts +++ b/typescript/src/adjustAutoImports.ts @@ -39,32 +39,34 @@ const initIgnoreAutoImport = () => { // }) } -export const getIgnoreAutoImportSetting = (c: GetConfig) => { - return c('suggestions.ignoreAutoImports').map((spec): ParsedIgnoreSetting => { - const hashIndex = spec.indexOf('#') - let module = hashIndex === -1 ? spec : spec.slice(0, hashIndex) - const moduleCompare = module.endsWith('/*') ? 'startsWith' : 'strict' - if (moduleCompare === 'startsWith') { - module = module.slice(0, -'/*'.length) - } - if (hashIndex === -1) { - return { - module, - symbols: [], - isAnySymbol: true, - moduleCompare, - } - } - const symbolsString = spec.slice(hashIndex + 1) - // * (glob asterisk) is reserved for future ussage - const isAnySymbol = symbolsString === '*' +export function parseIgnoreSpec(spec: string): ParsedIgnoreSetting { + const hashIndex = spec.indexOf('#') + let module = hashIndex === -1 ? spec : spec.slice(0, hashIndex) + const moduleCompare = module.endsWith('/*') ? 'startsWith' : 'strict' + if (moduleCompare === 'startsWith') { + module = module.slice(0, -'/*'.length) + } + if (hashIndex === -1) { return { module, - symbols: isAnySymbol ? [] : symbolsString.split(','), - isAnySymbol, + symbols: [], + isAnySymbol: true, moduleCompare, } - }) + } + const symbolsString = spec.slice(hashIndex + 1) + // * (glob asterisk) is reserved for future ussage + const isAnySymbol = symbolsString === '*' + return { + module, + symbols: isAnySymbol ? [] : symbolsString.split(','), + isAnySymbol, + moduleCompare, + } +} + +export const getIgnoreAutoImportSetting = (c: GetConfig) => { + return c('suggestions.ignoreAutoImports').map(spec => parseIgnoreSpec(spec)) } export const isAutoImportEntryShouldBeIgnored = (ignoreAutoImportsSetting: ParsedIgnoreSetting[], targetModule: string, symbol: string) => { @@ -78,6 +80,17 @@ export const isAutoImportEntryShouldBeIgnored = (ignoreAutoImportsSetting: Parse return false } +export const findIndexOfAutoImportSpec = (ignoreAutoImportsSetting: ParsedIgnoreSetting[], targetModule: string, symbol: string) => { + for (const [i, { module, moduleCompare, isAnySymbol, symbols }] of ignoreAutoImportsSetting.entries()) { + const isIgnoreModule = moduleCompare === 'startsWith' ? targetModule.startsWith(module) : targetModule === module + if (!isIgnoreModule) continue + if (isAnySymbol) return i + if (!symbols.includes(symbol)) continue + return i + } + return +} + export const shouldChangeSortingOfAutoImport = (symbolName: string, c: GetConfig) => { const arr = c('autoImport.changeSorting')[symbolName] return arr && arr.length > 0 diff --git a/typescript/src/codeActions/custom/changeStringReplaceToRegex.ts b/typescript/src/codeActions/custom/changeStringReplaceToRegex.ts new file mode 100644 index 00000000..f405c6af --- /dev/null +++ b/typescript/src/codeActions/custom/changeStringReplaceToRegex.ts @@ -0,0 +1,30 @@ +import { CodeAction } from '../getCodeActions' +import escapeStringRegexp from 'escape-string-regexp' + +const nodeToSpan = (node: ts.Node): ts.TextSpan => { + const start = node.pos + (node.getLeadingTriviaWidth() ?? 0) + return { start, length: node.end - start } +} + +export default { + id: 'changeStringReplaceToRegex', + name: 'Change to Regex', + tryToApply(sourceFile, position, _range, node) { + if (!node || !position) return + // requires full explicit object selection (be aware of comma) to not be annoying with suggestion + if (!ts.isStringLiteral(node)) return + if (!ts.isCallExpression(node.parent) || node.parent.arguments[0] !== node) return + if (!ts.isPropertyAccessExpression(node.parent.expression)) return + if (node.parent.expression.name.text !== 'replace') return + // though it does to much escaping and ideally should be simplified + const edits: ts.TextChange[] = [{ span: nodeToSpan(node), newText: `/${escapeStringRegexp(node.text)}/` }] + return { + edits: [ + { + fileName: sourceFile.fileName, + textChanges: edits, + }, + ], + } + }, +} satisfies CodeAction diff --git a/typescript/src/codeActions/objectSwapKeysAndValues.ts b/typescript/src/codeActions/custom/objectSwapKeysAndValues.ts similarity index 96% rename from typescript/src/codeActions/objectSwapKeysAndValues.ts rename to typescript/src/codeActions/custom/objectSwapKeysAndValues.ts index c8adf2ef..443ac996 100644 --- a/typescript/src/codeActions/objectSwapKeysAndValues.ts +++ b/typescript/src/codeActions/custom/objectSwapKeysAndValues.ts @@ -1,5 +1,5 @@ -import { approveCast } from '../utils' -import { CodeAction } from './getCodeActions' +import { approveCast } from '../../utils' +import { CodeAction } from '../getCodeActions' const nodeToSpan = (node: ts.Node): ts.TextSpan => { const start = node.pos + (node.getLeadingTriviaWidth() ?? 0) diff --git a/typescript/src/codeActions/toggleBraces.ts b/typescript/src/codeActions/custom/toggleBraces.ts similarity index 93% rename from typescript/src/codeActions/toggleBraces.ts rename to typescript/src/codeActions/custom/toggleBraces.ts index ba674311..f8caaa2b 100644 --- a/typescript/src/codeActions/toggleBraces.ts +++ b/typescript/src/codeActions/custom/toggleBraces.ts @@ -1,6 +1,6 @@ import { Statement } from 'typescript/lib/tsserverlibrary' -import { findChildContainingPosition, findClosestParent, getIndentFromPos } from '../utils' -import { ApplyCodeAction, CodeAction } from './getCodeActions' +import { findChildContainingPosition, findClosestParent, getIndentFromPos } from '../../utils' +import { ApplyCodeAction, CodeAction } from '../getCodeActions' const tryToApply: ApplyCodeAction = (sourceFile, pos, range) => { const currentNode = findChildContainingPosition(ts, sourceFile, pos) diff --git a/typescript/src/codeActions/decorateProxy.ts b/typescript/src/codeActions/decorateProxy.ts index d94c3004..078b6c6d 100644 --- a/typescript/src/codeActions/decorateProxy.ts +++ b/typescript/src/codeActions/decorateProxy.ts @@ -1,5 +1,6 @@ import { GetConfig } from '../types' import getCodeActions, { REFACTORS_CATEGORY } from './getCodeActions' +import improveBuiltin from './improveBuiltin' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => { proxy.getApplicableRefactors = (fileName, positionOrRange, preferences) => { @@ -23,6 +24,8 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, const { edit } = getCodeActions(sourceFile, positionOrRange, actionName) return edit } - return languageService.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactorName, actionName, preferences) + const prior = languageService.getEditsForRefactor(fileName, formatOptions, positionOrRange, refactorName, actionName, preferences) + if (!prior) return + return improveBuiltin(fileName, refactorName, actionName, languageService, c, prior) ?? prior } } diff --git a/typescript/src/codeActions/getCodeActions.ts b/typescript/src/codeActions/getCodeActions.ts index 2511485b..bbab2e63 100644 --- a/typescript/src/codeActions/getCodeActions.ts +++ b/typescript/src/codeActions/getCodeActions.ts @@ -1,7 +1,8 @@ import { compact } from '@zardoy/utils' import { findChildContainingPosition } from '../utils' -import objectSwapKeysAndValues from './objectSwapKeysAndValues' -import toggleBraces from './toggleBraces' +import objectSwapKeysAndValues from './custom/objectSwapKeysAndValues' +import changeStringReplaceToRegex from './custom/changeStringReplaceToRegex' +import toggleBraces from './custom/toggleBraces' type SimplifiedRefactorInfo = | { @@ -24,7 +25,7 @@ export type CodeAction = { tryToApply: ApplyCodeAction } -const codeActions: CodeAction[] = [/* toggleBraces */ objectSwapKeysAndValues] +const codeActions: CodeAction[] = [/* toggleBraces */ objectSwapKeysAndValues, changeStringReplaceToRegex] export const REFACTORS_CATEGORY = 'essential-refactors' diff --git a/typescript/src/codeActions/improveBuiltin.ts b/typescript/src/codeActions/improveBuiltin.ts new file mode 100644 index 00000000..cbb73889 --- /dev/null +++ b/typescript/src/codeActions/improveBuiltin.ts @@ -0,0 +1,49 @@ +import { GetConfig } from '../types' +import { findChildContainingExactPosition } from '../utils' + +export default ( + fileName: string, + refactorName: string, + actionName: string, + languageService: ts.LanguageService, + c: GetConfig, + prior: ts.RefactorEditInfo, +): ts.RefactorEditInfo | undefined => { + const extractToInterface = actionName === 'Extract to interface' + if (c('codeActions.extractTypeInferName') && (actionName === 'Extract to type alias' || extractToInterface)) { + const changeFirstEdit = (oldText: string, newTypeName: string) => { + const startMarker = extractToInterface ? 'interface ' : 'type ' + const endMarker = extractToInterface ? ' {' : ' = ' + return oldText.slice(0, oldText.indexOf(startMarker) + startMarker.length) + newTypeName + oldText.slice(oldText.indexOf(endMarker)) + } + + const fileEdit = prior.edits[0]! + const { textChanges } = fileEdit + + const sourceFile = languageService.getProgram()!.getSourceFile(fileName)! + let node = findChildContainingExactPosition(sourceFile, textChanges[1]!.span.start - 1) + if (!node) return + if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) node = node.parent + if (ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isPropertyAssignment(node) || ts.isPropertySignature(node)) { + let isWithinType = ts.isPropertySignature(node) + if (ts.isIdentifier(node.name)) { + const identifierName = node.name.text + if (!identifierName) return + let typeName = identifierName[0]!.toUpperCase() + identifierName.slice(1) + if (!isWithinType) typeName += 'Type' + const newFileEdit: ts.FileTextChanges = { + fileName, + textChanges: textChanges.map((textChange, i) => { + if (i === 0) return { ...textChange, newText: changeFirstEdit(textChange.newText, typeName) } + /* if (i === 1) */ return { ...textChange, newText: typeName } + }), + } + return { + edits: [newFileEdit], + } + } + } + return prior + } + return +} diff --git a/typescript/src/codeFixes.ts b/typescript/src/codeFixes.ts index b6a22d73..bedf79a2 100644 --- a/typescript/src/codeFixes.ts +++ b/typescript/src/codeFixes.ts @@ -2,7 +2,8 @@ import _ from 'lodash' import addMissingProperties from './codeFixes/addMissingProperties' import { changeSortingOfAutoImport, getIgnoreAutoImportSetting, isAutoImportEntryShouldBeIgnored } from './adjustAutoImports' import { GetConfig } from './types' -import { findChildContainingPosition, getIndentFromPos, patchMethod } from './utils' +import { findChildContainingPosition, getCancellationToken, getIndentFromPos, patchMethod } from './utils' +import namespaceAutoImports from './namespaceAutoImports' // codeFixes that I managed to put in files const externalCodeFixes = [addMissingProperties] @@ -22,33 +23,61 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, [Diagnostics.Remove_type_from_import_of_0_from_1, 1, 0], [Diagnostics.Remove_type_from_import_declaration_from_0, 0], ] - const oldCreateCodeFixAction = tsFull.codefix.createCodeFixAction + const addNamespaceImports = [] as ts.CodeFixAction[] + let prior: readonly ts.CodeFixAction[] + let toUnpatch: (() => void)[] = [] try { const { importFixName } = tsFull.codefix const ignoreAutoImportsSetting = getIgnoreAutoImportSetting(c) const sortFn = changeSortingOfAutoImport(c, (node as ts.Identifier).text) - tsFull.codefix.createCodeFixAction = (fixName, changes, description, fixId, fixAllDescription, command) => { - if (fixName !== importFixName) return oldCreateCodeFixAction(fixName, changes, description, fixId, fixAllDescription, command) - const placeholderIndexesInfo = moduleSymbolDescriptionPlaceholders.find(([diag]) => diag === description[0]) - let sorting = '-1' - if (placeholderIndexesInfo) { - const targetModule = description[placeholderIndexesInfo[1] + 1] - const symbolName = placeholderIndexesInfo[2] !== undefined ? description[placeholderIndexesInfo[2] + 1] : (node as ts.Identifier).text - const toIgnore = isAutoImportEntryShouldBeIgnored(ignoreAutoImportsSetting, targetModule, symbolName) - if (toIgnore) { - return { - fixName: 'IGNORE', - changes: [], - description: '', + const unpatch = patchMethod( + tsFull.codefix, + 'createCodeFixAction', + oldCreateCodeFixAction => (fixName, changes, description, fixId, fixAllDescription, command) => { + if (fixName !== importFixName) return oldCreateCodeFixAction(fixName, changes, description, fixId, fixAllDescription, command) + const placeholderIndexesInfo = moduleSymbolDescriptionPlaceholders.find(([diag]) => diag === description[0]) + let sorting = '-1' + if (placeholderIndexesInfo) { + const targetModule = description[placeholderIndexesInfo[1] + 1] + const symbolName = placeholderIndexesInfo[2] !== undefined ? description[placeholderIndexesInfo[2] + 1] : (node as ts.Identifier).text + + const toIgnore = isAutoImportEntryShouldBeIgnored(ignoreAutoImportsSetting, targetModule, symbolName) + + const namespaceImportAction = + !toIgnore && namespaceAutoImports(c, sourceFile, targetModule, preferences, formatOptions, start, symbolName) + + if (namespaceImportAction) { + const { textChanges, description } = namespaceImportAction + addNamespaceImports.push({ + fixName: importFixName, + fixAllDescription: 'Add all missing imports', + fixId: 'fixMissingImport', + description, + changes: [ + { + fileName, + textChanges, + }, + ], + }) + } + if (toIgnore /* || namespaceImportAction */) { + return { + fixName: 'IGNORE', + changes: [], + description: '', + } } + sorting = sortFn(targetModule).toString() + // todo this workaround is weird, sort in another way } - sorting = sortFn(targetModule).toString() - // todo this workaround is weird, sort in another way - } - return oldCreateCodeFixAction(fixName + sorting, changes, description, fixId, fixAllDescription, command) - } + return oldCreateCodeFixAction(fixName + sorting, changes, description, fixId, fixAllDescription, command) + }, + ) + toUnpatch.push(unpatch) prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences) + prior = [...addNamespaceImports, ...prior] prior = _.sortBy(prior, ({ fixName }) => { if (fixName.startsWith(importFixName)) { return +fixName.slice(importFixName.length) @@ -63,7 +92,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, throw err }) } finally { - tsFull.codefix.createCodeFixAction = oldCreateCodeFixAction + toUnpatch.forEach(x => x()) } // todo remove when 5.0 is released after 3 months // #region fix builtin codefixes/refactorings @@ -150,12 +179,8 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost as any /* cancellationToken */, ) const semanticDiagnostics = languageService.getSemanticDiagnostics(fileName) - const cancellationToken = languageServiceHost.getCompilerHost?.()?.getCancellationToken?.() ?? { - isCancellationRequested: () => false, - throwIfCancellationRequested: () => {}, - } const context: Record = { - cancellationToken, + cancellationToken: getCancellationToken(languageServiceHost), formatContext: tsFull.formatting.getFormatContext(formatOptions, languageServiceHost), host: languageServiceHost, preferences, @@ -164,6 +189,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, } const errorCodes = getFixAllErrorCodes() const ignoreAutoImportsSetting = getIgnoreAutoImportSetting(c) + const additionalTextChanges: ts.TextChange[] = [] for (const diagnostic of semanticDiagnostics) { if (!errorCodes.includes(diagnostic.code)) continue const toUnpatch: (() => any)[] = [] @@ -185,6 +211,41 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, }), ({ fix }) => sortFn(fix.moduleSpecifier), ) + const firstFix = fixes[0] + const namespaceImportAction = + !!firstFix && + namespaceAutoImports( + c, + sourceFile, + firstFix.fix.moduleSpecifier, + preferences, + formatOptions, + diagnostic.start!, + firstFix.symbolName, + undefined, + true, + ) + if (namespaceImportAction) { + fixes = [] + if (!namespaceImportAction.namespace) { + additionalTextChanges.push(...namespaceImportAction.textChanges) + } else { + const { namespace, useDefaultImport, textChanges } = namespaceImportAction + additionalTextChanges.push(textChanges[1]!) + fixes.unshift({ + ...fixes[0]!, + fix: { + kind: ImportFixKind.AddNew, + moduleSpecifier: firstFix.fix.moduleSpecifier, + importKind: useDefaultImport ? tsFull.ImportKind.Default : tsFull.ImportKind.Namespace, + addAsTypeOnly: false, + useRequire: false, + }, + symbolName: namespace, + } as FixInfo) + } + } + if (!fixes[0]) throw new Error('No fixes') return fixes[0] }) as any, ), @@ -196,14 +257,22 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, }, ), ) - importAdder.addImportFromDiagnostic({ ...diagnostic, file: sourceFile as any } as any, context) + try { + importAdder.addImportFromDiagnostic({ ...diagnostic, file: sourceFile as any } as any, context) + } catch (err) { + if (err.message === 'No fixes') continue + throw err + } } finally { for (const unpatch of toUnpatch) { unpatch() } } } - return tsFull.codefix.createCombinedCodeActions(tsFull.textChanges.ChangeTracker.with(context, importAdder.writeFixes)) + return tsFull.codefix.createCombinedCodeActions([ + ...tsFull.textChanges.ChangeTracker.with(context, importAdder.writeFixes), + { fileName, textChanges: additionalTextChanges }, + ]) } return languageService.getCombinedCodeFix(scope, fixId, formatOptions, preferences) } diff --git a/typescript/src/completions/additionalTypesSuggestions.ts b/typescript/src/completions/additionalTypesSuggestions.ts index acd44aaa..ad19699e 100644 --- a/typescript/src/completions/additionalTypesSuggestions.ts +++ b/typescript/src/completions/additionalTypesSuggestions.ts @@ -1,7 +1,33 @@ -export default (entries: ts.CompletionEntry[], program: ts.Program, leftNode: ts.Node) => { - if (!ts.isStringLiteral(leftNode) || !ts.isTypeParameterDeclaration(leftNode.parent.parent) || leftNode.parent.parent.default !== leftNode.parent) return +export default (entries: ts.CompletionEntry[], program: ts.Program, node: ts.Node) => { + if (!ts.isStringLiteral(node)) return const typeChecker = program.getTypeChecker() - const { constraint } = leftNode.parent.parent + if (ts.isLiteralTypeNode(node.parent)) { + node = node.parent + const previousValues: string[] = [] + if (node.parent.kind === ts.SyntaxKind.UnionType) { + ;(node.parent as ts.UnionTypeNode).types.forEach(type => { + if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) { + previousValues.push(type.literal.text) + } + }) + node = node.parent + } + if (ts.isTypeReferenceNode(node.parent) && node.parent.typeName.getText() === 'Omit' && node.parent.typeArguments?.[1] === node) { + // Omit<..., '|'> suggestions + const type = typeChecker.getTypeFromTypeNode(node.parent.typeArguments[0]!) + type.getProperties().forEach(({ name }) => { + if (previousValues.includes(name)) return + entries.push({ + name, + kind: ts.ScriptElementKind.string, + sortText: '', + }) + }) + return + } + } + if (!ts.isTypeParameterDeclaration(node.parent) || node.parent.default !== node) return + const { constraint } = node.parent if (!constraint) return const type = typeChecker.getTypeAtLocation(constraint) if (!(type.flags & ts.TypeFlags.Union)) return diff --git a/typescript/src/completions/ignoreAutoImports.ts b/typescript/src/completions/adjustAutoImports.ts similarity index 87% rename from typescript/src/completions/ignoreAutoImports.ts rename to typescript/src/completions/adjustAutoImports.ts index 98de176a..b5926b8e 100644 --- a/typescript/src/completions/ignoreAutoImports.ts +++ b/typescript/src/completions/adjustAutoImports.ts @@ -1,16 +1,17 @@ -import _ from 'lodash' import { changeSortingOfAutoImport, getIgnoreAutoImportSetting, isAutoImportEntryShouldBeIgnored, shouldChangeSortingOfAutoImport } from '../adjustAutoImports' -import { GetConfig } from '../types' import { sortBy } from 'rambda' +import { sharedCompletionContext } from './sharedContext' + +export default (entries: ts.CompletionEntry[]) => { + const { c } = sharedCompletionContext -export default (entries: ts.CompletionEntry[], languageService: ts.LanguageService, c: GetConfig) => { const ignoreAutoImportsSetting = getIgnoreAutoImportSetting(c) const ignoreKinds = [ts.ScriptElementKind.warning, ts.ScriptElementKind.string] entries = entries.filter(({ sourceDisplay, name, kind }) => { if (!sourceDisplay || ignoreKinds.includes(kind)) return true - const targetModule = ts.displayPartsToString(sourceDisplay) - const toIgnore = isAutoImportEntryShouldBeIgnored(ignoreAutoImportsSetting, targetModule, name) + const importModule = ts.displayPartsToString(sourceDisplay) + const toIgnore = isAutoImportEntryShouldBeIgnored(ignoreAutoImportsSetting, importModule, name) return !toIgnore }) // todo I'm not sure of incomplete completion (wasnt tested) @@ -36,5 +37,6 @@ export default (entries: ts.CompletionEntry[], languageService: ts.LanguageServi // final one seems to be slow, e.g. it might be slowing down completions entries.splice(i, 0, ...sortBy(({ sourceDisplay }) => sortFn(ts.displayPartsToString(sourceDisplay)), entriesToSort)) } + return entries } diff --git a/typescript/src/completions/displayImportedInfo.ts b/typescript/src/completions/displayImportedInfo.ts new file mode 100644 index 00000000..3fa6b059 --- /dev/null +++ b/typescript/src/completions/displayImportedInfo.ts @@ -0,0 +1,32 @@ +import { sharedCompletionContext } from './sharedContext' + +export default (entries: ts.CompletionEntry[]) => { + const { c, prevCompletionsMap } = sharedCompletionContext + + const displayImportedInfo = c('suggestions.displayImportedInfo') + if (displayImportedInfo === 'disable') return + + for (const entry of entries) { + const symbol = entry['symbol'] as ts.Symbol + if (!symbol) continue + const [node] = symbol.getDeclarations() ?? [] + if (!node) continue + let importDeclaration: ts.ImportDeclaration | undefined + if (ts.isImportSpecifier(node) && ts.isNamedImports(node.parent) && ts.isImportDeclaration(node.parent.parent.parent)) { + importDeclaration = node.parent.parent.parent + } else if (ts.isImportClause(node) && ts.isImportDeclaration(node.parent)) { + importDeclaration = node.parent + } else if (ts.isNamespaceImport(node) && ts.isImportClause(node.parent) && ts.isImportDeclaration(node.parent.parent)) { + // todo-low(builtin) maybe reformat text + importDeclaration = node.parent.parent + } + if (importDeclaration) { + prevCompletionsMap[entry.name] ??= {} + let importPath = importDeclaration.moduleSpecifier.getText() + const symbolsLimit = 40 + if (importPath.length > symbolsLimit) importPath = importPath.slice(0, symbolsLimit / 2) + '...' + importPath.slice(-symbolsLimit / 2) + const detailPrepend = displayImportedInfo === 'short-format' ? `(from ${importPath}) ` : `Imported from ${importPath}\n\n` + prevCompletionsMap[entry.name]!.detailPrepend = detailPrepend + } + } +} diff --git a/typescript/src/completions/fixPropertiesSorting.ts b/typescript/src/completions/fixPropertiesSorting.ts index 9733e155..8b6f24bb 100644 --- a/typescript/src/completions/fixPropertiesSorting.ts +++ b/typescript/src/completions/fixPropertiesSorting.ts @@ -1,7 +1,11 @@ import { oneOf } from '@zardoy/utils' -import { groupBy, partition } from 'rambda' +import { partition } from 'rambda' +import { getAllPropertiesOfType } from './objectLiteralCompletions' +import { sharedCompletionContext } from './sharedContext' -export default (entries: ts.CompletionEntry[], node: ts.Node | undefined, sourceFile: ts.SourceFile, program: ts.Program) => { +export default (entries: ts.CompletionEntry[]) => { + const { node, program, c } = sharedCompletionContext + if (!c('fixSuggestionsSorting')) return if (!node) return // if (ts.isObjectLiteralExpression(node) && ts.isCallExpression(node.parent)) { // const typeChecker = program.getTypeChecker() @@ -10,6 +14,7 @@ export default (entries: ts.CompletionEntry[], node: ts.Node | undefined, source // } let rightNode: ts.Node | undefined const upperNode = ts.isIdentifier(node) ? node.parent : node + if (ts.isObjectLiteralExpression(node)) rightNode = node if (ts.isPropertyAccessExpression(upperNode)) rightNode = upperNode.expression else if (ts.isObjectBindingPattern(node)) { if (ts.isVariableDeclaration(node.parent)) { @@ -25,8 +30,8 @@ export default (entries: ts.CompletionEntry[], node: ts.Node | undefined, source } if (!rightNode) return const typeChecker = program.getTypeChecker() - const type = typeChecker.getTypeAtLocation(rightNode) - const sourceProps = type.getProperties?.()?.map(({ name }) => name) + const type = typeChecker.getContextualType(rightNode as ts.Expression) ?? typeChecker.getTypeAtLocation(rightNode) + const sourceProps = getAllPropertiesOfType(type, typeChecker)?.map(({ name }) => name) // languageService.getSignatureHelpItems(fileName, position, {})) if (!sourceProps) return // const entriesBySortText = groupBy(({ sortText }) => sortText, entries) diff --git a/typescript/src/completions/objectLiteralCompletions.ts b/typescript/src/completions/objectLiteralCompletions.ts index ca52f7db..48e061f1 100644 --- a/typescript/src/completions/objectLiteralCompletions.ts +++ b/typescript/src/completions/objectLiteralCompletions.ts @@ -20,14 +20,13 @@ export default ( const typeChecker = languageService.getProgram()!.getTypeChecker()! const objType = typeChecker.getContextualType(node) if (!objType) return - // its doesn't return all actual properties in some cases e.g. it would be more correct to use symbols from entries, but there is a block from TS - const properties = objType.getProperties() + const properties = getAllPropertiesOfType(objType, typeChecker) for (const property of properties) { const entry = entries.find(({ name }) => name === property.name) if (!entry) continue const type = typeChecker.getTypeOfSymbolAtLocation(property, node) if (!type) continue - if (isMethodCompletionCall(type, typeChecker)) { + if (isFunctionType(type, typeChecker)) { if (['above', 'remove'].includes(keepOriginal) && preferences.includeCompletionsWithObjectLiteralMethodSnippets) { const methodEntryIndex = entries.findIndex(e => e.name === entry.name && isObjectLiteralMethodSnippet(e)) const methodEntry = entries[methodEntryIndex] @@ -50,6 +49,7 @@ export default ( const insertObjectArrayInnerText = c('objectLiteralCompletions.insertNewLine') ? '\n\t$1\n' : '$1' const completingStyleMap = [ [getQuotedSnippet, isStringCompletion], + [[': ${1|true,false|},$0', `: true/false,`], isBooleanCompletion], [[`: [${insertObjectArrayInnerText}],$0`, `: [],`], isArrayCompletion], [[`: {${insertObjectArrayInnerText}},$0`, `: {},`], isObjectCompletion], ] as const @@ -79,28 +79,44 @@ const isObjectLiteralMethodSnippet = (entry: ts.CompletionEntry) => { return detail?.startsWith('(') && detail.split('\n')[0]!.trimEnd().endsWith(')') } -const isMethodCompletionCall = (type: ts.Type, checker: ts.TypeChecker) => { +const isFunctionType = (type: ts.Type, checker: ts.TypeChecker) => { if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) return true - if (type.isUnion()) return type.types.some(type => isMethodCompletionCall(type, checker)) + if (type.isUnion()) return type.types.some(type => isFunctionType(type, checker)) +} + +const isEverySubtype = (type: ts.UnionType, predicate: (type: ts.Type) => boolean): boolean => { + // union cannot consist of only undefined types + return type.types.every(type => { + if (type.flags & ts.TypeFlags.Undefined) return true + return predicate(type) + }) } const isStringCompletion = (type: ts.Type) => { - if (type.flags & ts.TypeFlags.Undefined) return true + if (type.flags & ts.TypeFlags.Undefined) return false if (type.flags & ts.TypeFlags.StringLike) return true - if (type.isUnion()) return type.types.every(type => isStringCompletion(type)) + if (type.isUnion()) return isEverySubtype(type, type => isStringCompletion(type)) + return false +} + +const isBooleanCompletion = (type: ts.Type) => { + if (type.flags & ts.TypeFlags.Undefined) return false + // todo support boolean literals (boolean like) + if (type.flags & ts.TypeFlags.Boolean) return true + if (type.isUnion()) return isEverySubtype(type, type => isBooleanCompletion(type)) return false } const isArrayCompletion = (type: ts.Type, checker: ts.TypeChecker) => { if (type.flags & ts.TypeFlags.Any) return false - if (type.flags & ts.TypeFlags.Undefined) return true + if (type.flags & ts.TypeFlags.Undefined) return false if (checker['isArrayLikeType'](type)) return true - if (type.isUnion()) return type.types.every(type => isArrayCompletion(type, checker)) + if (type.isUnion()) return isEverySubtype(type, type => isArrayCompletion(type, checker)) return false } const isObjectCompletion = (type: ts.Type, checker: ts.TypeChecker) => { - if (type.flags & ts.TypeFlags.Undefined) return true + if (type.flags & ts.TypeFlags.Undefined) return false if (checker['isArrayLikeType'](type)) return false if (type.flags & ts.TypeFlags.Object) { if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Class) return false @@ -108,6 +124,26 @@ const isObjectCompletion = (type: ts.Type, checker: ts.TypeChecker) => { if (type.symbol?.escapedName === 'RegExp') return false return true } - if (type.isUnion()) return type.types.every(type => isObjectCompletion(type, checker)) + if (type.isUnion()) return isEverySubtype(type, type => isObjectCompletion(type, checker)) return false } + +export const getAllPropertiesOfType = (type: ts.Type, typeChecker: ts.TypeChecker) => { + const types = type.isUnion() ? type.types : [type] + let objectCount = 0 + const properties = types + .flatMap(type => { + if (isFunctionType(type, typeChecker)) return [] + if (isObjectCompletion(type, typeChecker)) { + objectCount++ + return typeChecker.getPropertiesOfType(type) + } + return [] + }) + .filter((property, i, arr) => { + // perf + if (objectCount === 1) return true + return !arr.find(({ name }, k) => name === property.name && i !== k) + }) + return properties +} diff --git a/typescript/src/completions/objectLiteralHelpers.ts b/typescript/src/completions/objectLiteralHelpers.ts index 171227ce..271cca6b 100644 --- a/typescript/src/completions/objectLiteralHelpers.ts +++ b/typescript/src/completions/objectLiteralHelpers.ts @@ -5,7 +5,7 @@ export default (node: ts.Node, entries: ts.CompletionEntry[]): ts.CompletionEntr if (ts.isObjectLiteralExpression(node) && isArrayLike(entries)) { return [ { - name: '(array)', + name: '', kind: ts.ScriptElementKind.label, sortText: '07', insertText: '[]', diff --git a/typescript/src/completions/sharedContext.ts b/typescript/src/completions/sharedContext.ts new file mode 100644 index 00000000..f1e481fd --- /dev/null +++ b/typescript/src/completions/sharedContext.ts @@ -0,0 +1,17 @@ +import { PrevCompletionMap } from '../completionsAtPosition' +import { GetConfig } from '../types' + +/** Must be used within functions */ +export const sharedCompletionContext = {} as unknown as Readonly<{ + position: number + sourceFile: ts.SourceFile + program: ts.Program + node: ts.Node | undefined + languageService: ts.LanguageService + isCheckedFile: boolean + prevCompletionsMap: PrevCompletionMap + c: GetConfig + formatOptions: ts.FormatCodeSettings + preferences: ts.UserPreferences + // languageServiceHost: ts.LanguageServiceHost +}> diff --git a/typescript/src/completionsAtPosition.ts b/typescript/src/completionsAtPosition.ts index 07944516..3bb84e51 100644 --- a/typescript/src/completionsAtPosition.ts +++ b/typescript/src/completionsAtPosition.ts @@ -3,7 +3,7 @@ import inKeywordCompletions from './inKeywordCompletions' // import * as emmet from '@vscode/emmet-helper' import isInBannedPosition from './completions/isInBannedPosition' import { GetConfig } from './types' -import { findChildContainingExactPosition, findChildContainingPosition } from './utils' +import { findChildContainingExactPosition, findChildContainingPosition, isTs5 } from './utils' import indexSignatureAccessCompletions from './completions/indexSignatureAccess' import fixPropertiesSorting from './completions/fixPropertiesSorting' import { isGoodPositionBuiltinMethodCompletion } from './completions/isGoodPositionMethodCompletion' @@ -21,15 +21,34 @@ import objectLiteralCompletions from './completions/objectLiteralCompletions' import filterJsxElements from './completions/filterJsxComponents' import markOrRemoveGlobalCompletions from './completions/markOrRemoveGlobalLibCompletions' import { compact, oneOf } from '@zardoy/utils' -import filterWIthIgnoreAutoImports from './completions/ignoreAutoImports' +import adjustAutoImports from './completions/adjustAutoImports' import escapeStringRegexp from 'escape-string-regexp' import addSourceDefinition from './completions/addSourceDefinition' +import { sharedCompletionContext } from './completions/sharedContext' +import displayImportedInfo from './completions/displayImportedInfo' -export type PrevCompletionMap = Record +export type PrevCompletionMap = Record< + string, + { + originalName?: string + /** use only if codeactions cant be returned (no source) */ + documentationOverride?: string | ts.SymbolDisplayPart[] + detailPrepend?: string + documentationAppend?: string + // textChanges?: ts.TextChange[] + } +> export type PrevCompletionsAdditionalData = { enableMethodCompletion: boolean } +type GetCompletionAtPositionReturnType = { + completions: ts.CompletionInfo + /** Let default getCompletionEntryDetails to know original name or let add documentation from here */ + prevCompletionsMap: PrevCompletionMap + prevCompletionsAdittionalData: PrevCompletionsAdditionalData +} + export const getCompletionsAtPosition = ( fileName: string, position: number, @@ -39,14 +58,7 @@ export const getCompletionsAtPosition = ( scriptSnapshot: ts.IScriptSnapshot, formatOptions: ts.FormatCodeSettings | undefined, additionalData: { scriptKind: ts.ScriptKind; compilerOptions: ts.CompilerOptions }, -): - | { - completions: ts.CompletionInfo - /** Let default getCompletionEntryDetails to know original name or let add documentation from here */ - prevCompletionsMap: PrevCompletionMap - prevCompletionsAdittionalData: PrevCompletionsAdditionalData - } - | undefined => { +): GetCompletionAtPositionReturnType | undefined => { const prevCompletionsMap: PrevCompletionMap = {} const program = languageService.getProgram() const sourceFile = program?.getSourceFile(fileName) @@ -55,12 +67,24 @@ export const getCompletionsAtPosition = ( const exactNode = findChildContainingExactPosition(sourceFile, position) const isCheckedFile = !tsFull.isSourceFileJS(sourceFile as any) || !!tsFull.isCheckJsEnabledForFile(sourceFile as any, additionalData.compilerOptions as any) + Object.assign(sharedCompletionContext, { + position, + languageService, + sourceFile, + program, + isCheckedFile, + node: exactNode, + prevCompletionsMap, + c, + formatOptions: formatOptions || {}, + preferences: options || {}, + } satisfies typeof sharedCompletionContext) const unpatch = patchBuiltinMethods(c, languageService, isCheckedFile) const getPrior = () => { try { return languageService.getCompletionsAtPosition(fileName, position, options, formatOptions) } finally { - unpatch() + unpatch?.() } } let prior = getPrior() @@ -96,8 +120,8 @@ export const getCompletionsAtPosition = ( } // #endregion } - if (leftNode && !hasSuggestions && ensurePrior() && prior) { - prior.entries = additionalTypesSuggestions(prior.entries, program, leftNode) ?? prior.entries + if (node && !hasSuggestions && ensurePrior() && prior) { + prior.entries = additionalTypesSuggestions(prior.entries, program, node) ?? prior.entries } const addSignatureAccessCompletions = hasSuggestions ? [] : indexSignatureAccessCompletions(position, node, scriptSnapshot, sourceFile, program) if (addSignatureAccessCompletions.length && ensurePrior() && prior) { @@ -145,7 +169,7 @@ export const getCompletionsAtPosition = ( } } - if (c('fixSuggestionsSorting')) prior.entries = fixPropertiesSorting(prior.entries, leftNode, sourceFile, program) ?? prior.entries + prior.entries = fixPropertiesSorting(prior.entries) ?? prior.entries if (node) prior.entries = boostKeywordSuggestions(prior.entries, position, node) ?? prior.entries const entryNames = new Set(prior.entries.map(({ name }) => name)) @@ -194,7 +218,7 @@ export const getCompletionsAtPosition = ( if (node) prior.entries = defaultHelpers(prior.entries, node, languageService) ?? prior.entries if (exactNode) prior.entries = objectLiteralCompletions(prior.entries, exactNode, languageService, options ?? {}, c) ?? prior.entries // 90% - prior.entries = filterWIthIgnoreAutoImports(prior.entries, languageService, c) + prior.entries = adjustAutoImports(prior.entries) const inKeywordCompletionsResult = inKeywordCompletions(position, node, sourceFile, program, languageService) if (inKeywordCompletionsResult) { @@ -233,6 +257,7 @@ export const getCompletionsAtPosition = ( // #endregion prior.entries = addSourceDefinition(prior.entries, prevCompletionsMap, c) ?? prior.entries + displayImportedInfo(prior.entries) if (c('improveJsxCompletions') && leftNode) prior.entries = improveJsxCompletions(prior.entries, leftNode, position, sourceFile, c('jsxCompletionsMap')) @@ -341,6 +366,8 @@ const arrayMoveItemToFrom = (array: T[], originalItem: ArrayPredicate, ite } const patchBuiltinMethods = (c: GetConfig, languageService: ts.LanguageService, isCheckedFile: boolean) => { + if (isTs5() && (isCheckedFile || !c('additionalIncludeExtensions').length)) return + let addFileExtensions: string[] | undefined const getAddFileExtensions = () => { const typeChecker = languageService.getProgram()!.getTypeChecker()! diff --git a/typescript/src/decorateFormatFeatures.ts b/typescript/src/decorateFormatFeatures.ts index c897c069..75570f8f 100644 --- a/typescript/src/decorateFormatFeatures.ts +++ b/typescript/src/decorateFormatFeatures.ts @@ -1,7 +1,6 @@ import { GetConfig } from './types' -import { patchMethod } from './utils' -export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => { +export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => { // todo: add our formatting rules! // tsFull.formatting.getAllRules @@ -13,8 +12,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, if (line.startsWith(expected)) return true return false } - const isFormattingLineIgnored = (sourceFile: ts.SourceFile, position: number) => { - const fullText = sourceFile.getFullText() + const isFormattingLineIgnored = (fullText: string, position: number) => { // check that lines before line are not ignored const linesBefore = fullText.slice(0, position).split('\n') if (isExpectedDirective(linesBefore[linesBefore.length - 2], '@ts-format-ignore-line')) { @@ -32,20 +30,24 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, } return isInsideIgnoredRegion } - const toPatchFormatMethods = ['formatSelection', 'formatOnOpeningCurly', 'formatOnClosingCurly', 'formatOnSemicolon', 'formatOnEnter'] - for (const toPatchFormatMethod of toPatchFormatMethods) { - patchMethod(tsFull.formatting, toPatchFormatMethod as any, oldFn => (...args) => { - const result = oldFn(...args) - // arg position depends on the method, so we need to find it - const sourceFile = args.find(arg => ts.isSourceFile(arg as any)) - return result.filter(({ span }) => { - if (isFormattingLineIgnored(sourceFile as ts.SourceFile, span.start)) { + + for (const method of [ + 'getFormattingEditsForDocument', + 'getFormattingEditsForRange', + 'getFormattingEditsAfterKeystroke', + ] satisfies (keyof ts.LanguageService)[]) { + proxy[method] = (...args) => { + const textChanges: ts.TextChange[] = (languageService[method] as any)(...args) + const fileName = args[0] + const scriptSnapshot = languageServiceHost.getScriptSnapshot(fileName)! + const fileContent = scriptSnapshot.getText(0, scriptSnapshot.getLength()) + + return textChanges.filter(({ span }) => { + if (isFormattingLineIgnored(fileContent, span.start)) { return false } return true }) - }) + } } - // we could easily patch languageService methods getFormattingEditsForDocument, getFormattingEditsAfterKeystroke and getFormattingEditsForRange - // but since formatting happens in syntax server, we don't have access to actual sourceFile, so we can't provide implementation } diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index 8ae8aa71..264db17a 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -13,6 +13,7 @@ import { GetConfig } from './types' import lodashGet from 'lodash.get' import decorateWorkspaceSymbolSearch from './workspaceSymbolSearch' import decorateFormatFeatures from './decorateFormatFeatures' +import namespaceAutoImports from './namespaceAutoImports' /** @internal */ export const thisPluginMarker = '__essentialPluginsMarker__' @@ -82,7 +83,7 @@ export const decorateLanguageService = ( const program = languageService.getProgram() const sourceFile = program?.getSourceFile(fileName) if (!program || !sourceFile) return - const { documentationOverride, documentationAppend } = prevCompletionsMap[entryName] ?? {} + const { documentationOverride, documentationAppend, detailPrepend } = prevCompletionsMap[entryName] ?? {} if (documentationOverride) { return { name: entryName, @@ -101,6 +102,38 @@ export const decorateLanguageService = ( data, ) if (!prior) return + if (source) { + const namespaceImport = namespaceAutoImports( + c, + languageService.getProgram()!.getSourceFile(fileName)!, + source, + preferences ?? {}, + formatOptions ?? {}, + position, + entryName, + prior, + ) + if (namespaceImport) { + const { textChanges, description } = namespaceImport + const namespace = textChanges[0]!.newText.slice(0, -1) + // todo-low think of cleanin up builtin code actions descriptions + prior.codeActions = [ + // ...(prior.codeActions ?? []), + { + description: description, + changes: [ + { + fileName, + textChanges, + }, + ], + }, + ] + } + } + if (detailPrepend) { + prior.displayParts = [{ kind: 'text', text: detailPrepend }, ...prior.displayParts] + } if (documentationAppend) { prior.documentation = [...(prior.documentation ?? []), { kind: 'text', text: documentationAppend }] } @@ -114,7 +147,7 @@ export const decorateLanguageService = ( decorateReferences(proxy, languageService, c) decorateDocumentHighlights(proxy, languageService, c) decorateWorkspaceSymbolSearch(proxy, languageService, c, languageServiceHost) - decorateFormatFeatures(proxy, languageService, c) + decorateFormatFeatures(proxy, languageService, languageServiceHost, c) proxy.findRenameLocations = (fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) => { if (overrideRequestPreferences.rename) { try { @@ -139,7 +172,7 @@ export const decorateLanguageService = ( // so we forced to communicate via fs const config = JSON.parse(ts.sys.readFile(require('path').join(__dirname, '../../plugin-config.json'), 'utf8') ?? '{}') proxy.getNavigationTree = fileName => { - if (c('patchOutline') || config.patchOutline) return getNavTreeItems(info, fileName) + if (c('patchOutline') || config.patchOutline) return getNavTreeItems(languageService, languageServiceHost, fileName, config.outline) return languageService.getNavigationTree(fileName) } } diff --git a/typescript/src/dummyLanguageService.ts b/typescript/src/dummyLanguageService.ts index 85c3b812..f32272b2 100644 --- a/typescript/src/dummyLanguageService.ts +++ b/typescript/src/dummyLanguageService.ts @@ -1,11 +1,11 @@ // only for basic testing, as vscode is actually using server import { nodeModules } from './utils' -export const createLanguageService = (files: Record, { useLib = true }: { useLib?: boolean } = {}) => { +export const createLanguageService = (files: Record, { useLib = true }: { useLib?: boolean } = {}, entrypoint?: string) => { const path = nodeModules!.path let dummyVersion = 1 let defaultLibDir: string | undefined - const languageService = ts.createLanguageService({ + const languageServiceHost: ts.LanguageServiceHost = { getProjectVersion: () => dummyVersion.toString(), getScriptVersion: () => dummyVersion.toString(), getCompilationSettings: () => ({ allowJs: true, jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.ESNext }), @@ -31,11 +31,25 @@ export const createLanguageService = (files: Record, { useLib = readFile(path) { return files[path]! }, - }) + } + const languageService = ts.createLanguageService(languageServiceHost) return { languageService, - updateProject() { + languageServiceHost, + updateProject(newFiles?: Record | string) { + if (newFiles) { + if (typeof newFiles === 'string') { + if (!entrypoint) throw new Error('entrypoint not set') + files = { [entrypoint!]: newFiles } + } else { + Object.assign(files, newFiles) + } + } dummyVersion++ }, + getCurrentFile() { + if (!entrypoint) throw new Error('entrypoint not set') + return files[entrypoint!]! + }, } } diff --git a/typescript/src/getPatchedNavTree.ts b/typescript/src/getPatchedNavTree.ts index 450fd093..899fdd31 100644 --- a/typescript/src/getPatchedNavTree.ts +++ b/typescript/src/getPatchedNavTree.ts @@ -1,12 +1,13 @@ -import { nodeModules } from './utils' -import * as semver from 'semver' +import { getCancellationToken, isTs5, nodeModules } from './utils' import { createLanguageService } from './dummyLanguageService' import { getCannotFindCodes } from './utils/cannotFindCodes' // used at testing only declare const __TS_SEVER_PATH__: string | undefined -const getPatchedNavModule = (): { getNavigationTree(...args) } => { +type AdditionalFeatures = Record<'arraysTuplesNumberedItems', boolean> + +const getPatchedNavModule = (additionalFeatures: AdditionalFeatures): { getNavigationTree(...args) } => { // what is happening here: grabbing & patching NavigationBar module contents from actual running JS const tsServerPath = typeof __TS_SEVER_PATH__ !== 'undefined' ? __TS_SEVER_PATH__ : require.main!.filename // current lib/tsserver.js @@ -23,13 +24,14 @@ const getPatchedNavModule = (): { getNavigationTree(...args) } => { linesOffset: number addString?: string removeLines?: number + // transform?: (found: string, content: string, position: number) => [string?, string?] } const patchLocations: PatchLocation[] = [ { searchString: 'function addChildrenRecursively(node)', linesOffset: 7, - addString: ` + addString: /* js */ ` case ts.SyntaxKind.JsxSelfClosingElement: addLeafNode(node) break; @@ -37,28 +39,55 @@ const getPatchedNavModule = (): { getNavigationTree(...args) } => { startNode(node) ts.forEachChild(node, addChildrenRecursively); endNode() - break`, + break;`, + }, + { + searchString: 'case 262 /* SyntaxKind.TypeAliasDeclaration */', + linesOffset: 3, + // https://github.com/microsoft/TypeScript/pull/52558/ + addString: /* js */ ` + case ts.SyntaxKind.TypeAliasDeclaration: + addNodeWithRecursiveChild(node, node.type); + break; + `, + }, + { + searchString: 'case 262 /* SyntaxKind.TypeAliasDeclaration */', + linesOffset: 0, + removeLines: 1, }, + // prettier-ignore + ...additionalFeatures.arraysTuplesNumberedItems ? [{ + searchString: 'function addChildrenRecursively(node)', + linesOffset: 7, + addString: /* js */ ` + case ts.SyntaxKind.TupleType: + case ts.SyntaxKind.ArrayLiteralExpression: + const { elements } = node; + for (const [i, element] of elements.entries()) { + addNodeWithRecursiveChild(element, element, ts.setTextRange(ts.factory.createIdentifier(i.toString()), element)); + } + break; + `, + }] : [], { searchString: 'return "";', linesOffset: -1, - addString: ` + addString: /* js */ ` case ts.SyntaxKind.JsxSelfClosingElement: - return getNameFromJsxTag(node); + return getNameFromJsxTag(node); case ts.SyntaxKind.JsxElement: - return getNameFromJsxTag(node.openingElement);`, + return getNameFromJsxTag(node.openingElement);`, }, ] - // semver: can't use compare as it incorrectly works with build postfix - const isTs5 = semver.major(ts.version) >= 5 const { markerModuleStart, markerModuleEnd, patches, returnModuleCode, skipStartMarker = false, - }: PatchData = !isTs5 + }: PatchData = !isTs5() ? { markerModuleStart: 'var NavigationBar;', markerModuleEnd: '(ts.NavigationBar = {}));', @@ -86,7 +115,7 @@ const getPatchedNavModule = (): { getNavigationTree(...args) } => { } const getModuleString = () => `module.exports = (ts, getNameFromJsxTag) => {\n${lines.join('\n')}\nreturn ${returnModuleCode}}` let moduleString = getModuleString() - if (isTs5) { + if (isTs5()) { const { languageService } = createLanguageService({ 'main.ts': moduleString, }) @@ -131,16 +160,17 @@ const getPatchedNavModule = (): { getNavigationTree(...args) } => { let navModule: { getNavigationTree: any } -export const getNavTreeItems = (info: ts.server.PluginCreateInfo, fileName: string) => { - if (!navModule) navModule = getPatchedNavModule() - const program = info.languageService.getProgram() - if (!program) throw new Error('no program') - const sourceFile = program?.getSourceFile(fileName) +export const getNavTreeItems = ( + languageService: ts.LanguageService, + languageServiceHost: ts.LanguageServiceHost, + fileName: string, + additionalFeatures: AdditionalFeatures, +) => { + if (!navModule) navModule = getPatchedNavModule(additionalFeatures) + const sourceFile = + (languageService as import('typescript-full').LanguageService).getNonBoundSourceFile?.(fileName) ?? + languageService.getProgram()!.getSourceFile(fileName) if (!sourceFile) throw new Error('no sourceFile') - const cancellationToken = info.languageServiceHost.getCompilerHost?.()?.getCancellationToken?.() ?? { - isCancellationRequested: () => false, - throwIfCancellationRequested: () => {}, - } - return navModule.getNavigationTree(sourceFile, cancellationToken) + return navModule.getNavigationTree(sourceFile, getCancellationToken(languageServiceHost)) } diff --git a/typescript/src/globals.d.ts b/typescript/src/globals.d.ts index 32af8fcb..f28206a6 100644 --- a/typescript/src/globals.d.ts +++ b/typescript/src/globals.d.ts @@ -3,6 +3,9 @@ import('ts-expose-internals') declare let ts: typeof import('typescript') declare let tsFull: typeof import('typescript-full') +declare type FullChecker = import('typescript-full').TypeChecker +declare type FullSourceFile = import('typescript-full').SourceFile + // declare type ts = import('typescript') // export {} // export * from 'typescript' diff --git a/typescript/src/namespaceAutoImports.ts b/typescript/src/namespaceAutoImports.ts new file mode 100644 index 00000000..c82fbbf9 --- /dev/null +++ b/typescript/src/namespaceAutoImports.ts @@ -0,0 +1,70 @@ +import { parseIgnoreSpec, findIndexOfAutoImportSpec } from './adjustAutoImports' +import { GetConfig } from './types' +import { getChangesTracker } from './utils' +import { camelCase } from 'change-case' + +export default ( + c: GetConfig, + sourceFile: ts.SourceFile, + importPath: string, + preferences: ts.UserPreferences, + formatOptions: ts.FormatCodeSettings, + position: number, + symbolName: string, + entryDetailsPrior?: ts.CompletionEntryDetails, + skipCreatingImport = false, +) => { + const changeToNamespaceImport = Object.entries(c('autoImport.changeToNamespaceImport')).map(([key, value]) => { + return [parseIgnoreSpec(key), value] as const + }) + const changeToNamespaceImportSpecs = changeToNamespaceImport.map(([spec]) => spec) + if (!changeToNamespaceImport.length) { + return + } + + const indexOfAutoImportSpec = findIndexOfAutoImportSpec(changeToNamespaceImportSpecs, importPath, '') + if (indexOfAutoImportSpec === undefined) return + const completionRangeStartPos = sourceFile + .getFullText() + .slice(0, position) + .match(/[\w\d]*$/i)!.index! + const { codeActions } = entryDetailsPrior ?? {} + // if import is already added, we exit + if (codeActions?.[0]?.changes[0]?.textChanges.length === 1) { + const codeAction = codeActions[0] + if (codeAction.description.startsWith('Change') && codeAction.changes[0]!.textChanges[0]!.span.start === completionRangeStartPos) { + return + } + } + + const { module } = changeToNamespaceImport[indexOfAutoImportSpec]![0] + const { namespace = camelCase(module), addImport = true, useDefaultImport } = changeToNamespaceImport[indexOfAutoImportSpec]![1] + const textChanges = [ + { + newText: `${namespace}.`, + span: { + start: completionRangeStartPos, + length: 0, + }, + }, + ] as ts.TextChange[] + if (!addImport) return { textChanges, description: `Change to '${namespace}.${symbolName}'` } + if (!skipCreatingImport) { + const { factory } = ts + const namespaceIdentifier = factory.createIdentifier(namespace) + const importDeclaration = factory.createImportDeclaration( + /*modifiers*/ undefined, + useDefaultImport + ? factory.createImportClause(false, namespaceIdentifier, undefined) + : factory.createImportClause(false, undefined, factory.createNamespaceImport(namespaceIdentifier)), + factory.createStringLiteral(importPath, preferences.quotePreference === 'single'), + ) + const changeTracker = getChangesTracker(formatOptions) + // todo respect sorting? + changeTracker.insertNodeAtTopOfFile(sourceFile as any, importDeclaration as any, true) + const changes = changeTracker.getChanges() + const { textChanges: importTextChanges } = changes[0]! + textChanges.unshift(...importTextChanges) + } + return { textChanges, description: `Add namespace import from '${importPath}'`, namespace, useDefaultImport } +} diff --git a/typescript/src/references.ts b/typescript/src/references.ts index 2a08c2da..ea67b706 100644 --- a/typescript/src/references.ts +++ b/typescript/src/references.ts @@ -54,4 +54,11 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, } return prior } + + // Volar 1.0.25 uses it + proxy.getReferencesAtPosition = (fileName, position) => { + const references = proxy.findReferences(fileName, position) + if (!references) return + return references.flatMap(({ references }) => references) + } } diff --git a/typescript/src/utils.ts b/typescript/src/utils.ts index 77caa0d7..a13d69c6 100644 --- a/typescript/src/utils.ts +++ b/typescript/src/utils.ts @@ -1,4 +1,5 @@ import { SetOptional } from 'type-fest' +import * as semver from 'semver' export function findChildContainingPosition(typescript: typeof ts, sourceFile: ts.SourceFile, position: number): ts.Node | undefined { function find(node: ts.Node): ts.Node | undefined { @@ -116,6 +117,9 @@ export const boostExistingSuggestions = (entries: ts.CompletionEntry[], predicat }) } +// semver: can't use compare as it incorrectly works with build postfix +export const isTs5 = () => semver.major(ts.version) >= 5 + // Workaround esbuild bundle modules export const nodeModules = __WEB__ ? null @@ -163,6 +167,30 @@ export function addObjectMethodResultInterceptors> } } +// have absolutely no idea why such useful utility is not publicly available +export const getChangesTracker = formatOptions => { + return new tsFull.textChanges.ChangeTracker(/* will be normalized by vscode anyway */ '\n', tsFull.formatting.getFormatContext(formatOptions, {})) +} + +export const getCancellationToken = (languageServiceHost: ts.LanguageServiceHost) => { + let cancellationToken = languageServiceHost.getCancellationToken?.() as ts.CancellationToken | undefined + // if (!cancellationToken) { + // debugger + // } + cancellationToken ??= { + isCancellationRequested: () => false, + throwIfCancellationRequested: () => {}, + } + if (!cancellationToken.throwIfCancellationRequested) { + cancellationToken.throwIfCancellationRequested = () => { + if (cancellationToken!.isCancellationRequested()) { + throw new ts.OperationCanceledException() + } + } + } + return cancellationToken +} + const wordRangeAtPos = (text: string, position: number) => { const isGood = (pos: number) => { return /[-\w\d]/i.test(text.at(pos) ?? '') diff --git a/typescript/src/volarConfig.ts b/typescript/src/volarConfig.ts index 7b979f76..cd62e7c3 100644 --- a/typescript/src/volarConfig.ts +++ b/typescript/src/volarConfig.ts @@ -21,6 +21,9 @@ const plugin = (context => { // todo support vue-specific settings const originalLsMethods = { ...typescript.languageService } + configurationHost.getConfiguration('[vue]').then(_configuration => { + console.log('_configuration', _configuration) + }) configurationHost.getConfiguration('tsEssentialPlugins').then(_configuration => { // if (typescript.languageService[thisPluginMarker]) return const config = patchConfig(_configuration) diff --git a/typescript/src/workspaceSymbolSearch.ts b/typescript/src/workspaceSymbolSearch.ts index 46057cba..195dbff7 100644 --- a/typescript/src/workspaceSymbolSearch.ts +++ b/typescript/src/workspaceSymbolSearch.ts @@ -1,4 +1,5 @@ import { GetConfig } from './types' +import { getCancellationToken } from './utils' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig, languageServiceHost: ts.LanguageServiceHost) => { proxy.getNavigateToItems = (searchValue, maxResultCount, fileName, excludeDtsFiles) => { @@ -8,10 +9,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, } const program = languageService.getProgram()! - const cancellationToken = languageServiceHost.getCompilerHost?.()?.getCancellationToken?.() ?? { - isCancellationRequested: () => false, - throwIfCancellationRequested: () => {}, - } + let sourceFiles = fileName ? [program.getSourceFile(fileName)!] : program.getSourceFiles() if (!fileName) { const excludes = tsFull.getRegularExpressionForWildcard(workspaceSymbolSearchExcludePatterns, '', 'exclude')?.slice(1) @@ -23,8 +21,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, return tsFull.NavigateTo.getNavigateToItems( sourceFiles as any, program.getTypeChecker() as any, - // TODO! use real cancellationToken - cancellationToken, + getCancellationToken(languageServiceHost), searchValue, maxResultCount, excludeDtsFiles ?? false, diff --git a/typescript/test/completions.spec.ts b/typescript/test/completions.spec.ts index 70cc01b8..8524a779 100644 --- a/typescript/test/completions.spec.ts +++ b/typescript/test/completions.spec.ts @@ -1,32 +1,16 @@ -//@ts-ignore plugin expect it to set globallly -globalThis.__WEB__ = false import { pickObj } from '@zardoy/utils' -import { createLanguageService } from '../src/dummyLanguageService' import { getCompletionsAtPosition as getCompletionsAtPositionRaw } from '../src/completionsAtPosition' import type {} from 'vitest/globals' -import ts from 'typescript/lib/tsserverlibrary' -import { getDefaultConfigFunc } from './defaultSettings' import { isGoodPositionBuiltinMethodCompletion, isGoodPositionMethodCompletion } from '../src/completions/isGoodPositionMethodCompletion' -import { getNavTreeItems } from '../src/getPatchedNavTree' -import { createRequire } from 'module' -import { findChildContainingPosition } from '../src/utils' +import { findChildContainingExactPosition } from '../src/utils' import handleCommand from '../src/specialCommands/handle' import _ from 'lodash' -import decorateFormatFeatures from '../src/decorateFormatFeatures' +import { defaultConfigFunc, entrypoint, settingsOverride, sharedLanguageService } from './shared' -// TODO rename file to plugin.spec.ts or move other tests - -const require = createRequire(import.meta.url) -//@ts-ignore plugin expect it to set globallly -globalThis.ts = globalThis.tsFull = ts - -const entrypoint = '/test.tsx' -const files = { [entrypoint]: '' } - -const { languageService, updateProject } = createLanguageService(files) +const { languageService, languageServiceHost, updateProject, getCurrentFile } = sharedLanguageService const getSourceFile = () => languageService.getProgram()!.getSourceFile(entrypoint)! -const getNode = (pos: number) => findChildContainingPosition(ts, getSourceFile(), pos) +const getNode = (pos: number) => findChildContainingExactPosition(getSourceFile(), pos) const newFileContents = (contents: string, fileName = entrypoint) => { const cursorPositions: number[] = [] @@ -36,8 +20,9 @@ const newFileContents = (contents: string, fileName = entrypoint) => { contents = contents.slice(0, cursorIndex) + contents.slice(cursorIndex + replacement.length) cursorPositions.push(cursorIndex) } - files[fileName] = contents - updateProject() + updateProject({ + [fileName]: contents, + }) return cursorPositions } @@ -62,8 +47,9 @@ const fileContentsSpecialPositions = (contents: string, fileName = entrypoint) = } replacement.lastIndex -= matchLength } - files[fileName] = contents - updateProject() + updateProject({ + [fileName]: contents, + }) if (cursorPositionsOnly.some(arr => arr.length)) { if (process.env.CI) throw new Error('Only positions not allowed on CI') return cursorPositionsOnly @@ -71,11 +57,63 @@ const fileContentsSpecialPositions = (contents: string, fileName = entrypoint) = return cursorPositions } -const settingsOverride = { - 'arrayMethodsSnippets.enable': true, +interface CompletionPartMatcher { + names?: string[] + all?: Pick +} + +interface CompletionMatcher { + exact?: CompletionPartMatcher + includes?: CompletionPartMatcher + excludes?: string[] +} + +interface CodeActionMatcher { + apply?: { + name: string + } +} + +const fourslashLikeTester = (contents: string, fileName = entrypoint) => { + const [positive, _negative, numberedPositions] = fileContentsSpecialPositions(contents, fileName) + return { + completion: (marker: number | number[], matcher: CompletionMatcher, meta?) => { + for (const mark of Array.isArray(marker) ? marker : [marker]) { + if (numberedPositions[mark] === undefined) throw new Error(`No marker ${mark} found`) + const result = getCompletionsAtPosition(numberedPositions[mark]!, { shouldHave: true })! + const message = ` at marker ${mark}` + const { exact, includes, excludes } = matcher + if (exact) { + const { names, all } = exact + if (names) { + expect(result?.entryNames, message).toEqual(names) + } + if (all) { + for (const entry of result.entries) { + expect(entry, entry.name + message).toContain(all) + } + } + } + if (includes) { + const { names, all } = includes + if (names) { + expect(result?.entryNames, message).toContain(names) + } + if (all) { + for (const entry of result.entries.filter(e => names?.includes(e.name))) { + expect(entry, entry.name + message).toContain(all) + } + } + } + if (excludes) { + expect(result?.entryNames, message).not.toContain(excludes) + } + } + }, + // TODO implement + codeAction: (marker: number | number[], matcher: CodeActionMatcher, meta?) => {}, + } } -//@ts-ignore -const defaultConfigFunc = await getDefaultConfigFunc(settingsOverride) const getCompletionsAtPosition = (pos: number, { fileName = entrypoint, shouldHave }: { fileName?: string; shouldHave?: boolean } = {}) => { if (pos === undefined) throw new Error('getCompletionsAtPosition: pos is undefined') @@ -85,7 +123,7 @@ const getCompletionsAtPosition = (pos: number, { fileName = entrypoint, shouldHa {}, defaultConfigFunc, languageService, - ts.ScriptSnapshot.fromString(files[entrypoint]), + languageServiceHost.getScriptSnapshot(entrypoint)!, undefined, { scriptKind: ts.ScriptKind.TSX, compilerOptions: {} }, ) @@ -277,7 +315,7 @@ test('Switch Case Exclude Covered', () => { }) test('Case-sensetive completions', () => { - settingsOverride['caseSensitiveCompletions'] = true + settingsOverride.caseSensitiveCompletions = true const [_positivePositions, _negativePositions, numPositions] = fileContentsSpecialPositions(/* ts */ ` const a = { TestItem: 5, @@ -296,6 +334,44 @@ test('Case-sensetive completions', () => { } }) +test('Omit<..., ""> suggestions', () => { + const tester = fourslashLikeTester(/* ts */ ` + interface A { + a: string; + b: number; + } + type B = Omit; + type B = Omit; + `) + tester.completion(1, { + exact: { + names: ['a', 'b'], + }, + }) + tester.completion(2, { + exact: { + names: ['b'], + }, + }) +}) + +test('Additional types suggestions', () => { + const tester = fourslashLikeTester(/* ts */ ` + type A = T; + type A = T; + `) + tester.completion(1, { + exact: { + names: ['extends'], + }, + }) + tester.completion(2, { + exact: { + names: ['a', 'b'], + }, + }) +}) + test('Object Literal Completions', () => { const [_positivePositions, _negativePositions, numPositions] = fileContentsSpecialPositions(/* ts */ ` interface Options { @@ -306,20 +382,24 @@ test('Object Literal Completions', () => { foo?: boolean } plugins: Array<{ name: string, setup(build) }> + undefinedOption: undefined } - const makeDay = (options: Options) => {} + const makeDay = (options?: Options) => {} makeDay({ usedOption, /*1*/ }) + + const somethingWithUntions: { a: string } | { a: any[], b: string } = {/*2*/} `) - const { entriesSorted } = getCompletionsAtPosition(numPositions[1]!) ?? {} + const { entriesSorted: pos1 } = getCompletionsAtPosition(numPositions[1]!)! + const { entriesSorted: pos2 } = getCompletionsAtPosition(numPositions[2]!)! // todo resolve sorting problem + add tests with other keepOriginal (it was tested manually) - for (const entry of entriesSorted ?? []) { + for (const entry of [...pos1, ...pos2]) { entry.insertText = entry.insertText?.replaceAll('\n', '\\n') } - expect(entriesSorted).toMatchInlineSnapshot(` + expect(pos1).toMatchInlineSnapshot(/* json */ ` [ { "insertText": "plugins", @@ -338,6 +418,13 @@ test('Object Literal Completions', () => { }, "name": "plugins", }, + { + "insertText": "undefinedOption", + "isSnippet": true, + "kind": "property", + "kindModifiers": "", + "name": "undefinedOption", + }, { "insertText": "additionalOptions", "isSnippet": true, @@ -381,63 +468,26 @@ test('Object Literal Completions', () => { }, ] `) -}) - -// TODO move/remove this test from here -test('Patched navtree (outline)', () => { - globalThis.__TS_SEVER_PATH__ = require.resolve('typescript/lib/tsserver') - newFileContents(/* tsx */ ` - const classes = { - header: '...', - title: '...' - } - function A() { - return - before -
-
- -
- after - - } - `) - const navTreeItems: ts.NavigationTree = getNavTreeItems({ languageService, languageServiceHost: {} } as any, entrypoint) - const simplify = (items: ts.NavigationTree[]) => { - const newItems: { text: any; childItems? }[] = [] - for (const { text, childItems } of items) { - if (text === 'classes') continue - newItems.push({ text, ...(childItems ? { childItems: simplify(childItems) } : {}) }) - } - return newItems - } - expect(simplify(navTreeItems.childItems ?? [])).toMatchInlineSnapshot(/* json */ ` + expect(pos2.map(x => x.insertText)).toMatchInlineSnapshot(` [ - { - "childItems": [ - { - "childItems": [ - { - "childItems": [ - { - "text": "div", - }, - { - "text": "span.good", - }, - ], - "text": "div#ok", - }, - ], - "text": "Notification.test.another#yes", - }, - ], - "text": "A", - }, + "a", + "b", + "b: \\"$1\\",$0", ] `) }) +test('Extract to type / interface name inference', () => { + fourslashLikeTester(/* ts */ ` + const foo: { bar: string; } = { bar: 'baz' } + const foo = { bar: 'baz' } satisfies { bar: 5 } + + const fn = (foo: { bar: 'baz' }, foo = {} as { bar: 'baz' }) => {} + + const obj = { foo: { bar: 'baz' } as { bar: string; } } + `) +}) + test('In Keyword Completions', () => { const [pos] = newFileContents(/* ts */ ` declare const a: { a: boolean, b: string } | { a: number, c: number } | string @@ -502,34 +552,3 @@ test('In Keyword Completions', () => { } `) }) - -test('Format ignore', () => { - decorateFormatFeatures(languageService, { ...languageService }, defaultConfigFunc) - newFileContents(/* ts */ ` -const a = { - //@ts-format-ignore-region - a: 1, - a1: 2, - // @ts-format-ignore-endregion - b: 3, - // @ts-format-ignore-line Any content don't care - c: 4, -}`) - const edits = languageService.getFormattingEditsForRange(entrypoint, 0, files[entrypoint]!.length, ts.getDefaultFormatCodeSettings()) - // const sourceFile = languageService.getProgram()!.getSourceFile(entrypoint)! - // const text = sourceFile.getFullText() - // edits.forEach(edit => { - // console.log(text.slice(0, edit.span.start) + '<<<' + edit.newText + '>>>' + text.slice(edit.span.start + edit.span.length)) - // }) - expect(edits).toMatchInlineSnapshot(/* json */ ` - [ - { - "newText": " ", - "span": { - "length": 2, - "start": 109, - }, - }, - ] - `) -}) diff --git a/typescript/test/other.spec.ts b/typescript/test/other.spec.ts new file mode 100644 index 00000000..a8a1c318 --- /dev/null +++ b/typescript/test/other.spec.ts @@ -0,0 +1,96 @@ +import decorateFormatFeatures from '../src/decorateFormatFeatures' +import { defaultConfigFunc, entrypoint, sharedLanguageService } from './shared' +import { getNavTreeItems } from '../src/getPatchedNavTree' +import { createRequire } from 'module' + +const { languageService, languageServiceHost, updateProject, getCurrentFile } = sharedLanguageService + +test('Format ignore', () => { + decorateFormatFeatures(languageService, { ...languageService }, languageServiceHost, defaultConfigFunc) + const contents = /* ts */ ` +const a = { + //@ts-format-ignore-region + a: 1, + a1: 2, + // @ts-format-ignore-endregion + b: 3, + // @ts-format-ignore-line Any content don't care + c: 4, +};` + updateProject(contents) + const edits = languageService.getFormattingEditsForRange(entrypoint, 0, contents.length, ts.getDefaultFormatCodeSettings()) + // const sourceFile = languageService.getProgram()!.getSourceFile(entrypoint)! + // const text = sourceFile.getFullText() + // edits.forEach(edit => { + // console.log(text.slice(0, edit.span.start) + '<<<' + edit.newText + '>>>' + text.slice(edit.span.start + edit.span.length)) + // }) + expect(edits).toMatchInlineSnapshot(/* json */ ` + [ + { + "newText": " ", + "span": { + "length": 2, + "start": 109, + }, + }, + ] + `) +}) + +const require = createRequire(import.meta.url) + +test('Patched navtree (outline)', () => { + globalThis.__TS_SEVER_PATH__ = require.resolve('typescript/lib/tsserver') + updateProject(/* tsx */ ` + const classes = { + header: '...', + title: '...' + } + function A() { + return + before +
+
+ +
+ after + + } + `) + const navTreeItems: ts.NavigationTree = getNavTreeItems(languageService, {} as any, entrypoint, { + arraysTuplesNumberedItems: false, + }) + const simplify = (items: ts.NavigationTree[]) => { + const newItems: { text: any; childItems? }[] = [] + for (const { text, childItems } of items) { + if (text === 'classes') continue + newItems.push({ text, ...(childItems ? { childItems: simplify(childItems) } : {}) }) + } + return newItems + } + expect(simplify(navTreeItems.childItems ?? [])).toMatchInlineSnapshot(/* json */ ` + [ + { + "childItems": [ + { + "childItems": [ + { + "childItems": [ + { + "text": "div", + }, + { + "text": "span.good", + }, + ], + "text": "div#ok", + }, + ], + "text": "Notification.test.another#yes", + }, + ], + "text": "A", + }, + ] + `) +}) diff --git a/typescript/test/shared.ts b/typescript/test/shared.ts new file mode 100644 index 00000000..8278472c --- /dev/null +++ b/typescript/test/shared.ts @@ -0,0 +1,18 @@ +beforeAll(() => { + //@ts-ignore plugin expect it to set globallly + globalThis.__WEB__ = false +}) + +import { createLanguageService } from '../src/dummyLanguageService' +import { Configuration } from '../src/types' +import { getDefaultConfigFunc } from './defaultSettings' + +export const entrypoint = '/test.tsx' + +export const sharedLanguageService = createLanguageService({ [entrypoint]: '' }, {}, entrypoint) + +export const settingsOverride: Partial = { + 'arrayMethodsSnippets.enable': true, + 'codeActions.extractTypeInferName': true, +} +export const defaultConfigFunc = await getDefaultConfigFunc(settingsOverride) diff --git a/vitest-environment-ts-plugin/index.js b/vitest-environment-ts-plugin/index.js new file mode 100644 index 00000000..b8a39aa3 --- /dev/null +++ b/vitest-environment-ts-plugin/index.js @@ -0,0 +1,11 @@ +/** @type {import('vitest').Environment} */ +module.exports = { + name: 'vitest-environment-ts-plugin', + setup() { + globalThis.__WEB__ = false + globalThis.ts = globalThis.tsFull = require('typescript/lib/tsserverlibrary') + return { + teardown() {}, + } + }, +}