add jit support, refactor for general reliability
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 235 KiB |
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 267 KiB |
|
@ -1 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
*.vsix
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
node_modules/**
|
||||
.vscode/**
|
||||
.github/**
|
||||
src/**
|
||||
packages/**
|
||||
tests/**
|
||||
**/*.ts
|
||||
**/*.map
|
||||
.gitignore
|
||||
**/tsconfig.json
|
||||
**/tsconfig.base.json
|
||||
contributing.md
|
||||
node_modules/**
|
||||
src/**
|
||||
tests/**
|
||||
.github/**
|
|
@ -0,0 +1,130 @@
|
|||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/.github/banner-dark.png" alt="" />
|
||||
|
||||
Tailwind CSS IntelliSense enhances the Tailwind development experience by providing Visual Studio Code users with advanced features such as autocomplete, syntax highlighting, and linting.
|
||||
|
||||
## Installation
|
||||
|
||||
**[Install via the Visual Studio Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
|
||||
|
||||
In order for the extension to activate you must have [`tailwindcss` installed](https://tailwindcss.com/docs/installation/#1-install-tailwind-via-npm) and a [Tailwind config file](https://tailwindcss.com/docs/installation/#3-create-your-tailwind-config-file-optional) named `tailwind.config.js` or `tailwind.js` in your workspace.
|
||||
|
||||
## Features
|
||||
|
||||
### Autocomplete
|
||||
|
||||
Intelligent suggestions for class names, as well as [CSS functions and directives](https://tailwindcss.com/docs/functions-and-directives/).
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/.github/autocomplete.png" alt="" />
|
||||
|
||||
### Linting
|
||||
|
||||
Highlights errors and potential bugs in both your CSS and your markup.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/.github/linting.png" alt="" />
|
||||
|
||||
### Hover Preview
|
||||
|
||||
See the complete CSS for a Tailwind class name by hovering over it.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/.github/hover.png" alt="" />
|
||||
|
||||
### CSS Syntax Highlighting
|
||||
|
||||
Provides syntax definitions so that Tailwind features are highlighted correctly.
|
||||
|
||||
## Recommended VS Code Settings
|
||||
|
||||
VS Code has built-in CSS validation which may display errors when using Tailwind-specific syntax, such as `@apply`. You can disable this with the `css.validate` setting:
|
||||
|
||||
```
|
||||
"css.validate": false
|
||||
```
|
||||
|
||||
By default VS Code will not trigger completions when editing "string" content, for example within JSX attribute values. Updating the `editor.quickSuggestions` setting may improve your experience, particularly when editing Tailwind classes within JSX:
|
||||
|
||||
```
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Settings
|
||||
|
||||
### `tailwindCSS.includeLanguages`
|
||||
|
||||
This setting allows you to add additional language support. The key of each entry is the new language ID and the value is any one of the extensions built-in languages, depending on how you want the new language to be treated (e.g. `html`, `css`, or `javascript`):
|
||||
|
||||
```json
|
||||
{
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"plaintext": "html"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `tailwindCSS.emmetCompletions`
|
||||
|
||||
Enable completions when using [Emmet](https://emmet.io/)-style syntax, for example `div.bg-red-500.uppercase`. **Default: `false`**
|
||||
|
||||
```json
|
||||
{
|
||||
"tailwindCSS.emmetCompletions": true
|
||||
}
|
||||
```
|
||||
|
||||
### `tailwindCSS.colorDecorators`
|
||||
|
||||
Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.
|
||||
|
||||
- `inherit`: Color decorators are rendered if `editor.colorDecorators` is enabled.
|
||||
- `on`: Color decorators are rendered.
|
||||
- `off`: Color decorators are not rendered.
|
||||
|
||||
### `tailwindCSS.showPixelEquivalents`
|
||||
|
||||
Show `px` equivalents for `rem` CSS values in completions and hovers. **Default: `true`**
|
||||
|
||||
### `tailwindCSS.rootFontSize`
|
||||
|
||||
Root font size in pixels. Used to convert `rem` CSS values to their `px` equivalents. See [`tailwindCSS.showPixelEquivalents`](#tailwindcssshowpixelequivalents). **Default: `16`**
|
||||
|
||||
### `tailwindCSS.validate`
|
||||
|
||||
Enable linting. Rules can be configured individually using the `tailwindcss.lint` settings:
|
||||
|
||||
- `ignore`: disable lint rule entirely
|
||||
- `warning`: rule violations will be considered "warnings," typically represented by a yellow underline
|
||||
- `error`: rule violations will be considered "errors," typically represented by a red underline
|
||||
|
||||
#### `tailwindCSS.lint.invalidScreen`
|
||||
|
||||
Unknown screen name used with the [`@screen` directive](https://tailwindcss.com/docs/functions-and-directives/#screen). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidVariant`
|
||||
|
||||
Unknown variant name used with the [`@variants` directive](https://tailwindcss.com/docs/functions-and-directives/#variants). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidTailwindDirective`
|
||||
|
||||
Unknown value used with the [`@tailwind` directive](https://tailwindcss.com/docs/functions-and-directives/#tailwind). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidApply`
|
||||
|
||||
Unsupported use of the [`@apply` directive](https://tailwindcss.com/docs/functions-and-directives/#apply). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidConfigPath`
|
||||
|
||||
Unknown or invalid path used with the [`theme` helper](https://tailwindcss.com/docs/functions-and-directives/#theme). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.cssConflict`
|
||||
|
||||
Class names on the same HTML element which apply the same CSS property or properties. **Default: `warning`**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you’re having issues getting the IntelliSense features to activate, there are a few things you can check:
|
||||
|
||||
- Ensure that you have a Tailwind config file in your workspace and that this is named `tailwind.config.js` or `tailwind.js`. Check out the Tailwind documentation for details on [creating a config file](https://tailwindcss.com/docs/installation/#3-create-your-tailwind-config-file-optional).
|
||||
- Ensure that the `tailwindcss` module is installed in your workspace, via `npm`, `yarn`, or `pnpm`. Tailwind CSS IntelliSense does not currently support Yarn Plug'n'Play.
|
||||
- If you installed `tailwindcss` or created your config file while your project was already open in Visual Studio Code you may need to reload the editor. You can either restart VS Code entirely, or use the `Developer: Reload Window` command which can be found in the command palette.
|
||||
- Make sure your VS Code settings aren’t causing your Tailwind config file to be excluded from search, for example via the `search.exclude` setting.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
@ -1,10 +1,241 @@
|
|||
{
|
||||
"name": "vscode-tailwindcss",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
"displayName": "Tailwind CSS IntelliSense",
|
||||
"description": "Intelligent Tailwind CSS tooling for VS Code",
|
||||
"preview": true,
|
||||
"author": "Brad Cornes <hello@bradley.dev>",
|
||||
"license": "MIT",
|
||||
"version": "0.5.9",
|
||||
"homepage": "https://github.com/tailwindlabs/tailwindcss-intellisense",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tailwindlabs/tailwindcss-intellisense/issues",
|
||||
"email": "hello@bradley.dev"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tailwindlabs/tailwindcss-intellisense.git"
|
||||
},
|
||||
"publisher": "bradlc",
|
||||
"keywords": [
|
||||
"tailwind",
|
||||
"tailwindcss",
|
||||
"css",
|
||||
"intellisense",
|
||||
"autocomplete",
|
||||
"vscode"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.33.0"
|
||||
},
|
||||
"categories": [
|
||||
"Linters",
|
||||
"Other"
|
||||
],
|
||||
"galleryBanner": {
|
||||
"color": "#f9fafb"
|
||||
},
|
||||
"icon": "media/icon.png",
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "dist/extension/index.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "tailwindCSS.showOutput",
|
||||
"title": "Tailwind CSS: Show Output"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"scopeName": "tailwindcss.injection",
|
||||
"path": "./syntaxes/tailwind.tmLanguage.json",
|
||||
"injectTo": [
|
||||
"source.css",
|
||||
"source.css.scss",
|
||||
"source.css.less",
|
||||
"source.css.postcss",
|
||||
"source.vue",
|
||||
"source.svelte",
|
||||
"text.html"
|
||||
]
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Tailwind CSS IntelliSense",
|
||||
"properties": {
|
||||
"tailwindCSS.emmetCompletions": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Enable class name completions when using Emmet-style syntax, for example `div.bg-red-500.uppercase`"
|
||||
},
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {},
|
||||
"markdownDescription": "Enable features in languages that are not supported by default. Add a mapping here between the new language and an already supported language.\n E.g.: `{\"plaintext\": \"html\"}`"
|
||||
},
|
||||
"tailwindCSS.colorDecorators": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"inherit",
|
||||
"on",
|
||||
"off"
|
||||
],
|
||||
"markdownEnumDescriptions": [
|
||||
"Color decorators are rendered if `editor.colorDecorators` is enabled.",
|
||||
"Color decorators are rendered.",
|
||||
"Color decorators are not rendered."
|
||||
],
|
||||
"default": "inherit",
|
||||
"markdownDescription": "Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.validate": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Enable linting. Rules can be configured individually using the `tailwindcss.lint.*` settings",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.cssConflict": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "warning",
|
||||
"markdownDescription": "Class names on the same HTML element which apply the same CSS property or properties",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidApply": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unsupported use of the [`@apply` directive](https://tailwindcss.com/docs/functions-and-directives/#apply)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidScreen": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown screen name used with the [`@screen` directive](https://tailwindcss.com/docs/functions-and-directives/#screen)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidVariant": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown variant name used with the [`@variants` directive](https://tailwindcss.com/docs/functions-and-directives/#variants)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidConfigPath": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown or invalid path used with the [`theme` helper](https://tailwindcss.com/docs/functions-and-directives/#theme)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidTailwindDirective": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown value used with the [`@tailwind` directive](https://tailwindcss.com/docs/functions-and-directives/#tailwind)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": {
|
||||
"type": "array",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.showPixelEquivalents": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Show `px` equivalents for `rem` CSS values."
|
||||
},
|
||||
"tailwindCSS.rootFontSize": {
|
||||
"type": "number",
|
||||
"default": 16,
|
||||
"markdownDescription": "Root font size in pixels. Used to convert `rem` CSS values to their `px` equivalents. See `#tailwindCSS.showPixelEquivalents#`."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "glob-exec --foreach --parallel \"src/*.ts\" -- \"ncc build {{file}} --watch -o dist/{{file.toString().replace(/^src\\//, '').replace(/\\.ts$/, '')}}\"",
|
||||
"build": "glob-exec --foreach --parallel \"src/*.ts\" -- \"ncc build {{file}} -o dist/{{file.toString().replace(/^src\\//, '').replace(/\\.ts$/, '')}}\"",
|
||||
"minify": "glob-exec --foreach --parallel \"dist/**/*.js\" -- \"terser {{file}} --compress --mangle --output {{file.toString()}}\"",
|
||||
"package": "vsce package",
|
||||
"publish": "vsce publish",
|
||||
"vscode:prepublish": "npm run clean && npm run build && npm run minify",
|
||||
"clean": "rimraf dist",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "3.1.4",
|
||||
"@types/debounce": "1.2.0",
|
||||
"@types/mocha": "5.2.0",
|
||||
"@types/node": "14.14.34",
|
||||
"@types/vscode": "1.54.0",
|
||||
"@vercel/ncc": "0.28.4",
|
||||
"builtin-modules": "3.2.0",
|
||||
"chokidar": "3.5.1",
|
||||
"debounce": "1.2.0",
|
||||
"dlv": "1.1.3",
|
||||
"dset": "2.0.1",
|
||||
"enhanced-resolve": "5.8.0",
|
||||
"fast-glob": "3.2.4",
|
||||
"find-up": "5.0.0",
|
||||
"glob-exec": "0.1.1",
|
||||
"jest": "25.5.4",
|
||||
"klona": "2.0.4",
|
||||
"normalize-path": "3.0.0",
|
||||
"pkg-up": "3.1.0",
|
||||
"postcss": "8.2.6",
|
||||
"postcss-load-config": "3.0.1",
|
||||
"postcss-selector-parser": "6.0.2",
|
||||
"prettier": "^2.2.1",
|
||||
"rimraf": "3.0.2",
|
||||
"semver": "7.3.2",
|
||||
"stack-trace": "0.0.10",
|
||||
"tailwindcss": "2.0.3",
|
||||
"terser": "4.6.12",
|
||||
"typescript": "4.2.4",
|
||||
"vsce": "1.87.0",
|
||||
"vscode-languageclient": "7.0.0",
|
||||
"vscode-languageserver": "7.0.0",
|
||||
"vscode-languageserver-textdocument": "1.0.1",
|
||||
"vscode-uri": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.2.1"
|
||||
"@types/moo": "0.5.3",
|
||||
"css.escape": "1.5.1",
|
||||
"detect-indent": "6.0.0",
|
||||
"line-column": "1.0.2",
|
||||
"moo": "0.5.1",
|
||||
"multi-regexp2": "1.0.3",
|
||||
"sift-string": "0.0.2",
|
||||
"vscode-emmet-helper-bundled": "0.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
node_modules
|
||||
dist
|
||||
*.vsix
|
|
@ -1,130 +0,0 @@
|
|||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/packages/tailwindcss-intellisense/.github/banner-dark.png" alt="" />
|
||||
|
||||
Tailwind CSS IntelliSense enhances the Tailwind development experience by providing Visual Studio Code users with advanced features such as autocomplete, syntax highlighting, and linting.
|
||||
|
||||
## Installation
|
||||
|
||||
**[Install via the Visual Studio Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
|
||||
|
||||
In order for the extension to activate you must have [`tailwindcss` installed](https://tailwindcss.com/docs/installation/#1-install-tailwind-via-npm) and a [Tailwind config file](https://tailwindcss.com/docs/installation/#3-create-your-tailwind-config-file-optional) named `tailwind.config.js` or `tailwind.js` in your workspace.
|
||||
|
||||
## Features
|
||||
|
||||
### Autocomplete
|
||||
|
||||
Intelligent suggestions for class names, as well as [CSS functions and directives](https://tailwindcss.com/docs/functions-and-directives/).
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/packages/tailwindcss-intellisense/.github/autocomplete.png" alt="" />
|
||||
|
||||
### Linting
|
||||
|
||||
Highlights errors and potential bugs in both your CSS and your markup.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/packages/tailwindcss-intellisense/.github/linting.png" alt="" />
|
||||
|
||||
### Hover Preview
|
||||
|
||||
See the complete CSS for a Tailwind class name by hovering over it.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/packages/tailwindcss-intellisense/.github/hover.png" alt="" />
|
||||
|
||||
### CSS Syntax Highlighting
|
||||
|
||||
Provides syntax definitions so that Tailwind features are highlighted correctly.
|
||||
|
||||
## Recommended VS Code Settings
|
||||
|
||||
VS Code has built-in CSS validation which may display errors when using Tailwind-specific syntax, such as `@apply`. You can disable this with the `css.validate` setting:
|
||||
|
||||
```
|
||||
"css.validate": false
|
||||
```
|
||||
|
||||
By default VS Code will not trigger completions when editing "string" content, for example within JSX attribute values. Updating the `editor.quickSuggestions` setting may improve your experience, particularly when editing Tailwind classes within JSX:
|
||||
|
||||
```
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Settings
|
||||
|
||||
### `tailwindCSS.includeLanguages`
|
||||
|
||||
This setting allows you to add additional language support. The key of each entry is the new language ID and the value is any one of the extensions built-in languages, depending on how you want the new language to be treated (e.g. `html`, `css`, or `javascript`):
|
||||
|
||||
```json
|
||||
{
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"plaintext": "html"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `tailwindCSS.emmetCompletions`
|
||||
|
||||
Enable completions when using [Emmet](https://emmet.io/)-style syntax, for example `div.bg-red-500.uppercase`. **Default: `false`**
|
||||
|
||||
```json
|
||||
{
|
||||
"tailwindCSS.emmetCompletions": true
|
||||
}
|
||||
```
|
||||
|
||||
### `tailwindCSS.colorDecorators`
|
||||
|
||||
Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.
|
||||
|
||||
- `inherit`: Color decorators are rendered if `editor.colorDecorators` is enabled.
|
||||
- `on`: Color decorators are rendered.
|
||||
- `off`: Color decorators are not rendered.
|
||||
|
||||
### `tailwindCSS.showPixelEquivalents`
|
||||
|
||||
Show `px` equivalents for `rem` CSS values in completions and hovers. **Default: `true`**
|
||||
|
||||
### `tailwindCSS.rootFontSize`
|
||||
|
||||
Root font size in pixels. Used to convert `rem` CSS values to their `px` equivalents. See [`tailwindCSS.showPixelEquivalents`](#tailwindcssshowpixelequivalents). **Default: `16`**
|
||||
|
||||
### `tailwindCSS.validate`
|
||||
|
||||
Enable linting. Rules can be configured individually using the `tailwindcss.lint` settings:
|
||||
|
||||
- `ignore`: disable lint rule entirely
|
||||
- `warning`: rule violations will be considered "warnings," typically represented by a yellow underline
|
||||
- `error`: rule violations will be considered "errors," typically represented by a red underline
|
||||
|
||||
#### `tailwindCSS.lint.invalidScreen`
|
||||
|
||||
Unknown screen name used with the [`@screen` directive](https://tailwindcss.com/docs/functions-and-directives/#screen). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidVariant`
|
||||
|
||||
Unknown variant name used with the [`@variants` directive](https://tailwindcss.com/docs/functions-and-directives/#variants). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidTailwindDirective`
|
||||
|
||||
Unknown value used with the [`@tailwind` directive](https://tailwindcss.com/docs/functions-and-directives/#tailwind). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidApply`
|
||||
|
||||
Unsupported use of the [`@apply` directive](https://tailwindcss.com/docs/functions-and-directives/#apply). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidConfigPath`
|
||||
|
||||
Unknown or invalid path used with the [`theme` helper](https://tailwindcss.com/docs/functions-and-directives/#theme). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.cssConflict`
|
||||
|
||||
Class names on the same HTML element which apply the same CSS property or properties. **Default: `warning`**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you’re having issues getting the IntelliSense features to activate, there are a few things you can check:
|
||||
|
||||
- Ensure that you have a Tailwind config file in your workspace and that this is named `tailwind.config.js` or `tailwind.js`. Check out the Tailwind documentation for details on [creating a config file](https://tailwindcss.com/docs/installation/#3-create-your-tailwind-config-file-optional).
|
||||
- Ensure that the `tailwindcss` module is installed in your workspace, via `npm`, `yarn`, or `pnpm`. Tailwind CSS IntelliSense does not currently support Yarn Plug'n'Play.
|
||||
- If you installed `tailwindcss` or created your config file while your project was already open in Visual Studio Code you may need to reload the editor. You can either restart VS Code entirely, or use the `Developer: Reload Window` command which can be found in the command palette.
|
||||
- Make sure your VS Code settings aren’t causing your Tailwind config file to be excluded from search, for example via the `search.exclude` setting.
|
|
@ -1,230 +0,0 @@
|
|||
{
|
||||
"name": "vscode-tailwindcss",
|
||||
"displayName": "Tailwind CSS IntelliSense",
|
||||
"description": "Intelligent Tailwind CSS tooling for VS Code",
|
||||
"preview": true,
|
||||
"author": "Brad Cornes <hello@bradley.dev>",
|
||||
"license": "MIT",
|
||||
"version": "0.5.10",
|
||||
"homepage": "https://github.com/tailwindlabs/tailwindcss-intellisense",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tailwindlabs/tailwindcss-intellisense/issues",
|
||||
"email": "hello@bradley.dev"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tailwindlabs/tailwindcss-intellisense.git"
|
||||
},
|
||||
"publisher": "bradlc",
|
||||
"keywords": [
|
||||
"tailwind",
|
||||
"tailwindcss",
|
||||
"css",
|
||||
"intellisense",
|
||||
"autocomplete",
|
||||
"vscode"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.33.0"
|
||||
},
|
||||
"categories": [
|
||||
"Linters",
|
||||
"Other"
|
||||
],
|
||||
"galleryBanner": {
|
||||
"color": "#f9fafb"
|
||||
},
|
||||
"icon": "media/icon.png",
|
||||
"activationEvents": [
|
||||
"workspaceContains:**/{tailwind,tailwind.config,tailwind-config,.tailwindrc}.{js,cjs}"
|
||||
],
|
||||
"main": "dist/extension/index.js",
|
||||
"contributes": {
|
||||
"grammars": [
|
||||
{
|
||||
"scopeName": "tailwindcss.injection",
|
||||
"path": "./syntaxes/tailwind.tmLanguage.json",
|
||||
"injectTo": [
|
||||
"source.css",
|
||||
"source.css.scss",
|
||||
"source.css.less",
|
||||
"source.css.postcss",
|
||||
"source.vue",
|
||||
"source.svelte",
|
||||
"text.html"
|
||||
]
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Tailwind CSS IntelliSense",
|
||||
"properties": {
|
||||
"tailwindCSS.emmetCompletions": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Enable class name completions when using Emmet-style syntax, for example `div.bg-red-500.uppercase`"
|
||||
},
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {},
|
||||
"markdownDescription": "Enable features in languages that are not supported by default. Add a mapping here between the new language and an already supported language.\n E.g.: `{\"plaintext\": \"html\"}`"
|
||||
},
|
||||
"tailwindCSS.colorDecorators": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"inherit",
|
||||
"on",
|
||||
"off"
|
||||
],
|
||||
"markdownEnumDescriptions": [
|
||||
"Color decorators are rendered if `editor.colorDecorators` is enabled.",
|
||||
"Color decorators are rendered.",
|
||||
"Color decorators are not rendered."
|
||||
],
|
||||
"default": "inherit",
|
||||
"markdownDescription": "Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.validate": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Enable linting. Rules can be configured individually using the `tailwindcss.lint.*` settings",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.cssConflict": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "warning",
|
||||
"markdownDescription": "Class names on the same HTML element which apply the same CSS property or properties",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidApply": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unsupported use of the [`@apply` directive](https://tailwindcss.com/docs/functions-and-directives/#apply)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidScreen": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown screen name used with the [`@screen` directive](https://tailwindcss.com/docs/functions-and-directives/#screen)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidVariant": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown variant name used with the [`@variants` directive](https://tailwindcss.com/docs/functions-and-directives/#variants)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidConfigPath": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown or invalid path used with the [`theme` helper](https://tailwindcss.com/docs/functions-and-directives/#theme)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.lint.invalidTailwindDirective": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ignore",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"default": "error",
|
||||
"markdownDescription": "Unknown value used with the [`@tailwind` directive](https://tailwindcss.com/docs/functions-and-directives/#tailwind)",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": {
|
||||
"type": "array",
|
||||
"scope": "language-overridable"
|
||||
},
|
||||
"tailwindCSS.showPixelEquivalents": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Show `px` equivalents for `rem` CSS values."
|
||||
},
|
||||
"tailwindCSS.rootFontSize": {
|
||||
"type": "number",
|
||||
"default": 16,
|
||||
"markdownDescription": "Root font size in pixels. Used to convert `rem` CSS values to their `px` equivalents. See `#tailwindCSS.showPixelEquivalents#`."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "glob-exec --foreach --parallel \"src/*.ts\" -- \"ncc build {{file}} --watch -o dist/{{file.toString().replace(/^src\\//, '').replace(/\\.ts$/, '')}}\"",
|
||||
"build": "glob-exec --foreach --parallel \"src/*.ts\" -- \"ncc build {{file}} -o dist/{{file.toString().replace(/^src\\//, '').replace(/\\.ts$/, '')}}\"",
|
||||
"minify": "glob-exec --foreach --parallel \"dist/**/*.js\" -- \"terser {{file}} --compress --mangle --output {{file.toString()}}\"",
|
||||
"package": "vsce package",
|
||||
"publish": "vsce publish",
|
||||
"vscode:prepublish": "npm run clean && npm run build && npm run minify",
|
||||
"clean": "rimraf dist",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/mocha": "^5.2.0",
|
||||
"@types/node": "^13.9.3",
|
||||
"@types/vscode": "^1.32.0",
|
||||
"@zeit/ncc": "^0.22.0",
|
||||
"bufferutil": "^4.0.2",
|
||||
"callsite": "^1.0.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"debounce": "^1.2.0",
|
||||
"dlv": "^1.1.3",
|
||||
"dset": "^2.0.1",
|
||||
"esm": "^3.2.25",
|
||||
"execa": "^3.4.0",
|
||||
"fast-glob": "^3.2.4",
|
||||
"find-up": "^5.0.0",
|
||||
"glob-exec": "^0.1.1",
|
||||
"import-from": "^3.0.0",
|
||||
"jest": "^25.5.4",
|
||||
"klona": "^2.0.4",
|
||||
"mitt": "^1.2.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"pkg-up": "^3.1.0",
|
||||
"postcss": "^7.0.27",
|
||||
"postcss-selector-parser": "^6.0.2",
|
||||
"resolve-from": "^5.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.2",
|
||||
"stack-trace": "0.0.10",
|
||||
"tailwindcss-language-service": "0.0.12",
|
||||
"terser": "^4.6.12",
|
||||
"tiny-invariant": "^1.1.0",
|
||||
"tslint": "^5.16.0",
|
||||
"typescript": "^3.8.3",
|
||||
"utf-8-validate": "^5.0.3",
|
||||
"vsce": "^1.76.1",
|
||||
"vscode-languageclient": "^6.1.3",
|
||||
"vscode-languageserver": "^6.1.1",
|
||||
"vscode-languageserver-textdocument": "^1.0.1",
|
||||
"vscode-uri": "^2.1.1"
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
import * as path from 'path' // if module is locally defined we path.resolve it
|
||||
import callsite from 'callsite'
|
||||
import Module from 'module'
|
||||
|
||||
function find(moduleName) {
|
||||
if (moduleName[0] === '.') {
|
||||
var stack = callsite()
|
||||
for (var i in stack) {
|
||||
var filename = stack[i].getFileName()
|
||||
// if (filename !== module.filename) {
|
||||
moduleName = path.resolve(path.dirname(filename), moduleName)
|
||||
break
|
||||
// }
|
||||
}
|
||||
}
|
||||
try {
|
||||
return __non_webpack_require__.resolve(moduleName)
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a module from the cache. We need this to re-load our http_request !
|
||||
* see: http://stackoverflow.com/a/14801711/1148249
|
||||
*/
|
||||
function decache(moduleName) {
|
||||
moduleName = find(moduleName)
|
||||
|
||||
if (!moduleName) {
|
||||
return
|
||||
}
|
||||
|
||||
// Run over the cache looking for the files
|
||||
// loaded by the specified module name
|
||||
searchCache(moduleName, function(mod) {
|
||||
delete __non_webpack_require__.cache[mod.id]
|
||||
})
|
||||
|
||||
// Remove cached paths to the module.
|
||||
// Thanks to @bentael for pointing this out.
|
||||
Object.keys(Module.prototype.constructor._pathCache).forEach(function(
|
||||
cacheKey
|
||||
) {
|
||||
if (cacheKey.indexOf(moduleName) > -1) {
|
||||
delete Module.prototype.constructor._pathCache[cacheKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs over the cache to search for all the cached
|
||||
* files
|
||||
*/
|
||||
function searchCache(moduleName, callback) {
|
||||
// Resolve the module identified by the specified name
|
||||
var mod = __non_webpack_require__.resolve(moduleName)
|
||||
var visited = {}
|
||||
|
||||
// Check if the module has been resolved and found within
|
||||
// the cache no else so #ignore else http://git.io/vtgMI
|
||||
/* istanbul ignore else */
|
||||
if (mod && (mod = __non_webpack_require__.cache[mod]) !== undefined) {
|
||||
// Recursively go over the results
|
||||
;(function run(current) {
|
||||
visited[current.id] = true
|
||||
// Go over each of the module's children and
|
||||
// run over it
|
||||
current.children.forEach(function(child) {
|
||||
// ignore .node files, decachine native modules throws a
|
||||
// "module did not self-register" error on second require
|
||||
if (path.extname(child.filename) !== '.node' && !visited[child.id]) {
|
||||
run(child)
|
||||
}
|
||||
})
|
||||
|
||||
// Call the specified callback providing the
|
||||
// found module
|
||||
callback(current)
|
||||
})(mod)
|
||||
}
|
||||
}
|
||||
|
||||
export default decache
|
|
@ -1,82 +0,0 @@
|
|||
import * as path from 'path'
|
||||
import findUp from 'find-up'
|
||||
import resolveFrom from 'resolve-from'
|
||||
import importFrom from 'import-from'
|
||||
|
||||
let isPnp
|
||||
let pnpApi
|
||||
|
||||
export function withUserEnvironment(base, root, cb) {
|
||||
if (isPnp === true) {
|
||||
return withPnpEnvironment(base, cb)
|
||||
}
|
||||
|
||||
if (isPnp === false) {
|
||||
return withNonPnpEnvironment(base, cb)
|
||||
}
|
||||
|
||||
const pnpPath = findUp.sync(
|
||||
(dir) => {
|
||||
let pnpFile = path.join(dir, '.pnp.js')
|
||||
if (findUp.sync.exists(pnpFile)) {
|
||||
return pnpFile
|
||||
}
|
||||
pnpFile = path.join(dir, '.pnp.cjs')
|
||||
if (findUp.sync.exists(pnpFile)) {
|
||||
return pnpFile
|
||||
}
|
||||
if (dir === root) {
|
||||
return findUp.stop
|
||||
}
|
||||
},
|
||||
{ cwd: base }
|
||||
)
|
||||
|
||||
if (pnpPath) {
|
||||
isPnp = true
|
||||
pnpApi = __non_webpack_require__(pnpPath)
|
||||
pnpApi.setup()
|
||||
} else {
|
||||
isPnp = false
|
||||
}
|
||||
|
||||
return withUserEnvironment(base, root, cb)
|
||||
}
|
||||
|
||||
function withPnpEnvironment(base, cb) {
|
||||
const pnpResolve = (request, from = base) => {
|
||||
return pnpApi.resolveRequest(request, from.replace(/\/$/, '') + '/')
|
||||
}
|
||||
|
||||
const pnpRequire = (request, from) => {
|
||||
return __non_webpack_require__(pnpResolve(request, from))
|
||||
}
|
||||
|
||||
const res = cb({ isPnp: true, resolve: pnpResolve, require: pnpRequire })
|
||||
|
||||
// check if it return a thenable
|
||||
if (res != null && res.then) {
|
||||
return res.then(
|
||||
(x) => {
|
||||
return x
|
||||
},
|
||||
(err) => {
|
||||
throw err
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
function withNonPnpEnvironment(base, cb) {
|
||||
return cb({
|
||||
isPnp: false,
|
||||
require(request, from = base) {
|
||||
return importFrom(from, request)
|
||||
},
|
||||
resolve(request, from = base) {
|
||||
return resolveFrom(from, request)
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
import * as path from 'path'
|
||||
import stackTrace from 'stack-trace'
|
||||
import pkgUp from 'pkg-up'
|
||||
import { isObject } from './isObject'
|
||||
import { withUserEnvironment } from './environment'
|
||||
|
||||
export async function getBuiltInPlugins({ base, root, resolvedConfig }) {
|
||||
return withUserEnvironment(base, root, ({ require, resolve }) => {
|
||||
const tailwindBase = path.dirname(resolve('tailwindcss/package.json'))
|
||||
try {
|
||||
return require('./lib/corePlugins.js', tailwindBase).default({
|
||||
corePlugins: resolvedConfig.corePlugins,
|
||||
})
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default function getPlugins(config) {
|
||||
let plugins = config.plugins
|
||||
|
||||
if (!Array.isArray(plugins)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return plugins.map((plugin) => {
|
||||
let pluginConfig = plugin.config
|
||||
if (!isObject(pluginConfig)) {
|
||||
pluginConfig = {}
|
||||
}
|
||||
|
||||
let contributes = {
|
||||
theme: isObject(pluginConfig.theme)
|
||||
? Object.keys(pluginConfig.theme)
|
||||
: [],
|
||||
variants: isObject(pluginConfig.variants)
|
||||
? Object.keys(pluginConfig.variants)
|
||||
: [],
|
||||
}
|
||||
|
||||
const fn = plugin.handler || plugin
|
||||
const fnName =
|
||||
typeof fn.name === 'string' && fn.name !== 'handler' && fn.name !== ''
|
||||
? fn.name
|
||||
: null
|
||||
|
||||
try {
|
||||
fn()
|
||||
} catch (e) {
|
||||
const trace = stackTrace.parse(e)
|
||||
if (trace.length === 0)
|
||||
return {
|
||||
name: fnName,
|
||||
}
|
||||
const file = trace[0].fileName
|
||||
const dir = path.dirname(file)
|
||||
let pkg = pkgUp.sync({ cwd: dir })
|
||||
if (!pkg)
|
||||
return {
|
||||
name: fnName,
|
||||
}
|
||||
try {
|
||||
pkg = __non_webpack_require__(pkg)
|
||||
} catch (_) {
|
||||
return {
|
||||
name: fnName,
|
||||
}
|
||||
}
|
||||
if (pkg.name && path.resolve(dir, pkg.main || 'index.js') === file) {
|
||||
return {
|
||||
name: pkg.name,
|
||||
homepage: pkg.homepage,
|
||||
contributes,
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: fnName,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import { runPlugin } from './runPlugin'
|
||||
import { getBuiltInPlugins } from './getPlugins'
|
||||
import { isObject } from './isObject'
|
||||
|
||||
const proxyHandler = (base = []) => ({
|
||||
get(target, key) {
|
||||
if (isObject(target[key])) {
|
||||
return new Proxy(target[key], proxyHandler([...base, key]))
|
||||
} else {
|
||||
if (
|
||||
[...base, key].every((x) => typeof x === 'string') &&
|
||||
target.hasOwnProperty(key)
|
||||
) {
|
||||
return '$dep$' + [...base, key].join('.')
|
||||
}
|
||||
return target[key]
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export async function getUtilityConfigMap({
|
||||
base,
|
||||
root,
|
||||
resolvedConfig,
|
||||
postcss,
|
||||
browserslist,
|
||||
}) {
|
||||
const builtInPlugins = await getBuiltInPlugins({ base, root, resolvedConfig })
|
||||
const userPlugins = Array.isArray(resolvedConfig.plugins)
|
||||
? resolvedConfig.plugins
|
||||
: []
|
||||
|
||||
try {
|
||||
const classNameConfigMap = {}
|
||||
const proxiedConfig = new Proxy(resolvedConfig, proxyHandler())
|
||||
|
||||
;[...builtInPlugins, ...userPlugins].forEach((plugin) => {
|
||||
runPlugin(plugin, {
|
||||
postcss,
|
||||
browserslist,
|
||||
config: proxiedConfig,
|
||||
addUtilities: (utilities) => {
|
||||
Object.keys(utilities).forEach((util) => {
|
||||
let props = Object.keys(utilities[util])
|
||||
if (
|
||||
props.length === 1 &&
|
||||
/^\.[^\s]+$/.test(util) &&
|
||||
typeof utilities[util][props[0]] === 'string' &&
|
||||
utilities[util][props[0]].substr(0, 5) === '$dep$'
|
||||
) {
|
||||
classNameConfigMap[util.substr(1)] = utilities[util][
|
||||
props[0]
|
||||
].substr(5)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return classNameConfigMap
|
||||
} catch (_) {
|
||||
return {}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import semver from 'semver'
|
||||
import { runPlugin } from './runPlugin'
|
||||
|
||||
export default function getVariants({
|
||||
config,
|
||||
version,
|
||||
postcss,
|
||||
browserslist,
|
||||
}) {
|
||||
let variants = ['responsive', 'hover']
|
||||
semver.gte(version, '0.3.0') && variants.push('focus', 'group-hover')
|
||||
semver.gte(version, '0.5.0') && variants.push('active')
|
||||
semver.gte(version, '0.7.0') && variants.push('focus-within')
|
||||
semver.gte(version, '1.0.0-beta.1') && variants.push('default')
|
||||
semver.gte(version, '1.1.0') &&
|
||||
variants.push('first', 'last', 'odd', 'even', 'disabled', 'visited')
|
||||
semver.gte(version, '1.3.0') && variants.push('group-focus')
|
||||
semver.gte(version, '1.5.0') && variants.push('focus-visible', 'checked')
|
||||
semver.gte(version, '1.6.0') && variants.push('motion-safe', 'motion-reduce')
|
||||
semver.gte(version, '2.0.0-alpha.1') && variants.push('dark')
|
||||
|
||||
let plugins = Array.isArray(config.plugins) ? config.plugins : []
|
||||
|
||||
plugins.forEach((plugin) => {
|
||||
runPlugin(plugin, {
|
||||
postcss,
|
||||
browserslist,
|
||||
config,
|
||||
addVariant: (name) => {
|
||||
variants.push(name)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return variants
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
/**
|
||||
* Adapted from: https://github.com/elastic/require-in-the-middle
|
||||
*/
|
||||
import Module from 'module'
|
||||
|
||||
export default function Hook(find, onrequire) {
|
||||
if (!(this instanceof Hook)) return new Hook(find, onrequire)
|
||||
|
||||
if (typeof Module._resolveFilename !== 'function') {
|
||||
throw new Error(
|
||||
`Error: Expected Module._resolveFilename to be a function (was: ${typeof Module._resolveFilename}) - aborting!`
|
||||
)
|
||||
}
|
||||
|
||||
this.cache = {}
|
||||
this.deps = []
|
||||
this._unhooked = false
|
||||
this._origRequire = Module.prototype.require
|
||||
|
||||
let self = this
|
||||
let patching = {}
|
||||
|
||||
this._require = Module.prototype.require = function(request) {
|
||||
if (self._unhooked) {
|
||||
// if the patched require function could not be removed because
|
||||
// someone else patched it after it was patched here, we just
|
||||
// abort and pass the request onwards to the original require
|
||||
return self._origRequire.apply(this, arguments)
|
||||
}
|
||||
|
||||
let filename = Module._resolveFilename(request, this)
|
||||
|
||||
// return known patched modules immediately
|
||||
if (self.cache.hasOwnProperty(filename)) {
|
||||
return self.cache[filename]
|
||||
}
|
||||
|
||||
// Check if this module has a patcher in-progress already.
|
||||
// Otherwise, mark this module as patching in-progress.
|
||||
let patched = patching[filename]
|
||||
if (!patched) {
|
||||
patching[filename] = true
|
||||
}
|
||||
|
||||
let exports = self._origRequire.apply(this, arguments)
|
||||
|
||||
if (filename !== find) {
|
||||
if (self._watching) {
|
||||
self.deps.push(filename)
|
||||
}
|
||||
return exports
|
||||
}
|
||||
|
||||
// If it's already patched, just return it as-is.
|
||||
if (patched) return exports
|
||||
|
||||
// The module has already been loaded,
|
||||
// so the patching mark can be cleaned up.
|
||||
delete patching[filename]
|
||||
|
||||
// only call onrequire the first time a module is loaded
|
||||
if (!self.cache.hasOwnProperty(filename)) {
|
||||
// ensure that the cache entry is assigned a value before calling
|
||||
// onrequire, in case calling onrequire requires the same module.
|
||||
self.cache[filename] = exports
|
||||
self.cache[filename] = onrequire(exports)
|
||||
}
|
||||
|
||||
return self.cache[filename]
|
||||
}
|
||||
}
|
||||
|
||||
Hook.prototype.unhook = function() {
|
||||
this._unhooked = true
|
||||
if (this._require === Module.prototype.require) {
|
||||
Module.prototype.require = this._origRequire
|
||||
}
|
||||
}
|
||||
|
||||
Hook.prototype.watch = function() {
|
||||
this._watching = true
|
||||
}
|
||||
|
||||
Hook.prototype.unwatch = function() {
|
||||
this._watching = false
|
||||
}
|
|
@ -1,353 +0,0 @@
|
|||
import extractClassNames from './extractClassNames'
|
||||
import Hook from './hook'
|
||||
import dlv from 'dlv'
|
||||
import dset from 'dset'
|
||||
import chokidar from 'chokidar'
|
||||
import semver from 'semver'
|
||||
import invariant from 'tiny-invariant'
|
||||
import getPlugins from './getPlugins'
|
||||
import getVariants from './getVariants'
|
||||
import resolveConfig from './resolveConfig'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import { getUtilityConfigMap } from './getUtilityConfigMap'
|
||||
import glob from 'fast-glob'
|
||||
import normalizePath from 'normalize-path'
|
||||
import { withUserEnvironment } from './environment'
|
||||
import execa from 'execa'
|
||||
import { klona } from 'klona/full'
|
||||
import { formatError } from '../lsp/util/formatError'
|
||||
|
||||
function arraysEqual(arr1, arr2) {
|
||||
return (
|
||||
JSON.stringify(arr1.concat([]).sort()) ===
|
||||
JSON.stringify(arr2.concat([]).sort())
|
||||
)
|
||||
}
|
||||
|
||||
const CONFIG_GLOB =
|
||||
'**/{tailwind,tailwind.config,tailwind-config,.tailwindrc}.{js,cjs}'
|
||||
|
||||
export default async function getClassNames(
|
||||
cwd = process.cwd(),
|
||||
{ onChange = () => {} } = {}
|
||||
) {
|
||||
async function run() {
|
||||
const configPaths = (
|
||||
await glob(CONFIG_GLOB, {
|
||||
cwd,
|
||||
ignore: ['**/node_modules'],
|
||||
onlyFiles: true,
|
||||
absolute: true,
|
||||
suppressErrors: true,
|
||||
// fast-glob defaults concurrency to `os.cpus().length`,
|
||||
// but this can be 0, so we override it here, ensuring
|
||||
// that concurrency is at least 1. Fix is here but is
|
||||
// currently unpublished:
|
||||
// https://github.com/mrmlnc/fast-glob/pull/296
|
||||
concurrency: Math.max(os.cpus().length, 1),
|
||||
})
|
||||
)
|
||||
.map(normalizePath)
|
||||
.sort((a, b) => a.split('/').length - b.split('/').length)
|
||||
.map(path.normalize)
|
||||
|
||||
invariant(configPaths.length > 0, 'No Tailwind CSS config found.')
|
||||
const configPath = configPaths[0]
|
||||
console.log(`Found Tailwind config file: ${configPath}`)
|
||||
const configDir = path.dirname(configPath)
|
||||
const {
|
||||
version,
|
||||
featureFlags = { future: [], experimental: [] },
|
||||
tailwindBase,
|
||||
} = loadMeta(configDir, cwd)
|
||||
|
||||
console.log(`Found tailwindcss v${version}: ${tailwindBase}`)
|
||||
|
||||
const sepLocation = semver.gte(version, '0.99.0')
|
||||
? ['separator']
|
||||
: ['options', 'separator']
|
||||
let userSeperator
|
||||
let userPurge
|
||||
let userMode
|
||||
let hook = Hook(fs.realpathSync(configPath), (exports) => {
|
||||
userSeperator = dlv(exports, sepLocation)
|
||||
userPurge = exports.purge
|
||||
userMode = exports.mode
|
||||
dset(
|
||||
exports,
|
||||
sepLocation,
|
||||
`__TWSEP__${
|
||||
typeof userSeperator === 'undefined' ? ':' : userSeperator
|
||||
}__TWSEP__`
|
||||
)
|
||||
exports.mode = 'aot'
|
||||
exports.purge = {}
|
||||
return exports
|
||||
})
|
||||
|
||||
hook.watch()
|
||||
let config
|
||||
try {
|
||||
config = __non_webpack_require__(configPath)
|
||||
} catch (error) {
|
||||
hook.unwatch()
|
||||
hook.unhook()
|
||||
throw error
|
||||
}
|
||||
|
||||
hook.unwatch()
|
||||
|
||||
const {
|
||||
postcssResult,
|
||||
resolvedConfig,
|
||||
browserslist,
|
||||
postcss,
|
||||
} = await withPackages(
|
||||
{
|
||||
configDir,
|
||||
cwd,
|
||||
userSeperator,
|
||||
version,
|
||||
},
|
||||
async ({
|
||||
postcss,
|
||||
tailwindcss,
|
||||
browserslistCommand,
|
||||
browserslistArgs,
|
||||
}) => {
|
||||
let postcssResult
|
||||
try {
|
||||
postcssResult = await postcss([tailwindcss(configPath)]).process(
|
||||
[
|
||||
semver.gte(version, '0.99.0') ? 'base' : 'preflight',
|
||||
'components',
|
||||
'utilities',
|
||||
]
|
||||
.map((x) => `/*__tw_intellisense_layer_${x}__*/\n@tailwind ${x};`)
|
||||
.join('\n'),
|
||||
{
|
||||
from: undefined,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
hook.unhook()
|
||||
}
|
||||
|
||||
if (typeof userSeperator !== 'undefined') {
|
||||
dset(config, sepLocation, userSeperator)
|
||||
} else {
|
||||
delete config[sepLocation]
|
||||
}
|
||||
if (typeof userPurge !== 'undefined') {
|
||||
config.purge = userPurge
|
||||
} else {
|
||||
delete config.purge
|
||||
}
|
||||
if (typeof userMode !== 'undefined') {
|
||||
config.mode = userMode
|
||||
} else {
|
||||
delete config.mode
|
||||
}
|
||||
|
||||
const resolvedConfig = resolveConfig({
|
||||
base: configDir,
|
||||
root: cwd,
|
||||
config,
|
||||
})
|
||||
|
||||
let browserslist = []
|
||||
if (
|
||||
browserslistCommand &&
|
||||
semver.gte(version, '1.4.0') &&
|
||||
semver.lte(version, '1.99.0')
|
||||
) {
|
||||
try {
|
||||
const { stdout } = await execa(
|
||||
browserslistCommand,
|
||||
browserslistArgs,
|
||||
{
|
||||
preferLocal: true,
|
||||
localDir: configDir,
|
||||
cwd: configDir,
|
||||
}
|
||||
)
|
||||
browserslist = stdout.split('\n')
|
||||
} catch (error) {
|
||||
console.error('Failed to load browserslist:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
postcssResult,
|
||||
resolvedConfig,
|
||||
postcss,
|
||||
browserslist,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
version,
|
||||
configPath,
|
||||
config: resolvedConfig,
|
||||
separator: typeof userSeperator === 'undefined' ? ':' : userSeperator,
|
||||
classNames: await extractClassNames(postcssResult.root),
|
||||
dependencies: hook.deps,
|
||||
plugins: getPlugins(config),
|
||||
variants: getVariants({ config, version, postcss, browserslist }),
|
||||
utilityConfigMap: await getUtilityConfigMap({
|
||||
base: configDir,
|
||||
root: cwd,
|
||||
resolvedConfig,
|
||||
postcss,
|
||||
browserslist,
|
||||
}),
|
||||
modules: {
|
||||
postcss,
|
||||
},
|
||||
featureFlags,
|
||||
}
|
||||
}
|
||||
|
||||
let watcher
|
||||
function watch(files = []) {
|
||||
unwatch()
|
||||
watcher = chokidar
|
||||
.watch(files, { cwd, ignorePermissionErrors: true })
|
||||
.on('change', handleChange)
|
||||
.on('unlink', handleChange)
|
||||
}
|
||||
function unwatch() {
|
||||
if (watcher) {
|
||||
watcher.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChange() {
|
||||
const prevDeps = result ? [result.configPath, ...result.dependencies] : []
|
||||
try {
|
||||
result = await run()
|
||||
} catch (error) {
|
||||
onChange({ error })
|
||||
return
|
||||
}
|
||||
const newDeps = [result.configPath, ...result.dependencies]
|
||||
if (!arraysEqual(prevDeps, newDeps)) {
|
||||
watch(newDeps)
|
||||
}
|
||||
onChange(result)
|
||||
}
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await run()
|
||||
console.log('Initialised successfully.')
|
||||
} catch (error) {
|
||||
console.error(formatError('Failed to initialise:', error))
|
||||
return null
|
||||
}
|
||||
|
||||
watch([result.configPath, ...result.dependencies])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function loadMeta(configDir, root) {
|
||||
return withUserEnvironment(configDir, root, ({ require, resolve }) => {
|
||||
const tailwindBase = path.dirname(resolve('tailwindcss/package.json'))
|
||||
const version = require('tailwindcss/package.json').version
|
||||
let featureFlags
|
||||
|
||||
try {
|
||||
featureFlags = require('./lib/featureFlags.js', tailwindBase).default
|
||||
} catch (_) {}
|
||||
|
||||
return { version, featureFlags, tailwindBase }
|
||||
})
|
||||
}
|
||||
|
||||
function withPackages({ configDir, cwd, userSeperator, version }, cb) {
|
||||
return withUserEnvironment(
|
||||
configDir,
|
||||
cwd,
|
||||
async ({ isPnp, require, resolve }) => {
|
||||
const tailwindBase = path.dirname(resolve('tailwindcss/package.json'))
|
||||
const postcss = require('postcss', tailwindBase)
|
||||
const tailwindcss = require('tailwindcss')
|
||||
|
||||
let browserslistCommand
|
||||
let browserslistArgs = []
|
||||
try {
|
||||
const browserslistBin = resolve(
|
||||
path.join(
|
||||
'browserslist',
|
||||
require('browserslist/package.json', tailwindBase).bin.browserslist
|
||||
),
|
||||
tailwindBase
|
||||
)
|
||||
if (isPnp) {
|
||||
browserslistCommand = 'yarn'
|
||||
browserslistArgs = ['node', browserslistBin]
|
||||
} else {
|
||||
browserslistCommand = process.execPath
|
||||
browserslistArgs = [browserslistBin]
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (semver.gte(version, '1.7.0')) {
|
||||
const applyComplexClasses = semver.gte(version, '1.99.0')
|
||||
? require('./lib/lib/substituteClassApplyAtRules', tailwindBase)
|
||||
: require('./lib/flagged/applyComplexClasses', tailwindBase)
|
||||
|
||||
if (!applyComplexClasses.default.__patched) {
|
||||
let _applyComplexClasses = applyComplexClasses.default
|
||||
applyComplexClasses.default = (config, ...args) => {
|
||||
let configClone = klona(config)
|
||||
configClone.separator =
|
||||
typeof userSeperator === 'undefined' ? ':' : userSeperator
|
||||
|
||||
let fn = _applyComplexClasses(configClone, ...args)
|
||||
|
||||
return async (css) => {
|
||||
css.walkRules((rule) => {
|
||||
const newSelector = rule.selector.replace(
|
||||
/__TWSEP__(.*?)__TWSEP__/g,
|
||||
'$1'
|
||||
)
|
||||
if (newSelector !== rule.selector) {
|
||||
rule.before(
|
||||
postcss.comment({
|
||||
text: '__ORIGINAL_SELECTOR__:' + rule.selector,
|
||||
})
|
||||
)
|
||||
rule.selector = newSelector
|
||||
}
|
||||
})
|
||||
|
||||
await fn(css)
|
||||
|
||||
css.walkComments((comment) => {
|
||||
if (comment.text.startsWith('__ORIGINAL_SELECTOR__:')) {
|
||||
comment.next().selector = comment.text.replace(
|
||||
/^__ORIGINAL_SELECTOR__:/,
|
||||
''
|
||||
)
|
||||
comment.remove()
|
||||
}
|
||||
})
|
||||
|
||||
return css
|
||||
}
|
||||
}
|
||||
applyComplexClasses.default.__patched = true
|
||||
}
|
||||
}
|
||||
|
||||
return cb({ postcss, tailwindcss, browserslistCommand, browserslistArgs })
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export function isObject(thing) {
|
||||
return Object.prototype.toString.call(thing) === '[object Object]'
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import * as path from 'path'
|
||||
import decache from './decache'
|
||||
import { withUserEnvironment } from './environment'
|
||||
|
||||
export default function resolveConfig({ base, root, config }) {
|
||||
if (typeof config === 'string') {
|
||||
if (!cwd) {
|
||||
cwd = path.dirname(config)
|
||||
}
|
||||
decache(config)
|
||||
config = __non_webpack_require__(config)
|
||||
}
|
||||
|
||||
return withUserEnvironment(base, root, ({ require, resolve }) => {
|
||||
let resolveConfigFn = (config) => config
|
||||
const tailwindBase = path.dirname(resolve('tailwindcss/package.json'))
|
||||
try {
|
||||
resolveConfigFn = require('./resolveConfig.js', tailwindBase)
|
||||
} catch (_) {
|
||||
try {
|
||||
const resolveConfig = require('./lib/util/resolveConfig.js', tailwindBase)
|
||||
const defaultConfig = require('./stubs/defaultConfig.stub.js', tailwindBase)
|
||||
resolveConfigFn = (config) => resolveConfig([config, defaultConfig])
|
||||
} catch (_) {
|
||||
try {
|
||||
const resolveConfig = require('./lib/util/mergeConfigWithDefaults.js', tailwindBase)
|
||||
.default
|
||||
const defaultConfig = require('./defaultConfig.js', tailwindBase)()
|
||||
resolveConfigFn = (config) => resolveConfig(config, defaultConfig)
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
return resolveConfigFn(config)
|
||||
})
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import dlv from 'dlv'
|
||||
|
||||
export function runPlugin(plugin, params = {}) {
|
||||
const { config, browserslist, ...rest } = params
|
||||
|
||||
const browserslistTarget =
|
||||
browserslist && browserslist.includes('ie 11') ? 'ie11' : 'relaxed'
|
||||
|
||||
try {
|
||||
;(plugin.handler || plugin)({
|
||||
addUtilities: () => {},
|
||||
addComponents: () => {},
|
||||
addBase: () => {},
|
||||
addVariant: () => {},
|
||||
e: (x) => x,
|
||||
prefix: (x) => x,
|
||||
theme: (path, defaultValue) => dlv(config, `theme.${path}`, defaultValue),
|
||||
variants: () => [],
|
||||
config: (path, defaultValue) => dlv(config, path, defaultValue),
|
||||
corePlugins: (path) => {
|
||||
if (Array.isArray(config.corePlugins)) {
|
||||
return config.corePlugins.includes(path)
|
||||
}
|
||||
return dlv(config, `corePlugins.${path}`, true)
|
||||
},
|
||||
target: (path) => {
|
||||
if (typeof config.target === 'string') {
|
||||
return config.target === 'browserslist'
|
||||
? browserslistTarget
|
||||
: config.target
|
||||
}
|
||||
const [defaultTarget, targetOverrides] = dlv(config, 'target')
|
||||
const target = dlv(targetOverrides, path, defaultTarget)
|
||||
return target === 'browserslist' ? browserslistTarget : target
|
||||
},
|
||||
...rest,
|
||||
})
|
||||
} catch (_) {}
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
/* --------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
import * as path from 'path'
|
||||
import {
|
||||
workspace as Workspace,
|
||||
window as Window,
|
||||
ExtensionContext,
|
||||
TextDocument,
|
||||
OutputChannel,
|
||||
WorkspaceFolder,
|
||||
Uri,
|
||||
ConfigurationScope,
|
||||
commands,
|
||||
SymbolInformation,
|
||||
} from 'vscode'
|
||||
import {
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
TransportKind,
|
||||
} from 'vscode-languageclient'
|
||||
import { registerConfigErrorHandler } from './lib/registerConfigErrorHandler'
|
||||
import { DEFAULT_LANGUAGES } from './lib/languages'
|
||||
import isObject from './util/isObject'
|
||||
import { dedupe, equal } from './util/array'
|
||||
import { createEmitter } from './lib/emitter'
|
||||
import { onMessage } from './lsp/notifications'
|
||||
import { registerColorDecorator } from './lib/registerColorDecorator'
|
||||
|
||||
const CLIENT_ID = 'tailwindcss-intellisense'
|
||||
const CLIENT_NAME = 'Tailwind CSS IntelliSense'
|
||||
|
||||
let clients: Map<string, LanguageClient> = new Map()
|
||||
let languages: Map<string, string[]> = new Map()
|
||||
|
||||
let _sortedWorkspaceFolders: string[] | undefined
|
||||
function sortedWorkspaceFolders(): string[] {
|
||||
if (_sortedWorkspaceFolders === void 0) {
|
||||
_sortedWorkspaceFolders = Workspace.workspaceFolders
|
||||
? Workspace.workspaceFolders
|
||||
.map((folder) => {
|
||||
let result = folder.uri.toString()
|
||||
if (result.charAt(result.length - 1) !== '/') {
|
||||
result = result + '/'
|
||||
}
|
||||
return result
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.length - b.length
|
||||
})
|
||||
: []
|
||||
}
|
||||
return _sortedWorkspaceFolders
|
||||
}
|
||||
Workspace.onDidChangeWorkspaceFolders(
|
||||
() => (_sortedWorkspaceFolders = undefined)
|
||||
)
|
||||
|
||||
function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder {
|
||||
let sorted = sortedWorkspaceFolders()
|
||||
for (let element of sorted) {
|
||||
let uri = folder.uri.toString()
|
||||
if (uri.charAt(uri.length - 1) !== '/') {
|
||||
uri = uri + '/'
|
||||
}
|
||||
if (uri.startsWith(element)) {
|
||||
return Workspace.getWorkspaceFolder(Uri.parse(element))!
|
||||
}
|
||||
}
|
||||
return folder
|
||||
}
|
||||
|
||||
function getUserLanguages(folder?: WorkspaceFolder): Record<string, string> {
|
||||
const langs = Workspace.getConfiguration('tailwindCSS', folder)
|
||||
.includeLanguages
|
||||
return isObject(langs) ? langs : {}
|
||||
}
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
let module = context.asAbsolutePath(path.join('dist', 'server', 'index.js'))
|
||||
let outputChannel: OutputChannel = Window.createOutputChannel(CLIENT_NAME)
|
||||
|
||||
// TODO: check if the actual language MAPPING changed
|
||||
// not just the language IDs
|
||||
// e.g. "plaintext" already exists but you change it from "html" to "css"
|
||||
Workspace.onDidChangeConfiguration((event) => {
|
||||
clients.forEach((client, key) => {
|
||||
const folder = Workspace.getWorkspaceFolder(Uri.parse(key))
|
||||
|
||||
if (event.affectsConfiguration('tailwindCSS', folder)) {
|
||||
const userLanguages = getUserLanguages(folder)
|
||||
if (userLanguages) {
|
||||
const userLanguageIds = Object.keys(userLanguages)
|
||||
const newLanguages = dedupe([
|
||||
...DEFAULT_LANGUAGES,
|
||||
...userLanguageIds,
|
||||
])
|
||||
if (!equal(newLanguages, languages.get(folder.uri.toString()))) {
|
||||
languages.set(folder.uri.toString(), newLanguages)
|
||||
|
||||
if (client) {
|
||||
clients.delete(folder.uri.toString())
|
||||
client.stop()
|
||||
bootWorkspaceClient(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function bootWorkspaceClient(folder: WorkspaceFolder) {
|
||||
if (clients.has(folder.uri.toString())) {
|
||||
return
|
||||
}
|
||||
|
||||
// placeholder so we don't boot another server before this one is ready
|
||||
clients.set(folder.uri.toString(), null)
|
||||
|
||||
let debugOptions = {
|
||||
execArgv: ['--nolazy', `--inspect=${6011 + clients.size}`],
|
||||
}
|
||||
let serverOptions = {
|
||||
run: { module, transport: TransportKind.ipc },
|
||||
debug: {
|
||||
module,
|
||||
transport: TransportKind.ipc,
|
||||
options: debugOptions,
|
||||
},
|
||||
}
|
||||
let clientOptions: LanguageClientOptions = {
|
||||
documentSelector: languages
|
||||
.get(folder.uri.toString())
|
||||
.map((language) => ({
|
||||
scheme: 'file',
|
||||
language,
|
||||
pattern: `${folder.uri.fsPath}/**/*`,
|
||||
})),
|
||||
diagnosticCollectionName: CLIENT_ID,
|
||||
workspaceFolder: folder,
|
||||
outputChannel: outputChannel,
|
||||
middleware: {},
|
||||
initializationOptions: {
|
||||
userLanguages: getUserLanguages(folder),
|
||||
},
|
||||
}
|
||||
let client = new LanguageClient(
|
||||
CLIENT_ID,
|
||||
CLIENT_NAME,
|
||||
serverOptions,
|
||||
clientOptions
|
||||
)
|
||||
|
||||
client.onReady().then(() => {
|
||||
let emitter = createEmitter(client)
|
||||
registerConfigErrorHandler(emitter)
|
||||
registerColorDecorator(client, context, emitter)
|
||||
|
||||
onMessage(client, 'getConfiguration', async (scope) => {
|
||||
return {
|
||||
tabSize:
|
||||
Workspace.getConfiguration('editor', scope).get('tabSize') || 2,
|
||||
...Workspace.getConfiguration('tailwindCSS', scope),
|
||||
}
|
||||
})
|
||||
|
||||
onMessage(client, 'getDocumentSymbols', async ({ uri }) => {
|
||||
return {
|
||||
symbols: await commands.executeCommand<SymbolInformation[]>(
|
||||
'vscode.executeDocumentSymbolProvider',
|
||||
Uri.parse(uri)
|
||||
),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
client.start()
|
||||
clients.set(folder.uri.toString(), client)
|
||||
}
|
||||
|
||||
function didOpenTextDocument(document: TextDocument): void {
|
||||
// We are only interested in language mode text
|
||||
if (document.uri.scheme !== 'file') {
|
||||
return
|
||||
}
|
||||
|
||||
let uri = document.uri
|
||||
let folder = Workspace.getWorkspaceFolder(uri)
|
||||
// Files outside a folder can't be handled. This might depend on the language.
|
||||
// Single file languages like JSON might handle files outside the workspace folders.
|
||||
if (!folder) {
|
||||
return
|
||||
}
|
||||
// If we have nested workspace folders we only start a server on the outer most workspace folder.
|
||||
folder = getOuterMostWorkspaceFolder(folder)
|
||||
|
||||
if (!languages.has(folder.uri.toString())) {
|
||||
languages.set(
|
||||
folder.uri.toString(),
|
||||
dedupe([...DEFAULT_LANGUAGES, ...Object.keys(getUserLanguages())])
|
||||
)
|
||||
}
|
||||
|
||||
bootWorkspaceClient(folder)
|
||||
}
|
||||
|
||||
Workspace.onDidOpenTextDocument(didOpenTextDocument)
|
||||
Workspace.textDocuments.forEach(didOpenTextDocument)
|
||||
Workspace.onDidChangeWorkspaceFolders((event) => {
|
||||
for (let folder of event.removed) {
|
||||
let client = clients.get(folder.uri.toString())
|
||||
if (client) {
|
||||
clients.delete(folder.uri.toString())
|
||||
client.stop()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> {
|
||||
let promises: Thenable<void>[] = []
|
||||
for (let client of clients.values()) {
|
||||
promises.push(client.stop())
|
||||
}
|
||||
return Promise.all(promises).then(() => undefined)
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import mitt from 'mitt'
|
||||
import { LanguageClient } from 'vscode-languageclient'
|
||||
import crypto from 'crypto'
|
||||
import { Connection } from 'vscode-languageserver'
|
||||
|
||||
export interface NotificationEmitter {
|
||||
on: (name: string, handler: (args: any) => void) => void
|
||||
off: (name: string, handler: (args: any) => void) => void
|
||||
emit: (name: string, args: any) => Promise<any>
|
||||
}
|
||||
|
||||
export function createEmitter(
|
||||
client: LanguageClient | Connection
|
||||
): NotificationEmitter {
|
||||
const emitter = mitt()
|
||||
const registered: string[] = []
|
||||
|
||||
const on = (name: string, handler: (args: any) => void) => {
|
||||
if (!registered.includes(name)) {
|
||||
registered.push(name)
|
||||
client.onNotification(`tailwindcss/${name}`, (args) =>
|
||||
emitter.emit(name, args)
|
||||
)
|
||||
}
|
||||
emitter.on(name, handler)
|
||||
}
|
||||
|
||||
const off = (name: string, handler: (args: any) => void) => {
|
||||
emitter.off(name, handler)
|
||||
}
|
||||
|
||||
const emit = (name: string, params: Record<string, any> = {}) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
const id = crypto.randomBytes(16).toString('hex')
|
||||
on(`${name}Response`, (result) => {
|
||||
const { _id, ...rest } = result
|
||||
if (_id === id) {
|
||||
resolve(rest)
|
||||
}
|
||||
})
|
||||
client.sendNotification(`tailwindcss/${name}`, {
|
||||
_id: id,
|
||||
...params,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
on,
|
||||
off,
|
||||
emit,
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
import { window, workspace, ExtensionContext, TextEditor } from 'vscode'
|
||||
import { NotificationEmitter } from './emitter'
|
||||
import { LanguageClient } from 'vscode-languageclient'
|
||||
import debounce from 'debounce'
|
||||
|
||||
const colorDecorationType = window.createTextEditorDecorationType({
|
||||
before: {
|
||||
width: '0.8em',
|
||||
height: '0.8em',
|
||||
contentText: ' ',
|
||||
border: '0.1em solid',
|
||||
margin: '0.1em 0.2em 0',
|
||||
},
|
||||
dark: {
|
||||
before: {
|
||||
borderColor: '#eeeeee',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
before: {
|
||||
borderColor: '#000000',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function registerColorDecorator(
|
||||
client: LanguageClient,
|
||||
context: ExtensionContext,
|
||||
emitter: NotificationEmitter
|
||||
) {
|
||||
let activeEditor = window.activeTextEditor
|
||||
|
||||
async function updateDecorations() {
|
||||
return updateDecorationsInEditor(activeEditor)
|
||||
}
|
||||
|
||||
async function updateDecorationsInEditor(editor: TextEditor) {
|
||||
if (!editor) return
|
||||
if (editor.document.uri.scheme !== 'file') return
|
||||
|
||||
let workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri)
|
||||
if (
|
||||
!workspaceFolder ||
|
||||
workspaceFolder.uri.toString() !==
|
||||
client.clientOptions.workspaceFolder.uri.toString()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let preference =
|
||||
workspace.getConfiguration('tailwindCSS', editor.document)
|
||||
.colorDecorators || 'inherit'
|
||||
|
||||
let enabled: boolean =
|
||||
preference === 'inherit'
|
||||
? Boolean(workspace.getConfiguration('editor').colorDecorators)
|
||||
: preference === 'on'
|
||||
|
||||
if (!enabled) {
|
||||
editor.setDecorations(colorDecorationType, [])
|
||||
return
|
||||
}
|
||||
|
||||
let { colors } = await emitter.emit('getDocumentColors', {
|
||||
document: editor.document.uri.toString(),
|
||||
})
|
||||
|
||||
editor.setDecorations(
|
||||
colorDecorationType,
|
||||
colors
|
||||
.filter(({ color }) => color !== 'rgba(0, 0, 0, 0.01)')
|
||||
.map(({ range, color }) => ({
|
||||
range,
|
||||
renderOptions: { before: { backgroundColor: color } },
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
const triggerUpdateDecorations = debounce(updateDecorations, 200)
|
||||
|
||||
if (activeEditor) {
|
||||
triggerUpdateDecorations()
|
||||
}
|
||||
|
||||
window.onDidChangeActiveTextEditor(
|
||||
(editor) => {
|
||||
activeEditor = editor
|
||||
if (editor) {
|
||||
triggerUpdateDecorations()
|
||||
}
|
||||
},
|
||||
null,
|
||||
context.subscriptions
|
||||
)
|
||||
|
||||
workspace.onDidChangeTextDocument(
|
||||
(event) => {
|
||||
if (activeEditor && event.document === activeEditor.document) {
|
||||
triggerUpdateDecorations()
|
||||
}
|
||||
},
|
||||
null,
|
||||
context.subscriptions
|
||||
)
|
||||
|
||||
workspace.onDidOpenTextDocument(
|
||||
(document) => {
|
||||
if (activeEditor && document === activeEditor.document) {
|
||||
triggerUpdateDecorations()
|
||||
}
|
||||
},
|
||||
null,
|
||||
context.subscriptions
|
||||
)
|
||||
|
||||
workspace.onDidChangeConfiguration((e) => {
|
||||
if (
|
||||
e.affectsConfiguration('editor.colorDecorators') ||
|
||||
e.affectsConfiguration('tailwindCSS.colorDecorators')
|
||||
) {
|
||||
window.visibleTextEditors.forEach(updateDecorationsInEditor)
|
||||
}
|
||||
})
|
||||
|
||||
emitter.on('configUpdated', () => {
|
||||
window.visibleTextEditors.forEach(updateDecorationsInEditor)
|
||||
})
|
||||
|
||||
emitter.on('configError', () => {
|
||||
window.visibleTextEditors.forEach(updateDecorationsInEditor)
|
||||
})
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import { window, Uri, Range, Position } from 'vscode'
|
||||
import { NotificationEmitter } from './emitter'
|
||||
|
||||
export function registerConfigErrorHandler(emitter: NotificationEmitter) {
|
||||
emitter.on('configError', async ({ message, file, line }) => {
|
||||
const actions: string[] = file ? ['View'] : []
|
||||
const action = await window.showErrorMessage(
|
||||
`Tailwind CSS: ${message}`,
|
||||
...actions
|
||||
)
|
||||
if (action === 'View') {
|
||||
window.showTextDocument(Uri.file(file), {
|
||||
selection: new Range(
|
||||
new Position(line - 1, 0),
|
||||
new Position(line - 1, 0)
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { Connection } from 'vscode-languageserver'
|
||||
import { LanguageClient } from 'vscode-languageclient'
|
||||
|
||||
export function onMessage(
|
||||
connection: LanguageClient | Connection,
|
||||
name: string,
|
||||
handler: (params: any) => Thenable<Record<string, any>>
|
||||
): void {
|
||||
connection.onNotification(`tailwindcss/${name}`, async (params: any) => {
|
||||
const { _id, ...rest } = params
|
||||
connection.sendNotification(`tailwindcss/${name}Response`, {
|
||||
_id,
|
||||
...(await handler(rest)),
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { onMessage } from '../notifications'
|
||||
import { State } from '../util/state'
|
||||
import { getDocumentColors } from 'tailwindcss-language-service'
|
||||
|
||||
export function registerDocumentColorProvider(state: State) {
|
||||
onMessage(
|
||||
state.editor.connection,
|
||||
'getDocumentColors',
|
||||
async ({ document }) => {
|
||||
let doc = state.editor.documents.get(document)
|
||||
if (!doc) return { colors: [] }
|
||||
|
||||
return { colors: await getDocumentColors(state, doc) }
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
/* --------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
|
||||
import {
|
||||
createConnection,
|
||||
TextDocuments,
|
||||
ProposedFeatures,
|
||||
TextDocumentSyncKind,
|
||||
CompletionItem,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
CompletionParams,
|
||||
CompletionList,
|
||||
Hover,
|
||||
TextDocumentPositionParams,
|
||||
DidChangeConfigurationNotification,
|
||||
CodeActionParams,
|
||||
CodeAction,
|
||||
} from 'vscode-languageserver'
|
||||
import getTailwindState from '../class-names/index'
|
||||
import { State, Settings, EditorState } from 'tailwindcss-language-service'
|
||||
import {
|
||||
resolveCompletionItem,
|
||||
doComplete,
|
||||
doHover,
|
||||
doCodeActions,
|
||||
} from 'tailwindcss-language-service'
|
||||
import { URI } from 'vscode-uri'
|
||||
import {
|
||||
provideDiagnostics,
|
||||
updateAllDiagnostics,
|
||||
clearAllDiagnostics,
|
||||
} from './providers/diagnostics/diagnosticsProvider'
|
||||
import { createEmitter } from '../lib/emitter'
|
||||
import { registerDocumentColorProvider } from './providers/documentColorProvider'
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||
import { formatError } from './util/formatError'
|
||||
|
||||
let connection = createConnection(ProposedFeatures.all)
|
||||
const state: State = { enabled: false, emitter: createEmitter(connection) }
|
||||
let documents = new TextDocuments(TextDocument)
|
||||
let workspaceFolder: string | null
|
||||
|
||||
console.log = connection.console.log.bind(connection.console)
|
||||
console.error = connection.console.error.bind(connection.console)
|
||||
|
||||
process.on('unhandledRejection', (e: any) => {
|
||||
connection.console.error(formatError(`Unhandled exception`, e))
|
||||
})
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
tabSize: 2,
|
||||
emmetCompletions: false,
|
||||
includeLanguages: {},
|
||||
experimental: {
|
||||
classRegex: [],
|
||||
},
|
||||
showPixelEquivalents: true,
|
||||
rootFontSize: 16,
|
||||
validate: true,
|
||||
lint: {
|
||||
cssConflict: 'warning',
|
||||
invalidApply: 'error',
|
||||
invalidScreen: 'error',
|
||||
invalidVariant: 'error',
|
||||
invalidConfigPath: 'error',
|
||||
invalidTailwindDirective: 'error',
|
||||
},
|
||||
}
|
||||
let globalSettings: Settings = defaultSettings
|
||||
let documentSettings: Map<string, Settings> = new Map()
|
||||
|
||||
documents.onDidClose((event) => {
|
||||
documentSettings.delete(event.document.uri)
|
||||
})
|
||||
documents.onDidChangeContent((change) => {
|
||||
if (!state.enabled) return
|
||||
provideDiagnostics(state, change.document)
|
||||
})
|
||||
documents.listen(connection)
|
||||
|
||||
connection.onInitialize(
|
||||
async (params: InitializeParams): Promise<InitializeResult> => {
|
||||
const capabilities = params.capabilities
|
||||
|
||||
state.editor = {
|
||||
connection,
|
||||
documents,
|
||||
documentSettings,
|
||||
globalSettings,
|
||||
userLanguages:
|
||||
params.initializationOptions &&
|
||||
params.initializationOptions.userLanguages
|
||||
? params.initializationOptions.userLanguages
|
||||
: {},
|
||||
capabilities: {
|
||||
configuration:
|
||||
capabilities.workspace && !!capabilities.workspace.configuration,
|
||||
diagnosticRelatedInformation:
|
||||
capabilities.textDocument &&
|
||||
capabilities.textDocument.publishDiagnostics &&
|
||||
capabilities.textDocument.publishDiagnostics.relatedInformation,
|
||||
},
|
||||
}
|
||||
|
||||
const tailwindState = await getTailwindState(
|
||||
params.rootPath || URI.parse(params.rootUri).path,
|
||||
{
|
||||
// @ts-ignore
|
||||
onChange: (newState: State): void => {
|
||||
if (newState && !newState.error) {
|
||||
Object.assign(state, newState, { enabled: true })
|
||||
connection.sendNotification('tailwindcss/configUpdated', [
|
||||
state.configPath,
|
||||
state.config,
|
||||
state.plugins,
|
||||
])
|
||||
updateAllDiagnostics(state)
|
||||
} else {
|
||||
state.enabled = false
|
||||
if (newState && newState.error) {
|
||||
const payload: {
|
||||
message: string
|
||||
file?: string
|
||||
line?: number
|
||||
} = { message: newState.error.message }
|
||||
const lines = newState.error.stack.toString().split('\n')
|
||||
const match = /^(?<file>.*?):(?<line>[0-9]+)$/.exec(lines[0])
|
||||
if (match) {
|
||||
payload.file = match.groups.file
|
||||
payload.line = parseInt(match.groups.line, 10)
|
||||
}
|
||||
connection.sendNotification('tailwindcss/configError', [payload])
|
||||
}
|
||||
clearAllDiagnostics(state)
|
||||
// TODO
|
||||
// connection.sendNotification('tailwindcss/configUpdated', [null])
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (tailwindState) {
|
||||
Object.assign(state, tailwindState, { enabled: true })
|
||||
} else {
|
||||
state.enabled = false
|
||||
}
|
||||
|
||||
return {
|
||||
capabilities: {
|
||||
textDocumentSync: TextDocumentSyncKind.Full,
|
||||
completionProvider: {
|
||||
resolveProvider: true,
|
||||
triggerCharacters: [
|
||||
// class attributes
|
||||
'"',
|
||||
"'",
|
||||
'`',
|
||||
// between class names
|
||||
' ',
|
||||
// @apply and emmet-style
|
||||
'.',
|
||||
// config/theme helper
|
||||
'[',
|
||||
// TODO: restart server if separater changes?
|
||||
typeof state.separator === 'undefined' ? ':' : state.separator,
|
||||
],
|
||||
},
|
||||
hoverProvider: true,
|
||||
codeActionProvider: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
connection.onInitialized &&
|
||||
connection.onInitialized(async () => {
|
||||
if (state.editor.capabilities.configuration) {
|
||||
connection.client.register(
|
||||
DidChangeConfigurationNotification.type,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
connection.sendNotification('tailwindcss/configUpdated', [
|
||||
state.configPath,
|
||||
state.config,
|
||||
state.plugins,
|
||||
])
|
||||
|
||||
registerDocumentColorProvider(state)
|
||||
})
|
||||
|
||||
connection.onDidChangeConfiguration((change) => {
|
||||
if (state.editor.capabilities.configuration) {
|
||||
// Reset all cached document settings
|
||||
state.editor.documentSettings.clear()
|
||||
} else {
|
||||
state.editor.globalSettings = <Settings>(
|
||||
(change.settings.tailwindCSS || defaultSettings)
|
||||
)
|
||||
}
|
||||
|
||||
updateAllDiagnostics(state)
|
||||
})
|
||||
|
||||
connection.onCompletion(
|
||||
(params: CompletionParams): Promise<CompletionList> => {
|
||||
if (!state.enabled) return null
|
||||
let document = state.editor.documents.get(params.textDocument.uri)
|
||||
if (!document) return null
|
||||
return doComplete(state, document, params.position)
|
||||
}
|
||||
)
|
||||
|
||||
connection.onCompletionResolve(
|
||||
(item: CompletionItem): Promise<CompletionItem> => {
|
||||
if (!state.enabled) return null
|
||||
return resolveCompletionItem(state, item)
|
||||
}
|
||||
)
|
||||
|
||||
connection.onHover(
|
||||
(params: TextDocumentPositionParams): Promise<Hover> => {
|
||||
if (!state.enabled) return null
|
||||
let document = state.editor.documents.get(params.textDocument.uri)
|
||||
if (!document) return null
|
||||
return doHover(state, document, params.position)
|
||||
}
|
||||
)
|
||||
|
||||
connection.onCodeAction(
|
||||
(params: CodeActionParams): Promise<CodeAction[]> => {
|
||||
if (!state.enabled) return null
|
||||
return doCodeActions(state, params)
|
||||
}
|
||||
)
|
||||
|
||||
connection.listen()
|
|
@ -1,12 +0,0 @@
|
|||
// https://github.com/vscode-langservers/vscode-json-languageserver/blob/master/src/utils/runner.ts
|
||||
export function formatError(message: string, err: any): string {
|
||||
if (err instanceof Error) {
|
||||
let error = <Error>err
|
||||
return `${message}: ${error.message}\n${error.stack}`
|
||||
} else if (typeof err === 'string') {
|
||||
return `${message}: ${err}`
|
||||
} else if (err) {
|
||||
return `${message}: ${err.toString()}`
|
||||
}
|
||||
return message
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from 'tailwindcss-language-service'
|
|
@ -1 +0,0 @@
|
|||
import './lsp/server'
|
|
@ -1,28 +0,0 @@
|
|||
export function dedupe<T>(arr: Array<T>): Array<T> {
|
||||
return arr.filter((value, index, self) => self.indexOf(value) === index)
|
||||
}
|
||||
|
||||
export function dedupeBy<T>(
|
||||
arr: Array<T>,
|
||||
transform: (item: T) => any
|
||||
): Array<T> {
|
||||
return arr.filter(
|
||||
(value, index, self) =>
|
||||
self.map(transform).indexOf(transform(value)) === index
|
||||
)
|
||||
}
|
||||
|
||||
export function ensureArray<T>(value: T | T[]): T[] {
|
||||
return Array.isArray(value) ? value : [value]
|
||||
}
|
||||
|
||||
export function flatten<T>(arrays: T[][]): T[] {
|
||||
return [].concat.apply([], arrays)
|
||||
}
|
||||
|
||||
export function equal(arr1: any[], arr2: any[]): boolean {
|
||||
return (
|
||||
JSON.stringify(arr1.concat([]).sort()) ===
|
||||
JSON.stringify(arr2.concat([]).sort())
|
||||
)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"rootDir": "../",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["src", "../tailwindcss-language-service"]
|
||||
}
|
|
@ -14,23 +14,27 @@
|
|||
"lint": "tsdx lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.1.4",
|
||||
"@types/moo": "^0.5.3",
|
||||
"css.escape": "^1.5.1",
|
||||
"detect-indent": "^6.0.0",
|
||||
"dlv": "^1.1.3",
|
||||
"line-column": "^1.0.2",
|
||||
"mitt": "^2.1.0",
|
||||
"moo": "^0.5.1",
|
||||
"multi-regexp2": "^1.0.3",
|
||||
"semver": "^7.3.2",
|
||||
"sift-string": "^0.0.2",
|
||||
"tsdx": "^0.13.3",
|
||||
"tslib": "^2.0.1",
|
||||
"typescript": "^4.0.2",
|
||||
"vscode-emmet-helper-bundled": "^0.0.1",
|
||||
"vscode-languageclient": "^6.1.3",
|
||||
"vscode-languageserver": "^6.1.1",
|
||||
"vscode-languageserver-textdocument": "^1.0.1"
|
||||
"@ctrl/tinycolor": "3.1.4",
|
||||
"@types/moo": "0.5.3",
|
||||
"css.escape": "1.5.1",
|
||||
"detect-indent": "6.0.0",
|
||||
"dlv": "1.1.3",
|
||||
"dset": "2.0.1",
|
||||
"line-column": "1.0.2",
|
||||
"moo": "0.5.1",
|
||||
"multi-regexp2": "1.0.3",
|
||||
"postcss-selector-parser": "6.0.2",
|
||||
"semver": "7.3.2",
|
||||
"sift-string": "0.0.2",
|
||||
"vscode-emmet-helper-bundled": "0.0.1",
|
||||
"vscode-languageclient": "7.0.0",
|
||||
"vscode-languageserver": "7.0.0",
|
||||
"vscode-languageserver-textdocument": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"postcss": "8.2.6",
|
||||
"tsdx": "0.14.1",
|
||||
"tslib": "2.2.0",
|
||||
"typescript": "4.2.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
isInvalidTailwindDirectiveDiagnostic,
|
||||
isInvalidScreenDiagnostic,
|
||||
isInvalidVariantDiagnostic,
|
||||
isIncorrectVariantOrderDiagnostic,
|
||||
} from '../diagnostics/types'
|
||||
import { flatten, dedupeBy } from '../util/array'
|
||||
import { provideCssConflictCodeActions } from './provideCssConflictCodeActions'
|
||||
|
@ -38,10 +39,7 @@ async function getDiagnosticsFromCodeActionParams(
|
|||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export async function doCodeActions(
|
||||
state: State,
|
||||
params: CodeActionParams
|
||||
): Promise<CodeAction[]> {
|
||||
export async function doCodeActions(state: State, params: CodeActionParams): Promise<CodeAction[]> {
|
||||
let diagnostics = await getDiagnosticsFromCodeActionParams(
|
||||
state,
|
||||
params,
|
||||
|
@ -64,7 +62,8 @@ export async function doCodeActions(
|
|||
isInvalidConfigPathDiagnostic(diagnostic) ||
|
||||
isInvalidTailwindDirectiveDiagnostic(diagnostic) ||
|
||||
isInvalidScreenDiagnostic(diagnostic) ||
|
||||
isInvalidVariantDiagnostic(diagnostic)
|
||||
isInvalidVariantDiagnostic(diagnostic) ||
|
||||
isIncorrectVariantOrderDiagnostic(diagnostic)
|
||||
) {
|
||||
return provideSuggestionCodeActions(state, params, diagnostic)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import type {
|
||||
CodeAction,
|
||||
CodeActionParams,
|
||||
TextEdit,
|
||||
Range,
|
||||
} from 'vscode-languageserver'
|
||||
import type { CodeAction, CodeActionParams, TextEdit, Range } from 'vscode-languageserver'
|
||||
import { State } from '../util/state'
|
||||
import { InvalidApplyDiagnostic } from '../diagnostics/types'
|
||||
import { isCssDoc } from '../util/css'
|
||||
|
@ -13,7 +8,7 @@ import { getClassNameParts } from '../util/getClassNameAtPosition'
|
|||
import { validateApply } from '../util/validateApply'
|
||||
import { isWithinRange } from '../util/isWithinRange'
|
||||
const dlv = require('dlv')
|
||||
import type { Root, NodeSource } from 'postcss'
|
||||
import type { Root, Source } from 'postcss'
|
||||
import { absoluteRange } from '../util/absoluteRange'
|
||||
import { removeRangesFromString } from '../util/removeRangesFromString'
|
||||
import detectIndent from 'detect-indent'
|
||||
|
@ -35,9 +30,7 @@ export async function provideInvalidApplyCodeActions(
|
|||
const { postcss } = state.modules
|
||||
let changes: TextEdit[] = []
|
||||
|
||||
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
|
||||
/\s+/
|
||||
).length
|
||||
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(/\s+/).length
|
||||
|
||||
let className = diagnostic.className.className
|
||||
let classNameParts = getClassNameParts(state, className)
|
||||
|
@ -50,16 +43,17 @@ export async function provideInvalidApplyCodeActions(
|
|||
if (!isCssDoc(state, document)) {
|
||||
let languageBoundaries = getLanguageBoundaries(state, document)
|
||||
if (!languageBoundaries) return []
|
||||
cssRange = languageBoundaries.css.find((range) =>
|
||||
isWithinRange(diagnostic.range.start, range)
|
||||
)
|
||||
cssRange = languageBoundaries.css.find((range) => isWithinRange(diagnostic.range.start, range))
|
||||
if (!cssRange) return []
|
||||
cssText = document.getText(cssRange)
|
||||
}
|
||||
|
||||
try {
|
||||
await postcss([
|
||||
postcss.plugin('', (_options = {}) => {
|
||||
await postcss
|
||||
.module([
|
||||
// TODO: use plain function?
|
||||
// @ts-ignore
|
||||
postcss.module.plugin('', (_options = {}) => {
|
||||
return (root: Root) => {
|
||||
root.walkRules((rule) => {
|
||||
if (changes.length) return false
|
||||
|
@ -70,8 +64,7 @@ export async function provideInvalidApplyCodeActions(
|
|||
atRuleRange = absoluteRange(atRuleRange, cssRange)
|
||||
}
|
||||
|
||||
if (!isWithinRange(diagnostic.range.start, atRuleRange))
|
||||
return true
|
||||
if (!isWithinRange(diagnostic.range.start, atRuleRange)) return undefined // true
|
||||
|
||||
let ast = classNameToAst(
|
||||
state,
|
||||
|
@ -118,10 +111,7 @@ export async function provideInvalidApplyCodeActions(
|
|||
.replace(/([^\s^]){$/gm, '$1 {')
|
||||
.replace(/^\s+/gm, (m: string) => {
|
||||
if (typeof outputIndent === 'undefined') outputIndent = m
|
||||
return m.replace(
|
||||
new RegExp(outputIndent, 'g'),
|
||||
documentIndent.indent
|
||||
)
|
||||
return m.replace(new RegExp(outputIndent, 'g'), documentIndent.indent)
|
||||
})
|
||||
.replace(/^(\s+)(.*?[^{}]\n)([^\s}])/gm, '$1$2$1$3'),
|
||||
})
|
||||
|
@ -129,11 +119,12 @@ export async function provideInvalidApplyCodeActions(
|
|||
return false
|
||||
})
|
||||
|
||||
return true
|
||||
return undefined // true
|
||||
})
|
||||
}
|
||||
}),
|
||||
]).process(cssText, { from: undefined })
|
||||
])
|
||||
.process(cssText, { from: undefined })
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
|
@ -156,7 +147,7 @@ export async function provideInvalidApplyCodeActions(
|
|||
]
|
||||
}
|
||||
|
||||
function postcssSourceToRange(source: NodeSource): Range {
|
||||
function postcssSourceToRange(source: Source): Range {
|
||||
return {
|
||||
start: {
|
||||
line: source.start.line - 1,
|
||||
|
@ -177,10 +168,7 @@ function classNameToAst(
|
|||
) {
|
||||
const baseClassName = classNameParts[classNameParts.length - 1]
|
||||
const validatedBaseClassName = validateApply(state, [baseClassName])
|
||||
if (
|
||||
validatedBaseClassName === null ||
|
||||
validatedBaseClassName.isApplyable === false
|
||||
) {
|
||||
if (validatedBaseClassName === null || validatedBaseClassName.isApplyable === false) {
|
||||
return null
|
||||
}
|
||||
const meta = getClassNameMeta(state, classNameParts)
|
||||
|
@ -188,11 +176,7 @@ function classNameToAst(
|
|||
let context = meta.context
|
||||
let pseudo = meta.pseudo
|
||||
const globalContexts = state.classNames.context
|
||||
let screens = dlv(
|
||||
state.config,
|
||||
'theme.screens',
|
||||
dlv(state.config, 'screens', {})
|
||||
)
|
||||
let screens = dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
|
||||
if (!isObject(screens)) screens = {}
|
||||
screens = Object.keys(screens)
|
||||
const path = []
|
||||
|
@ -231,10 +215,7 @@ function classNameToAst(
|
|||
return cssObjToAst(obj, state.modules.postcss)
|
||||
}
|
||||
|
||||
function appendPseudosToSelector(
|
||||
selector: string,
|
||||
pseudos: string[]
|
||||
): string | null {
|
||||
function appendPseudosToSelector(selector: string, pseudos: string[]): string | null {
|
||||
if (pseudos.length === 0) return selector
|
||||
|
||||
let canTransform = true
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { State } from '../util/state'
|
||||
import type {
|
||||
CodeActionParams,
|
||||
CodeAction,
|
||||
} from 'vscode-languageserver'
|
||||
import type { CodeActionParams, CodeAction } from 'vscode-languageserver'
|
||||
import {
|
||||
InvalidConfigPathDiagnostic,
|
||||
InvalidTailwindDirectiveDiagnostic,
|
||||
InvalidScreenDiagnostic,
|
||||
InvalidVariantDiagnostic,
|
||||
IncorrectVariantOrderDiagnostic,
|
||||
} from '../diagnostics/types'
|
||||
|
||||
export function provideSuggestionCodeActions(
|
||||
|
@ -18,6 +16,7 @@ export function provideSuggestionCodeActions(
|
|||
| InvalidTailwindDirectiveDiagnostic
|
||||
| InvalidScreenDiagnostic
|
||||
| InvalidVariantDiagnostic
|
||||
| IncorrectVariantOrderDiagnostic
|
||||
): CodeAction[] {
|
||||
return diagnostic.suggestions.map((suggestion) => ({
|
||||
title: `Replace with '${suggestion}'`,
|
||||
|
|
|
@ -19,26 +19,31 @@ import { stringifyScreen, Screen } from './util/screens'
|
|||
import isObject from './util/isObject'
|
||||
import * as emmetHelper from 'vscode-emmet-helper-bundled'
|
||||
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
|
||||
import { getDocumentSettings } from './util/getDocumentSettings'
|
||||
import { isJsContext } from './util/js'
|
||||
import { naturalExpand } from './util/naturalExpand'
|
||||
import semver from 'semver'
|
||||
import { docsUrl } from './util/docsUrl'
|
||||
import { ensureArray } from './util/array'
|
||||
import {
|
||||
getClassAttributeLexer,
|
||||
getComputedClassAttributeLexer,
|
||||
} from './util/lexers'
|
||||
import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers'
|
||||
import { validateApply } from './util/validateApply'
|
||||
import { flagEnabled } from './util/flagEnabled'
|
||||
import { remToPx } from './util/remToPx'
|
||||
import { createMultiRegexp } from './util/createMultiRegexp'
|
||||
import * as jit from './util/jit'
|
||||
import { TinyColor } from '@ctrl/tinycolor'
|
||||
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
|
||||
|
||||
let isUtil = (className) =>
|
||||
Array.isArray(className.__info)
|
||||
? className.__info.some((x) => x.__source === 'utilities')
|
||||
: className.__info.__source === 'utilities'
|
||||
|
||||
export function completionsFromClassList(
|
||||
state: State,
|
||||
classList: string,
|
||||
classListRange: Range,
|
||||
filter?: (item: CompletionItem) => boolean
|
||||
filter?: (item: CompletionItem) => boolean,
|
||||
document?: TextDocument
|
||||
): CompletionList {
|
||||
let classNames = classList.split(/[\s+]/)
|
||||
const partialClassName = classNames[classNames.length - 1]
|
||||
|
@ -56,23 +61,126 @@ export function completionsFromClassList(
|
|||
},
|
||||
}
|
||||
|
||||
if (state.jit) {
|
||||
let allVariants = Object.keys(state.variants)
|
||||
let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName)
|
||||
|
||||
replacementRange.start.character += offset
|
||||
|
||||
let important = partialClassName.substr(offset).startsWith('!')
|
||||
if (important) {
|
||||
replacementRange.start.character += 1
|
||||
}
|
||||
|
||||
let items: CompletionItem[] = []
|
||||
|
||||
if (!important) {
|
||||
items.push(
|
||||
...Object.entries(state.variants)
|
||||
.filter(([variant]) => !existingVariants.includes(variant))
|
||||
.map(([variant, definition], index) => {
|
||||
let resultingVariants = [...existingVariants, variant].sort(
|
||||
(a, b) => allVariants.indexOf(b) - allVariants.indexOf(a)
|
||||
)
|
||||
|
||||
return {
|
||||
label: variant + sep,
|
||||
kind: 9,
|
||||
detail: definition,
|
||||
data: 'variant',
|
||||
command: {
|
||||
title: '',
|
||||
command: 'editor.action.triggerSuggest',
|
||||
},
|
||||
sortText: '-' + naturalExpand(index),
|
||||
textEdit: {
|
||||
newText: resultingVariants[resultingVariants.length - 1] + sep,
|
||||
range: replacementRange,
|
||||
},
|
||||
additionalTextEdits:
|
||||
resultingVariants.length > 1
|
||||
? [
|
||||
{
|
||||
newText:
|
||||
resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep,
|
||||
range: {
|
||||
start: {
|
||||
...classListRange.start,
|
||||
character: classListRange.end.character - partialClassName.length,
|
||||
},
|
||||
end: {
|
||||
...replacementRange.start,
|
||||
character: replacementRange.start.character,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
} as CompletionItem
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
isIncomplete: false,
|
||||
items: items
|
||||
.concat(
|
||||
Object.keys(state.classNames.classNames)
|
||||
.filter((className) => {
|
||||
let item = state.classNames.classNames[className]
|
||||
if (existingVariants.length === 0) {
|
||||
return item.__info
|
||||
}
|
||||
return item.__info && isUtil(item)
|
||||
})
|
||||
.map((className, index) => {
|
||||
let kind: CompletionItemKind = 21
|
||||
let documentation: string = null
|
||||
|
||||
const color = getColor(state, className)
|
||||
if (color !== null) {
|
||||
kind = 16
|
||||
if (typeof color !== 'string' && color.a !== 0) {
|
||||
documentation = color.toRgbString()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: className,
|
||||
kind,
|
||||
documentation,
|
||||
sortText: naturalExpand(index),
|
||||
data: [...existingVariants, important ? `!${className}` : className],
|
||||
textEdit: {
|
||||
newText: className,
|
||||
range: replacementRange,
|
||||
},
|
||||
} as CompletionItem
|
||||
})
|
||||
)
|
||||
.filter((item) => {
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
if (filter && !filter(item)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = parts.length - 1; i > 0; i--) {
|
||||
let keys = parts.slice(0, i).filter(Boolean)
|
||||
subset = dlv(state.classNames.classNames, keys)
|
||||
if (
|
||||
typeof subset !== 'undefined' &&
|
||||
typeof dlv(subset, ['__info', '__rule']) === 'undefined'
|
||||
) {
|
||||
if (typeof subset !== 'undefined' && typeof dlv(subset, ['__info', '__rule']) === 'undefined') {
|
||||
isSubset = true
|
||||
subsetKey = keys
|
||||
replacementRange = {
|
||||
...replacementRange,
|
||||
start: {
|
||||
...replacementRange.start,
|
||||
character:
|
||||
replacementRange.start.character +
|
||||
keys.join(sep).length +
|
||||
sep.length,
|
||||
character: replacementRange.start.character + keys.join(sep).length + sep.length,
|
||||
},
|
||||
}
|
||||
break
|
||||
|
@ -106,17 +214,13 @@ export function completionsFromClassList(
|
|||
.concat(
|
||||
Object.keys(isSubset ? subset : state.classNames.classNames)
|
||||
.filter((className) =>
|
||||
dlv(state.classNames.classNames, [
|
||||
...subsetKey,
|
||||
className,
|
||||
'__info',
|
||||
])
|
||||
dlv(state.classNames.classNames, [...subsetKey, className, '__info'])
|
||||
)
|
||||
.map((className, index) => {
|
||||
let kind: CompletionItemKind = 21
|
||||
let documentation: string = null
|
||||
|
||||
const color = getColor(state, [className])
|
||||
const color = getColor(state, className)
|
||||
if (color !== null) {
|
||||
kind = 16
|
||||
if (typeof color !== 'string' && color.a !== 0) {
|
||||
|
@ -159,10 +263,7 @@ function provideClassAttributeCompletions(
|
|||
end: position,
|
||||
})
|
||||
|
||||
const match = findLast(
|
||||
/(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi,
|
||||
str
|
||||
)
|
||||
const match = findLast(/(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi, str)
|
||||
|
||||
if (match === null) {
|
||||
return null
|
||||
|
@ -187,13 +288,19 @@ function provideClassAttributeCompletions(
|
|||
}
|
||||
}
|
||||
|
||||
return completionsFromClassList(state, classList, {
|
||||
return completionsFromClassList(
|
||||
state,
|
||||
classList,
|
||||
{
|
||||
start: {
|
||||
line: position.line,
|
||||
character: position.character - classList.length,
|
||||
},
|
||||
end: position,
|
||||
})
|
||||
},
|
||||
undefined,
|
||||
document
|
||||
)
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
|
@ -205,7 +312,7 @@ async function provideCustomClassNameCompletions(
|
|||
document: TextDocument,
|
||||
position: Position
|
||||
): Promise<CompletionList> {
|
||||
const settings = await getDocumentSettings(state, document)
|
||||
const settings = await state.editor.getConfiguration(document.uri)
|
||||
const regexes = dlv(settings, 'experimental.classRegex', [])
|
||||
if (regexes.length === 0) return null
|
||||
|
||||
|
@ -220,9 +327,7 @@ async function provideCustomClassNameCompletions(
|
|||
|
||||
for (let i = 0; i < regexes.length; i++) {
|
||||
try {
|
||||
let [containerRegex, classRegex] = Array.isArray(regexes[i])
|
||||
? regexes[i]
|
||||
: [regexes[i]]
|
||||
let [containerRegex, classRegex] = Array.isArray(regexes[i]) ? regexes[i] : [regexes[i]]
|
||||
|
||||
containerRegex = createMultiRegexp(containerRegex)
|
||||
let containerMatch
|
||||
|
@ -239,9 +344,7 @@ async function provideCustomClassNameCompletions(
|
|||
classRegex = createMultiRegexp(classRegex)
|
||||
let classMatch
|
||||
|
||||
while (
|
||||
(classMatch = classRegex.exec(containerMatch.match)) !== null
|
||||
) {
|
||||
while ((classMatch = classRegex.exec(containerMatch.match)) !== null) {
|
||||
const classMatchStart = matchStart + classMatch.start
|
||||
const classMatchEnd = matchStart + classMatch.end
|
||||
if (cursor >= classMatchStart && cursor <= classMatchEnd) {
|
||||
|
@ -302,8 +405,7 @@ function provideAtApplyCompletions(
|
|||
(item) => {
|
||||
if (item.kind === 9) {
|
||||
return (
|
||||
semver.gte(state.version, '2.0.0-alpha.1') ||
|
||||
flagEnabled(state, 'applyComplexClasses')
|
||||
semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')
|
||||
)
|
||||
}
|
||||
let validated = validateApply(state, item.data)
|
||||
|
@ -317,10 +419,7 @@ function provideClassNameCompletions(
|
|||
document: TextDocument,
|
||||
position: Position
|
||||
): CompletionList {
|
||||
if (
|
||||
isHtmlContext(state, document, position) ||
|
||||
isJsContext(state, document, position)
|
||||
) {
|
||||
if (isHtmlContext(state, document, position) || isJsContext(state, document, position)) {
|
||||
return provideClassAttributeCompletions(state, document, position)
|
||||
}
|
||||
|
||||
|
@ -354,10 +453,7 @@ function provideCssHelperCompletions(
|
|||
return null
|
||||
}
|
||||
|
||||
let base =
|
||||
match.groups.helper === 'config'
|
||||
? state.config
|
||||
: dlv(state.config, 'theme', {})
|
||||
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
|
||||
let parts = match.groups.keys.split(/([\[\].]+)/)
|
||||
let keys = parts.filter((_, i) => i % 2 === 0)
|
||||
let separators = parts.filter((_, i) => i % 2 !== 0)
|
||||
|
@ -372,9 +468,7 @@ function provideCssHelperCompletions(
|
|||
|
||||
let obj: any
|
||||
let offset: number = 0
|
||||
let separator: string = separators.length
|
||||
? separators[separators.length - 1]
|
||||
: null
|
||||
let separator: string = separators.length ? separators[separators.length - 1] : null
|
||||
|
||||
if (keys.length === 1) {
|
||||
obj = base
|
||||
|
@ -396,8 +490,7 @@ function provideCssHelperCompletions(
|
|||
isIncomplete: false,
|
||||
items: Object.keys(obj).map((item, index) => {
|
||||
let color = getColorFromValue(obj[item])
|
||||
const replaceDot: boolean =
|
||||
item.indexOf('.') !== -1 && separator && separator.endsWith('.')
|
||||
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
|
||||
const insertClosingBrace: boolean =
|
||||
text.charAt(text.length - 1) !== ']' &&
|
||||
(replaceDot || (separator && separator.endsWith('[')))
|
||||
|
@ -408,21 +501,16 @@ function provideCssHelperCompletions(
|
|||
filterText: `${replaceDot ? '.' : ''}${item}`,
|
||||
sortText: naturalExpand(index),
|
||||
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
|
||||
// VS Code bug causes '0' to not display in some cases
|
||||
detail: detail === '0' ? '0 ' : detail,
|
||||
documentation: color,
|
||||
// VS Code bug causes some values to not display in some cases
|
||||
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
|
||||
documentation: color instanceof TinyColor && color.a !== 0 ? color.toRgbString() : null,
|
||||
textEdit: {
|
||||
newText: `${replaceDot ? '[' : ''}${item}${
|
||||
insertClosingBrace ? ']' : ''
|
||||
}`,
|
||||
newText: `${replaceDot ? '[' : ''}${item}${insertClosingBrace ? ']' : ''}`,
|
||||
range: {
|
||||
start: {
|
||||
line: position.line,
|
||||
character:
|
||||
position.character -
|
||||
keys[keys.length - 1].length -
|
||||
(replaceDot ? 1 : 0) -
|
||||
offset,
|
||||
position.character - keys[keys.length - 1].length - (replaceDot ? 1 : 0) - offset,
|
||||
},
|
||||
end: position,
|
||||
},
|
||||
|
@ -433,7 +521,6 @@ function provideCssHelperCompletions(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: vary docs links based on Tailwind version
|
||||
function provideTailwindDirectiveCompletions(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
|
@ -550,13 +637,15 @@ function provideVariantsDirectiveCompletions(
|
|||
|
||||
return {
|
||||
isIncomplete: false,
|
||||
items: state.variants
|
||||
items: Object.keys(state.variants)
|
||||
.filter((v) => existingVariants.indexOf(v) === -1)
|
||||
.map((variant) => ({
|
||||
.map((variant, index) => ({
|
||||
// TODO: detail
|
||||
label: variant,
|
||||
detail: state.variants[variant],
|
||||
kind: 21,
|
||||
data: 'variant',
|
||||
sortText: naturalExpand(index),
|
||||
textEdit: {
|
||||
newText: variant,
|
||||
range: {
|
||||
|
@ -628,11 +717,7 @@ function provideScreenDirectiveCompletions(
|
|||
|
||||
if (match === null) return null
|
||||
|
||||
const screens = dlv(
|
||||
state.config,
|
||||
['screens'],
|
||||
dlv(state.config, ['theme', 'screens'], {})
|
||||
)
|
||||
const screens = dlv(state.config, ['screens'], dlv(state.config, ['theme', 'screens'], {}))
|
||||
|
||||
if (!isObject(screens)) return null
|
||||
|
||||
|
@ -767,7 +852,7 @@ async function provideEmmetCompletions(
|
|||
document: TextDocument,
|
||||
position: Position
|
||||
): Promise<CompletionList> {
|
||||
let settings = await getDocumentSettings(state, document)
|
||||
let settings = await state.editor.getConfiguration(document.uri)
|
||||
if (settings.emmetCompletions !== true) return null
|
||||
|
||||
const isHtml = isHtmlContext(state, document, position)
|
||||
|
@ -779,26 +864,16 @@ async function provideEmmetCompletions(
|
|||
return null
|
||||
}
|
||||
|
||||
const extractAbbreviationResults = emmetHelper.extractAbbreviation(
|
||||
document,
|
||||
position,
|
||||
true
|
||||
)
|
||||
const extractAbbreviationResults = emmetHelper.extractAbbreviation(document, position, true)
|
||||
if (
|
||||
!extractAbbreviationResults ||
|
||||
!emmetHelper.isAbbreviationValid(
|
||||
syntax,
|
||||
extractAbbreviationResults.abbreviation
|
||||
)
|
||||
!emmetHelper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
!isValidLocationForEmmetAbbreviation(
|
||||
document,
|
||||
extractAbbreviationResults.abbreviationRange
|
||||
)
|
||||
!isValidLocationForEmmetAbbreviation(document, extractAbbreviationResults.abbreviationRange)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
@ -808,16 +883,13 @@ async function provideEmmetCompletions(
|
|||
if (abbreviation.startsWith('this.')) {
|
||||
return null
|
||||
}
|
||||
const { symbols } = await state.emitter.emit('getDocumentSymbols', {
|
||||
uri: document.uri,
|
||||
})
|
||||
const symbols = await state.editor.getDocumentSymbols(document.uri)
|
||||
if (
|
||||
symbols &&
|
||||
symbols.find(
|
||||
(symbol) =>
|
||||
abbreviation === symbol.name ||
|
||||
(abbreviation.startsWith(symbol.name + '.') &&
|
||||
!/>|\*|\+/.test(abbreviation))
|
||||
(abbreviation.startsWith(symbol.name + '.') && !/>|\*|\+/.test(abbreviation))
|
||||
)
|
||||
) {
|
||||
return null
|
||||
|
@ -847,11 +919,7 @@ async function provideEmmetCompletions(
|
|||
})
|
||||
}
|
||||
|
||||
export async function doComplete(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
position: Position
|
||||
) {
|
||||
export async function doComplete(state: State, document: TextDocument, position: Position) {
|
||||
if (state === null) return { items: [], isIncomplete: false }
|
||||
|
||||
const result =
|
||||
|
@ -873,32 +941,44 @@ export async function resolveCompletionItem(
|
|||
state: State,
|
||||
item: CompletionItem
|
||||
): Promise<CompletionItem> {
|
||||
if (
|
||||
['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)
|
||||
) {
|
||||
if (['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)) {
|
||||
return item
|
||||
}
|
||||
|
||||
if (item.data === 'screen') {
|
||||
let screens = dlv(
|
||||
state.config,
|
||||
['theme', 'screens'],
|
||||
dlv(state.config, ['screens'], {})
|
||||
)
|
||||
let screens = dlv(state.config, ['theme', 'screens'], dlv(state.config, ['screens'], {}))
|
||||
if (!isObject(screens)) screens = {}
|
||||
item.detail = stringifyScreen(screens[item.label] as Screen)
|
||||
return item
|
||||
}
|
||||
|
||||
if (state.jit) {
|
||||
if (item.kind === 9) return item
|
||||
let { root, rules } = jit.generateRules(state, [item.data.join(state.separator)])
|
||||
if (rules.length === 0) return item
|
||||
if (!item.detail) {
|
||||
if (rules.length === 1) {
|
||||
item.detail = jit.stringifyDecls(rules[0])
|
||||
} else {
|
||||
item.detail = `${rules.length} rules`
|
||||
}
|
||||
}
|
||||
if (!item.documentation) {
|
||||
item.documentation = {
|
||||
kind: 'markdown' as typeof MarkupKind.Markdown,
|
||||
value: ['```css', await jit.stringifyRoot(state, root), '```'].join('\n'),
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
const className = dlv(state.classNames.classNames, [...item.data, '__info'])
|
||||
if (item.kind === 9) {
|
||||
item.detail = state.classNames.context[
|
||||
item.data[item.data.length - 1]
|
||||
].join(', ')
|
||||
item.detail = state.classNames.context[item.data[item.data.length - 1]].join(', ')
|
||||
} else {
|
||||
item.detail = await getCssDetail(state, className)
|
||||
if (!item.documentation) {
|
||||
const settings = await getDocumentSettings(state)
|
||||
const settings = await state.editor.getConfiguration()
|
||||
const css = stringifyCss(item.data.join(':'), className, {
|
||||
tabSize: dlv(settings, 'tabSize', 2),
|
||||
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
|
||||
|
@ -949,9 +1029,7 @@ function stringifyDecls(
|
|||
.map((prop) =>
|
||||
ensureArray(obj[prop])
|
||||
.map((value) => {
|
||||
const px = showPixelEquivalents
|
||||
? remToPx(value, rootFontSize)
|
||||
: undefined
|
||||
const px = showPixelEquivalents ? remToPx(value, rootFontSize) : undefined
|
||||
return `${prop}: ${value}${px ? `/* ${px} */` : ''};`
|
||||
})
|
||||
.join(' ')
|
||||
|
@ -964,7 +1042,7 @@ async function getCssDetail(state: State, className: any): Promise<string> {
|
|||
return `${className.length} rules`
|
||||
}
|
||||
if (className.__rule === true) {
|
||||
const settings = await getDocumentSettings(state)
|
||||
const settings = await state.editor.getConfiguration()
|
||||
return stringifyDecls(removeMeta(className), {
|
||||
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
|
||||
rootFontSize: dlv(settings, 'rootFontSize', 16),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { TextDocument } from 'vscode-languageserver'
|
||||
import { State } from '../util/state'
|
||||
import { getDocumentSettings } from '../util/getDocumentSettings'
|
||||
import { DiagnosticKind, AugmentedDiagnostic } from './types'
|
||||
import { getCssConflictDiagnostics } from './getCssConflictDiagnostics'
|
||||
import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics'
|
||||
|
@ -8,6 +7,7 @@ import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics'
|
|||
import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics'
|
||||
import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics'
|
||||
import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics'
|
||||
import { getIncorrectVariantOrderDiagnostics } from './getIncorrectVariantOrderDiagnostics'
|
||||
|
||||
export async function doValidate(
|
||||
state: State,
|
||||
|
@ -19,9 +19,10 @@ export async function doValidate(
|
|||
DiagnosticKind.InvalidVariant,
|
||||
DiagnosticKind.InvalidConfigPath,
|
||||
DiagnosticKind.InvalidTailwindDirective,
|
||||
DiagnosticKind.IncorrectVariantOrder,
|
||||
]
|
||||
): Promise<AugmentedDiagnostic[]> {
|
||||
const settings = await getDocumentSettings(state, document)
|
||||
const settings = await state.editor.getConfiguration(document.uri)
|
||||
|
||||
return settings.validate
|
||||
? [
|
||||
|
@ -43,22 +44,25 @@ export async function doValidate(
|
|||
...(only.includes(DiagnosticKind.InvalidTailwindDirective)
|
||||
? getInvalidTailwindDirectiveDiagnostics(state, document, settings)
|
||||
: []),
|
||||
...(only.includes(DiagnosticKind.IncorrectVariantOrder)
|
||||
? await getIncorrectVariantOrderDiagnostics(state, document, settings)
|
||||
: []),
|
||||
]
|
||||
: []
|
||||
}
|
||||
|
||||
export async function provideDiagnostics(state: State, document: TextDocument) {
|
||||
state.editor.connection.sendDiagnostics({
|
||||
uri: document.uri,
|
||||
diagnostics: await doValidate(state, document),
|
||||
})
|
||||
// state.editor.connection.sendDiagnostics({
|
||||
// uri: document.uri,
|
||||
// diagnostics: await doValidate(state, document),
|
||||
// })
|
||||
}
|
||||
|
||||
export function clearDiagnostics(state: State, document: TextDocument): void {
|
||||
state.editor.connection.sendDiagnostics({
|
||||
uri: document.uri,
|
||||
diagnostics: [],
|
||||
})
|
||||
// state.editor.connection.sendDiagnostics({
|
||||
// uri: document.uri,
|
||||
// diagnostics: [],
|
||||
// })
|
||||
}
|
||||
|
||||
export function clearAllDiagnostics(state: State): void {
|
||||
|
|
|
@ -2,13 +2,11 @@ import { joinWithAnd } from '../util/joinWithAnd'
|
|||
import { State, Settings } from '../util/state'
|
||||
import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
|
||||
import { CssConflictDiagnostic, DiagnosticKind } from './types'
|
||||
import {
|
||||
findClassListsInDocument,
|
||||
getClassNamesInClassList,
|
||||
} from '../util/find'
|
||||
import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'
|
||||
import { getClassNameDecls } from '../util/getClassNameDecls'
|
||||
import { getClassNameMeta } from '../util/getClassNameMeta'
|
||||
import { equal } from '../util/array'
|
||||
import * as jit from '../util/jit'
|
||||
|
||||
export async function getCssConflictDiagnostics(
|
||||
state: State,
|
||||
|
@ -25,6 +23,81 @@ export async function getCssConflictDiagnostics(
|
|||
const classNames = getClassNamesInClassList(classList)
|
||||
|
||||
classNames.forEach((className, index) => {
|
||||
if (state.jit) {
|
||||
let { rules } = jit.generateRules(state, [className.className])
|
||||
if (rules.length !== 1) {
|
||||
return
|
||||
}
|
||||
let rule = rules[0]
|
||||
let context: string[]
|
||||
let properties = []
|
||||
rule.walkDecls(({ prop }) => {
|
||||
properties.push(prop)
|
||||
})
|
||||
|
||||
let otherClassNames = classNames.filter((_className, i) => i !== index)
|
||||
|
||||
let conflictingClassNames = otherClassNames.filter((otherClassName) => {
|
||||
let { rules } = jit.generateRules(state, [otherClassName.className])
|
||||
if (rules.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
let otherRule = rules[0]
|
||||
|
||||
let otherProperties = []
|
||||
otherRule.walkDecls(({ prop }) => {
|
||||
otherProperties.push(prop)
|
||||
})
|
||||
|
||||
if (!equal(properties, otherProperties)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
context = jit.getRuleContext(state, rule, className.className)
|
||||
}
|
||||
let otherContext = jit.getRuleContext(state, otherRule, otherClassName.className)
|
||||
|
||||
if (!equal(context, otherContext)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (conflictingClassNames.length === 0) return
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.CssConflict,
|
||||
className,
|
||||
otherClassNames: conflictingClassNames,
|
||||
range: className.range,
|
||||
severity:
|
||||
severity === 'error'
|
||||
? 1 /* DiagnosticSeverity.Error */
|
||||
: 2 /* DiagnosticSeverity.Warning */,
|
||||
message: `'${className.className}' applies the same CSS ${
|
||||
properties.length === 1 ? 'property' : 'properties'
|
||||
} as ${joinWithAnd(
|
||||
conflictingClassNames.map(
|
||||
(conflictingClassName) => `'${conflictingClassName.className}'`
|
||||
)
|
||||
)}.`,
|
||||
relatedInformation: conflictingClassNames.map((conflictingClassName) => {
|
||||
return {
|
||||
message: conflictingClassName.className,
|
||||
location: {
|
||||
uri: document.uri,
|
||||
range: conflictingClassName.range,
|
||||
},
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let decls = getClassNameDecls(state, className.className)
|
||||
if (!decls) return
|
||||
|
||||
|
@ -63,12 +136,9 @@ export async function getCssConflictDiagnostics(
|
|||
message: `'${className.className}' applies the same CSS ${
|
||||
properties.length === 1 ? 'property' : 'properties'
|
||||
} as ${joinWithAnd(
|
||||
conflictingClassNames.map(
|
||||
(conflictingClassName) => `'${conflictingClassName.className}'`
|
||||
)
|
||||
conflictingClassNames.map((conflictingClassName) => `'${conflictingClassName.className}'`)
|
||||
)}.`,
|
||||
relatedInformation: conflictingClassNames.map(
|
||||
(conflictingClassName) => {
|
||||
relatedInformation: conflictingClassNames.map((conflictingClassName) => {
|
||||
return {
|
||||
message: conflictingClassName.className,
|
||||
location: {
|
||||
|
@ -76,8 +146,7 @@ export async function getCssConflictDiagnostics(
|
|||
range: conflictingClassName.range,
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { State, Settings } from '../util/state'
|
||||
import type { TextDocument } from 'vscode-languageserver'
|
||||
import { IncorrectVariantOrderDiagnostic, DiagnosticKind } from './types'
|
||||
import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'
|
||||
import * as jit from '../util/jit'
|
||||
import { getVariantsFromClassName } from '../util/getVariantsFromClassName'
|
||||
import { equalExact } from '../util/array'
|
||||
|
||||
export async function getIncorrectVariantOrderDiagnostics(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
settings: Settings
|
||||
): Promise<IncorrectVariantOrderDiagnostic[]> {
|
||||
if (!state.jit) return []
|
||||
|
||||
let severity = settings.lint.incorrectVariantOrder
|
||||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: IncorrectVariantOrderDiagnostic[] = []
|
||||
const classLists = await findClassListsInDocument(state, document)
|
||||
|
||||
classLists.forEach((classList) => {
|
||||
const classNames = getClassNamesInClassList(classList)
|
||||
classNames.forEach((className) => {
|
||||
let { rules } = jit.generateRules(state, [className.className])
|
||||
if (rules.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let { variants, offset } = getVariantsFromClassName(state, className.className)
|
||||
let sortedVariants = [...variants].sort((a, b) =>
|
||||
jit.bigSign(state.jitContext.variantOrder.get(b) - state.jitContext.variantOrder.get(a))
|
||||
)
|
||||
|
||||
if (!equalExact(variants, sortedVariants)) {
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.IncorrectVariantOrder,
|
||||
suggestions: [
|
||||
[...sortedVariants, className.className.substr(offset)].join(state.separator),
|
||||
],
|
||||
range: className.range,
|
||||
severity:
|
||||
severity === 'error'
|
||||
? 1 /* DiagnosticSeverity.Error */
|
||||
: 2 /* DiagnosticSeverity.Warning */,
|
||||
message:
|
||||
'Variants are not in the recommended order, which may cause unexpected CSS output.',
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
|
@ -61,10 +61,7 @@ export function getInvalidTailwindDirectiveDiagnostics(
|
|||
code: DiagnosticKind.InvalidTailwindDirective,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(
|
||||
text,
|
||||
match.index + match[0].length - match.groups.value.length
|
||||
),
|
||||
start: indexToPosition(text, match.index + match[0].length - match.groups.value.length),
|
||||
end: indexToPosition(text, match.index + match[0].length),
|
||||
},
|
||||
range
|
||||
|
|
|
@ -37,13 +37,13 @@ export function getInvalidVariantDiagnostics(
|
|||
|
||||
for (let i = 0; i < variants.length; i += 2) {
|
||||
let variant = variants[i].trim()
|
||||
if (state.variants.includes(variant)) {
|
||||
if (Object.keys(state.variants).includes(variant)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let message = `The variant '${variant}' does not exist.`
|
||||
let suggestions: string[] = []
|
||||
let suggestion = closest(variant, state.variants)
|
||||
let suggestion = closest(variant, Object.keys(state.variants))
|
||||
|
||||
if (suggestion) {
|
||||
suggestions.push(suggestion)
|
||||
|
|
|
@ -8,6 +8,7 @@ export enum DiagnosticKind {
|
|||
InvalidVariant = 'invalidVariant',
|
||||
InvalidConfigPath = 'invalidConfigPath',
|
||||
InvalidTailwindDirective = 'invalidTailwindDirective',
|
||||
IncorrectVariantOrder = 'incorrectVariantOrder',
|
||||
}
|
||||
|
||||
export type CssConflictDiagnostic = Diagnostic & {
|
||||
|
@ -77,6 +78,17 @@ export function isInvalidTailwindDirectiveDiagnostic(
|
|||
return diagnostic.code === DiagnosticKind.InvalidTailwindDirective
|
||||
}
|
||||
|
||||
export type IncorrectVariantOrderDiagnostic = Diagnostic & {
|
||||
code: DiagnosticKind.IncorrectVariantOrder
|
||||
suggestions: string[]
|
||||
}
|
||||
|
||||
export function isIncorrectVariantOrderDiagnostic(
|
||||
diagnostic: AugmentedDiagnostic
|
||||
): diagnostic is IncorrectVariantOrderDiagnostic {
|
||||
return diagnostic.code === DiagnosticKind.IncorrectVariantOrder
|
||||
}
|
||||
|
||||
export type AugmentedDiagnostic =
|
||||
| CssConflictDiagnostic
|
||||
| InvalidApplyDiagnostic
|
||||
|
@ -84,3 +96,4 @@ export type AugmentedDiagnostic =
|
|||
| InvalidVariantDiagnostic
|
||||
| InvalidConfigPathDiagnostic
|
||||
| InvalidTailwindDirectiveDiagnostic
|
||||
| IncorrectVariantOrderDiagnostic
|
||||
|
|
|
@ -4,27 +4,34 @@ import {
|
|||
getClassNamesInClassList,
|
||||
findHelperFunctionsInDocument,
|
||||
} from './util/find'
|
||||
import { getClassNameParts } from './util/getClassNameAtPosition'
|
||||
import { getColor, getColorFromValue } from './util/color'
|
||||
import { getColor, getColorFromValue, tinyColorToVscodeColor } from './util/color'
|
||||
import { stringToPath } from './util/stringToPath'
|
||||
import type { TextDocument } from 'vscode-languageserver'
|
||||
const dlv = require('dlv')
|
||||
import type { TextDocument, ColorInformation } from 'vscode-languageserver'
|
||||
import { TinyColor } from '@ctrl/tinycolor'
|
||||
import dlv from 'dlv'
|
||||
|
||||
export async function getDocumentColors(state: State, document: TextDocument) {
|
||||
let colors = []
|
||||
export async function getDocumentColors(
|
||||
state: State,
|
||||
document: TextDocument
|
||||
): Promise<ColorInformation[]> {
|
||||
let colors: ColorInformation[] = []
|
||||
if (!state.enabled) return colors
|
||||
|
||||
let settings = await state.editor.getConfiguration(document.uri)
|
||||
if (settings.colorDecorators === 'off') return colors
|
||||
|
||||
let classLists = await findClassListsInDocument(state, document)
|
||||
classLists.forEach((classList) => {
|
||||
let classNames = getClassNamesInClassList(classList)
|
||||
classNames.forEach((className) => {
|
||||
let parts = getClassNameParts(state, className.className)
|
||||
if (!parts) return
|
||||
let color = getColor(state, parts)
|
||||
let color = getColor(state, className.className)
|
||||
if (color === null || typeof color === 'string' || color.a === 0) {
|
||||
return
|
||||
}
|
||||
colors.push({ range: className.range, color: color.toRgbString() })
|
||||
colors.push({
|
||||
range: className.range,
|
||||
color: tinyColorToVscodeColor(color),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -34,8 +41,8 @@ export async function getDocumentColors(state: State, document: TextDocument) {
|
|||
let base = fn.helper === 'theme' ? ['theme'] : []
|
||||
let value = dlv(state.config, [...base, ...keys])
|
||||
let color = getColorFromValue(value)
|
||||
if (color) {
|
||||
colors.push({ range: fn.valueRange, color })
|
||||
if (color instanceof TinyColor && color.a !== 0) {
|
||||
colors.push({ range: fn.valueRange, color: tinyColorToVscodeColor(color) })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { isCssContext } from './util/css'
|
|||
import { findClassNameAtPosition } from './util/find'
|
||||
import { validateApply } from './util/validateApply'
|
||||
import { getClassNameParts } from './util/getClassNameAtPosition'
|
||||
import { getDocumentSettings } from './util/getDocumentSettings'
|
||||
import * as jit from './util/jit'
|
||||
|
||||
export async function doHover(
|
||||
state: State,
|
||||
|
@ -19,11 +19,7 @@ export async function doHover(
|
|||
)
|
||||
}
|
||||
|
||||
function provideCssHelperHover(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
position: Position
|
||||
): Hover {
|
||||
function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover {
|
||||
if (!isCssContext(state, document, position)) return null
|
||||
|
||||
const line = document.getText({
|
||||
|
@ -31,9 +27,7 @@ function provideCssHelperHover(
|
|||
end: { line: position.line + 1, character: 0 },
|
||||
})
|
||||
|
||||
const match = line.match(
|
||||
/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/
|
||||
)
|
||||
const match = line.match(/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/)
|
||||
|
||||
if (match === null) return null
|
||||
|
||||
|
@ -80,6 +74,22 @@ async function provideClassNameHover(
|
|||
let className = await findClassNameAtPosition(state, document, position)
|
||||
if (className === null) return null
|
||||
|
||||
if (state.jit) {
|
||||
let { root, rules } = jit.generateRules(state, [className.className])
|
||||
|
||||
if (rules.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
contents: {
|
||||
language: 'css',
|
||||
value: await jit.stringifyRoot(state, root, document.uri),
|
||||
},
|
||||
range: className.range,
|
||||
}
|
||||
}
|
||||
|
||||
const parts = getClassNameParts(state, className.className)
|
||||
if (!parts) return null
|
||||
|
||||
|
@ -90,7 +100,7 @@ async function provideClassNameHover(
|
|||
}
|
||||
}
|
||||
|
||||
const settings = await getDocumentSettings(state, document)
|
||||
const settings = await state.editor.getConfiguration(document.uri)
|
||||
|
||||
const css = stringifyCss(
|
||||
className.className,
|
||||
|
|
|
@ -2,14 +2,8 @@ export function dedupe<T>(arr: Array<T>): Array<T> {
|
|||
return arr.filter((value, index, self) => self.indexOf(value) === index)
|
||||
}
|
||||
|
||||
export function dedupeBy<T>(
|
||||
arr: Array<T>,
|
||||
transform: (item: T) => any
|
||||
): Array<T> {
|
||||
return arr.filter(
|
||||
(value, index, self) =>
|
||||
self.map(transform).indexOf(transform(value)) === index
|
||||
)
|
||||
export function dedupeBy<T>(arr: Array<T>, transform: (item: T) => any): Array<T> {
|
||||
return arr.filter((value, index, self) => self.map(transform).indexOf(transform(value)) === index)
|
||||
}
|
||||
|
||||
export function ensureArray<T>(value: T | T[]): T[] {
|
||||
|
@ -20,9 +14,27 @@ export function flatten<T>(arrays: T[][]): T[] {
|
|||
return [].concat.apply([], arrays)
|
||||
}
|
||||
|
||||
export function equal(arr1: any[], arr2: any[]): boolean {
|
||||
return (
|
||||
JSON.stringify(arr1.concat([]).sort()) ===
|
||||
JSON.stringify(arr2.concat([]).sort())
|
||||
)
|
||||
export function equal(a: any[], b: any[]): boolean {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
|
||||
let aSorted = a.concat().sort()
|
||||
let bSorted = b.concat().sort()
|
||||
|
||||
for (let i = 0; i < aSorted.length; ++i) {
|
||||
if (aSorted[i] !== bSorted[i]) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function equalExact(a: any[], b: any[]): boolean {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ import { State } from './state'
|
|||
import removeMeta from './removeMeta'
|
||||
import { TinyColor, names as colorNames } from '@ctrl/tinycolor'
|
||||
import { ensureArray, dedupe, flatten } from './array'
|
||||
import type { Color } from 'vscode-languageserver'
|
||||
import { getClassNameParts } from './getClassNameAtPosition'
|
||||
import * as jit from './jit'
|
||||
|
||||
const COLOR_PROPS = [
|
||||
'caret-color',
|
||||
|
@ -21,58 +24,78 @@ const COLOR_PROPS = [
|
|||
'text-decoration-color',
|
||||
]
|
||||
|
||||
function isKeyword(value: string): boolean {
|
||||
return ['transparent', 'currentcolor'].includes(value.toLowerCase())
|
||||
type KeywordColor = 'transparent' | 'currentColor'
|
||||
|
||||
function getKeywordColor(value: unknown): KeywordColor | null {
|
||||
if (typeof value !== 'string') return null
|
||||
let lowercased = value.toLowerCase()
|
||||
if (lowercased === 'transparent') {
|
||||
return 'transparent'
|
||||
}
|
||||
if (lowercased === 'currentcolor') {
|
||||
return 'currentColor'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getColor(
|
||||
state: State,
|
||||
keys: string[]
|
||||
): TinyColor | string | null {
|
||||
const item = dlv(state.classNames.classNames, [...keys, '__info'])
|
||||
if (!item.__rule) return null
|
||||
const props = Object.keys(removeMeta(item))
|
||||
// https://github.com/khalilgharbaoui/coloregex
|
||||
const colorRegex = new RegExp(
|
||||
`(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\\((-?[\\d.]+%?[,\\s]+){2,3}\\s*([\\d.]+%?|var\\([^)]+\\))?\\)|transparent|currentColor|${Object.keys(
|
||||
colorNames
|
||||
).join('|')})`,
|
||||
'gi'
|
||||
)
|
||||
|
||||
function getColorsInString(str: string): (TinyColor | KeywordColor)[] {
|
||||
if (/(?:box|drop)-shadow/.test(str)) return []
|
||||
|
||||
return (
|
||||
str
|
||||
.match(colorRegex)
|
||||
?.map((color) => color.replace(/var\([^)]+\)/, '1'))
|
||||
.map((color) => getKeywordColor(color) ?? new TinyColor(color))
|
||||
.filter((color) => (color instanceof TinyColor ? color.isValid : true)) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
function getColorFromDecls(
|
||||
decls: Record<string, string | string[]>
|
||||
): TinyColor | KeywordColor | null {
|
||||
let props = Object.keys(decls)
|
||||
if (props.length === 0) return null
|
||||
|
||||
const nonCustomProps = props.filter((prop) => !prop.startsWith('--'))
|
||||
|
||||
const areAllCustom = nonCustomProps.length === 0
|
||||
|
||||
if (
|
||||
!areAllCustom &&
|
||||
nonCustomProps.some((prop) => !COLOR_PROPS.includes(prop))
|
||||
) {
|
||||
if (!areAllCustom && nonCustomProps.some((prop) => !COLOR_PROPS.includes(prop))) {
|
||||
// they should all be color-based props
|
||||
return null
|
||||
}
|
||||
|
||||
const propsToCheck = areAllCustom ? props : nonCustomProps
|
||||
|
||||
const colors = flatten(
|
||||
propsToCheck.map((prop) => ensureArray(item[prop]).map(createColor))
|
||||
)
|
||||
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap(getColorsInString))
|
||||
|
||||
// check that all of the values are valid colors
|
||||
if (colors.some((color) => typeof color !== 'string' && !color.isValid)) {
|
||||
return null
|
||||
}
|
||||
// if (colors.some((color) => color instanceof TinyColor && !color.isValid)) {
|
||||
// return null
|
||||
// }
|
||||
|
||||
// check that all of the values are the same color, ignoring alpha
|
||||
const colorStrings = dedupe(
|
||||
colors.map((color) =>
|
||||
typeof color === 'string' ? color : `${color.r}-${color.g}-${color.b}`
|
||||
)
|
||||
colors.map((color) => (color instanceof TinyColor ? `${color.r}-${color.g}-${color.b}` : color))
|
||||
)
|
||||
if (colorStrings.length !== 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isKeyword(colorStrings[0])) {
|
||||
return colorStrings[0]
|
||||
let keyword = getKeywordColor(colorStrings[0])
|
||||
if (keyword) {
|
||||
return keyword
|
||||
}
|
||||
|
||||
const nonKeywordColors = colors.filter(
|
||||
(color): color is TinyColor => typeof color !== 'string'
|
||||
)
|
||||
const nonKeywordColors = colors.filter((color): color is TinyColor => typeof color !== 'string')
|
||||
|
||||
const alphas = dedupe(nonKeywordColors.map((color) => color.a))
|
||||
|
||||
|
@ -87,11 +110,48 @@ export function getColor(
|
|||
return null
|
||||
}
|
||||
|
||||
export function getColorFromValue(value: unknown): string {
|
||||
export function getColor(state: State, className: string): TinyColor | KeywordColor | null {
|
||||
if (state.jit) {
|
||||
const item = dlv(state.classNames.classNames, [className, '__info'])
|
||||
if (item && item.__rule) {
|
||||
return getColorFromDecls(removeMeta(item))
|
||||
}
|
||||
|
||||
let { root, rules } = jit.generateRules(state, [className])
|
||||
if (rules.length === 0) return null
|
||||
let decls: Record<string, string | string[]> = {}
|
||||
root.walkDecls((decl) => {
|
||||
let value = decls[decl.prop]
|
||||
if (value) {
|
||||
if (Array.isArray(value)) {
|
||||
value.push(decl.value)
|
||||
} else {
|
||||
decls[decl.prop] = [value, decl.value]
|
||||
}
|
||||
} else {
|
||||
decls[decl.prop] = decl.value
|
||||
}
|
||||
})
|
||||
return getColorFromDecls(decls)
|
||||
}
|
||||
|
||||
let parts = getClassNameParts(state, className)
|
||||
if (!parts) return null
|
||||
|
||||
const item = dlv(state.classNames.classNames, [...parts, '__info'])
|
||||
if (!item.__rule) return null
|
||||
|
||||
return getColorFromDecls(removeMeta(item))
|
||||
}
|
||||
|
||||
export function getColorFromValue(value: unknown): TinyColor | KeywordColor | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const trimmedValue = value.trim()
|
||||
if (trimmedValue === 'transparent') {
|
||||
return 'rgba(0, 0, 0, 0.01)'
|
||||
if (trimmedValue.toLowerCase() === 'transparent') {
|
||||
return 'transparent'
|
||||
}
|
||||
if (trimmedValue.toLowerCase() === 'currentcolor') {
|
||||
return 'currentColor'
|
||||
}
|
||||
if (
|
||||
!/^\s*(?:rgba?|hsla?)\s*\([^)]+\)\s*$/.test(trimmedValue) &&
|
||||
|
@ -102,20 +162,22 @@ export function getColorFromValue(value: unknown): string {
|
|||
}
|
||||
const color = new TinyColor(trimmedValue)
|
||||
if (color.isValid) {
|
||||
return color.toRgbString()
|
||||
return color
|
||||
// return { red: color.r / 255, green: color.g / 255, blue: color.b / 255, alpha: color.a }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function createColor(str: string): TinyColor | string {
|
||||
if (isKeyword(str)) {
|
||||
return str
|
||||
function createColor(str: string): TinyColor | KeywordColor {
|
||||
let keyword = getKeywordColor(str)
|
||||
if (keyword) {
|
||||
return keyword
|
||||
}
|
||||
|
||||
// matches: rgba(<r>, <g>, <b>, var(--bg-opacity))
|
||||
// TODO: support other formats? e.g. hsla, css level 4
|
||||
const match = str.match(
|
||||
/^\s*rgba\(\s*(?<r>[0-9]{1,3})\s*,\s*(?<g>[0-9]{1,3})\s*,\s*(?<b>[0-9]{1,3})\s*,\s*var/
|
||||
/^\s*rgba\(\s*(?<r>[0-9.]+)\s*,\s*(?<g>[0-9.]+)\s*,\s*(?<b>[0-9.]+)\s*,\s*var/
|
||||
)
|
||||
|
||||
if (match) {
|
||||
|
@ -128,3 +190,7 @@ function createColor(str: string): TinyColor | string {
|
|||
|
||||
return new TinyColor(str)
|
||||
}
|
||||
|
||||
export function tinyColorToVscodeColor(color: TinyColor): Color {
|
||||
return { red: color.r / 255, green: color.g / 255, blue: color.b / 255, alpha: color.a }
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
} from './lexers'
|
||||
import { getLanguageBoundaries } from './getLanguageBoundaries'
|
||||
import { resolveRange } from './resolveRange'
|
||||
import { getDocumentSettings } from './getDocumentSettings'
|
||||
const dlv = require('dlv')
|
||||
import { createMultiRegexp } from './createMultiRegexp'
|
||||
|
||||
|
@ -146,7 +145,7 @@ async function findCustomClassLists(
|
|||
doc: TextDocument,
|
||||
range?: Range
|
||||
): Promise<DocumentClassList[]> {
|
||||
const settings = await getDocumentSettings(state, doc)
|
||||
const settings = await state.editor.getConfiguration(doc.uri)
|
||||
const regexes = dlv(settings, 'experimental.classRegex', [])
|
||||
|
||||
if (!Array.isArray(regexes) || regexes.length === 0) return []
|
||||
|
|
|
@ -21,6 +21,10 @@ export function getClassNameMeta(
|
|||
}))
|
||||
}
|
||||
|
||||
if (info === undefined) {
|
||||
console.log({ classNameOrParts })
|
||||
}
|
||||
|
||||
return {
|
||||
source: info.__source,
|
||||
pseudo: info.__pseudo,
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { State, Settings } from './state'
|
||||
import type { TextDocument } from 'vscode-languageserver'
|
||||
|
||||
export async function getDocumentSettings(
|
||||
state: State,
|
||||
document?: TextDocument
|
||||
): Promise<Settings> {
|
||||
if (!state.editor.capabilities.configuration) {
|
||||
return Promise.resolve(state.editor.globalSettings)
|
||||
}
|
||||
const uri = document ? document.uri : undefined
|
||||
let result = state.editor.documentSettings.get(uri)
|
||||
if (!result) {
|
||||
result = await state.emitter.emit(
|
||||
'getConfiguration',
|
||||
document
|
||||
? {
|
||||
languageId: document.languageId,
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
state.editor.documentSettings.set(uri, result)
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { State } from './state'
|
||||
|
||||
export function getVariantsFromClassName(
|
||||
state: State,
|
||||
className: string
|
||||
): { variants: string[]; offset: number } {
|
||||
let str = className
|
||||
let allVariants = Object.keys(state.variants)
|
||||
let allVariantsByLength = allVariants.sort((a, b) => b.length - a.length)
|
||||
let variants = new Set<string>()
|
||||
let offset = 0
|
||||
|
||||
while (str) {
|
||||
let found = false
|
||||
for (let variant of allVariantsByLength) {
|
||||
if (str.startsWith(variant + state.separator)) {
|
||||
variants.add(variant)
|
||||
str = str.substr(variant.length + state.separator.length)
|
||||
offset += variant.length + state.separator.length
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found) str = ''
|
||||
}
|
||||
|
||||
return { variants: Array.from(variants), offset }
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import { State } from './state'
|
||||
import type { Container, Root, Rule } from 'postcss'
|
||||
import dlv from 'dlv'
|
||||
import { remToPx } from './remToPx'
|
||||
|
||||
export function bigSign(bigIntValue) {
|
||||
// @ts-ignore
|
||||
return (bigIntValue > 0n) - (bigIntValue < 0n)
|
||||
}
|
||||
|
||||
export function generateRules(state: State, classNames: string[]): { root: Root; rules: Rule[] } {
|
||||
let rules: [bigint, Rule][] = state.modules.jit.generateRules
|
||||
.module(new Set(classNames), state.jitContext)
|
||||
.sort(([a], [z]) => bigSign(a - z))
|
||||
|
||||
let actualRules: Rule[] = []
|
||||
|
||||
for (let [, rule] of rules) {
|
||||
if (rule.type === 'rule') {
|
||||
actualRules.push(rule)
|
||||
} else if (rule.walkRules) {
|
||||
rule.walkRules((subRule) => {
|
||||
actualRules.push(subRule)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
root: state.modules.postcss.module.root({ nodes: rules.map(([, rule]) => rule) }),
|
||||
rules: actualRules,
|
||||
}
|
||||
}
|
||||
|
||||
export async function stringifyRoot(state: State, root: Root, uri?: string): Promise<string> {
|
||||
let settings = await state.editor.getConfiguration(uri)
|
||||
let tabSize = dlv(settings, 'tabSize', 2)
|
||||
let showPixelEquivalents = dlv(settings, 'showPixelEquivalents', true)
|
||||
let rootFontSize = dlv(settings, 'rootFontSize', 16)
|
||||
|
||||
let clone = root
|
||||
|
||||
if (showPixelEquivalents) {
|
||||
clone = root.clone()
|
||||
clone.walkDecls((decl) => {
|
||||
let px = remToPx(decl.value, rootFontSize)
|
||||
if (px) {
|
||||
decl.value = `${decl.value}/* ${px} */`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return clone
|
||||
.toString()
|
||||
.replace(/([^}{;])$/gm, '$1;')
|
||||
.replace(/^(?: )+/gm, (indent: string) => ' '.repeat((indent.length / 4) * tabSize))
|
||||
}
|
||||
|
||||
export function stringifyRules(state: State, rules: Rule[], tabSize: number = 2): string {
|
||||
return rules
|
||||
.map((rule) => rule.toString().replace(/([^}{;])$/gm, '$1;'))
|
||||
.join('\n\n')
|
||||
.replace(/^(?: )+/gm, (indent: string) => ' '.repeat((indent.length / 4) * tabSize))
|
||||
}
|
||||
|
||||
export function stringifyDecls(rule: Rule): string {
|
||||
let result = []
|
||||
rule.walkDecls(({ prop, value }) => {
|
||||
result.push(`${prop}: ${value};`)
|
||||
})
|
||||
return result.join(' ')
|
||||
}
|
||||
|
||||
function replaceClassName(state: State, selector: string, find: string, replace: string): string {
|
||||
const transform = (selectors) => {
|
||||
selectors.walkClasses((className) => {
|
||||
if (className.value === find) {
|
||||
className.value = replace
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return state.modules.postcssSelectorParser.module(transform).processSync(selector)
|
||||
}
|
||||
|
||||
export function getRuleContext(state: State, rule: Rule, className: string): string[] {
|
||||
let context: string[] = [replaceClassName(state, rule.selector, className, '__placeholder__')]
|
||||
|
||||
let p: Container = rule
|
||||
while (p.parent.type !== 'root') {
|
||||
p = p.parent
|
||||
if (p.type === 'atrule') {
|
||||
// @ts-ignore
|
||||
context.unshift(`@${p.name} ${p.params}`)
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
export function remToPx(
|
||||
value: string,
|
||||
rootSize: number = 16
|
||||
): string | undefined {
|
||||
export function remToPx(value: string, rootSize: number = 16): string | undefined {
|
||||
if (/^-?[0-9.]+rem$/.test(value)) {
|
||||
let number = parseFloat(value.substr(0, value.length - 3))
|
||||
if (!isNaN(number)) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { TextDocuments, Connection, Range } from 'vscode-languageserver'
|
||||
import type { TextDocuments, Connection, Range, SymbolInformation } from 'vscode-languageserver'
|
||||
import type { TextDocument } from 'vscode-languageserver-textdocument'
|
||||
import type { Postcss } from 'postcss'
|
||||
|
||||
export type ClassNamesTree = {
|
||||
[key: string]: ClassNamesTree
|
||||
|
@ -17,13 +18,14 @@ export type ClassNames = {
|
|||
export type EditorState = {
|
||||
connection: Connection
|
||||
documents: TextDocuments<TextDocument>
|
||||
documentSettings: Map<string, Settings>
|
||||
globalSettings: Settings
|
||||
userLanguages: Record<string, string>
|
||||
capabilities: {
|
||||
configuration: boolean
|
||||
diagnosticRelatedInformation: boolean
|
||||
}
|
||||
getConfiguration: (uri?: string) => Promise<Settings>
|
||||
getDocumentSymbols: (uri: string) => Promise<SymbolInformation[]>
|
||||
}
|
||||
|
||||
type DiagnosticSeveritySetting = 'ignore' | 'warning' | 'error'
|
||||
|
@ -35,6 +37,7 @@ export type Settings = {
|
|||
validate: boolean
|
||||
showPixelEquivalents: boolean
|
||||
rootFontSize: number
|
||||
colorDecorators: 'inherit' | 'on' | 'off'
|
||||
lint: {
|
||||
cssConflict: DiagnosticSeveritySetting
|
||||
invalidApply: DiagnosticSeveritySetting
|
||||
|
@ -42,36 +45,41 @@ export type Settings = {
|
|||
invalidVariant: DiagnosticSeveritySetting
|
||||
invalidConfigPath: DiagnosticSeveritySetting
|
||||
invalidTailwindDirective: DiagnosticSeveritySetting
|
||||
incorrectVariantOrder: DiagnosticSeveritySetting
|
||||
}
|
||||
experimental: {
|
||||
classRegex: string[]
|
||||
}
|
||||
}
|
||||
|
||||
interface NotificationEmitter {
|
||||
on: (name: string, handler: (args: any) => void) => void
|
||||
off: (name: string, handler: (args: any) => void) => void
|
||||
emit: (name: string, args: any) => Promise<any>
|
||||
export interface FeatureFlags {
|
||||
future: string[]
|
||||
experimental: string[]
|
||||
}
|
||||
|
||||
export type State = null | {
|
||||
export interface State {
|
||||
enabled: boolean
|
||||
emitter?: NotificationEmitter
|
||||
version?: string
|
||||
configPath?: string
|
||||
config?: any
|
||||
modules?: {
|
||||
tailwindcss: any
|
||||
postcss: any
|
||||
}
|
||||
version?: string
|
||||
separator?: string
|
||||
plugins?: any[]
|
||||
variants?: string[]
|
||||
classNames?: ClassNames
|
||||
dependencies?: string[]
|
||||
featureFlags?: { future: string[]; experimental: string[] }
|
||||
plugins?: any
|
||||
variants?: Record<string, string | null>
|
||||
modules?: {
|
||||
tailwindcss?: { version: string; module: any }
|
||||
postcss?: { version: string; module: Postcss }
|
||||
postcssSelectorParser?: { module: any }
|
||||
resolveConfig?: { module: any }
|
||||
jit?: { generateRules: { module: any } }
|
||||
}
|
||||
browserslist?: string[]
|
||||
featureFlags?: FeatureFlags
|
||||
classNames?: ClassNames
|
||||
editor?: EditorState
|
||||
error?: Error
|
||||
jit?: boolean
|
||||
jitContext?: any
|
||||
// postcssPlugins?: { before: any[]; after: any[] }
|
||||
}
|
||||
|
||||
export type DocumentClassList = {
|
||||
|
|
|
@ -7,13 +7,14 @@ export function validateApply(
|
|||
state: State,
|
||||
classNameOrParts: string | string[]
|
||||
): { isApplyable: true } | { isApplyable: false; reason: string } | null {
|
||||
if (state.jit) {
|
||||
return { isApplyable: true }
|
||||
}
|
||||
|
||||
const meta = getClassNameMeta(state, classNameOrParts)
|
||||
if (!meta) return null
|
||||
|
||||
if (
|
||||
semver.gte(state.version, '2.0.0-alpha.1') ||
|
||||
flagEnabled(state, 'applyComplexClasses')
|
||||
) {
|
||||
if (semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')) {
|
||||
return { isApplyable: true }
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,332 @@
|
|||
/* --------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
import * as path from 'path'
|
||||
import {
|
||||
workspace as Workspace,
|
||||
window as Window,
|
||||
ExtensionContext,
|
||||
TextDocument,
|
||||
OutputChannel,
|
||||
WorkspaceFolder,
|
||||
Uri,
|
||||
commands,
|
||||
SymbolInformation,
|
||||
Position,
|
||||
Range,
|
||||
TextEditorDecorationType,
|
||||
} from 'vscode'
|
||||
import { LanguageClient, LanguageClientOptions, TransportKind } from 'vscode-languageclient/node'
|
||||
import { DEFAULT_LANGUAGES } from './lib/languages'
|
||||
import isObject from './util/isObject'
|
||||
import { dedupe, equal } from 'tailwindcss-language-service/src/util/array'
|
||||
import { names as namedColors } from '@ctrl/tinycolor'
|
||||
|
||||
const colorNames = Object.keys(namedColors)
|
||||
|
||||
const CLIENT_ID = 'tailwindcss-intellisense'
|
||||
const CLIENT_NAME = 'Tailwind CSS IntelliSense'
|
||||
|
||||
let clients: Map<string, LanguageClient> = new Map()
|
||||
let languages: Map<string, string[]> = new Map()
|
||||
|
||||
let _sortedWorkspaceFolders: string[] | undefined
|
||||
function sortedWorkspaceFolders(): string[] {
|
||||
if (_sortedWorkspaceFolders === void 0) {
|
||||
_sortedWorkspaceFolders = Workspace.workspaceFolders
|
||||
? Workspace.workspaceFolders
|
||||
.map((folder) => {
|
||||
let result = folder.uri.toString()
|
||||
if (result.charAt(result.length - 1) !== '/') {
|
||||
result = result + '/'
|
||||
}
|
||||
return result
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.length - b.length
|
||||
})
|
||||
: []
|
||||
}
|
||||
return _sortedWorkspaceFolders
|
||||
}
|
||||
Workspace.onDidChangeWorkspaceFolders(() => (_sortedWorkspaceFolders = undefined))
|
||||
|
||||
function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder {
|
||||
let sorted = sortedWorkspaceFolders()
|
||||
for (let element of sorted) {
|
||||
let uri = folder.uri.toString()
|
||||
if (uri.charAt(uri.length - 1) !== '/') {
|
||||
uri = uri + '/'
|
||||
}
|
||||
if (uri.startsWith(element)) {
|
||||
return Workspace.getWorkspaceFolder(Uri.parse(element))!
|
||||
}
|
||||
}
|
||||
return folder
|
||||
}
|
||||
|
||||
function getUserLanguages(folder?: WorkspaceFolder): Record<string, string> {
|
||||
const langs = Workspace.getConfiguration('tailwindCSS', folder).includeLanguages
|
||||
return isObject(langs) ? langs : {}
|
||||
}
|
||||
|
||||
let colorDecorationType: TextEditorDecorationType
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
let module = context.asAbsolutePath(path.join('dist', 'server', 'index.js'))
|
||||
let outputChannel: OutputChannel = Window.createOutputChannel(CLIENT_NAME)
|
||||
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('tailwindCSS.showOutput', () => {
|
||||
outputChannel.show()
|
||||
})
|
||||
)
|
||||
|
||||
// TODO: check if the actual language MAPPING changed
|
||||
// not just the language IDs
|
||||
// e.g. "plaintext" already exists but you change it from "html" to "css"
|
||||
Workspace.onDidChangeConfiguration((event) => {
|
||||
clients.forEach((client, key) => {
|
||||
const folder = Workspace.getWorkspaceFolder(Uri.parse(key))
|
||||
|
||||
if (event.affectsConfiguration('tailwindCSS', folder)) {
|
||||
const userLanguages = getUserLanguages(folder)
|
||||
if (userLanguages) {
|
||||
const userLanguageIds = Object.keys(userLanguages)
|
||||
const newLanguages = dedupe([...DEFAULT_LANGUAGES, ...userLanguageIds])
|
||||
if (!equal(newLanguages, languages.get(folder.uri.toString()))) {
|
||||
languages.set(folder.uri.toString(), newLanguages)
|
||||
|
||||
if (client) {
|
||||
clients.delete(folder.uri.toString())
|
||||
client.stop()
|
||||
bootWorkspaceClient(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function bootWorkspaceClient(folder: WorkspaceFolder) {
|
||||
if (clients.has(folder.uri.toString())) {
|
||||
return
|
||||
}
|
||||
|
||||
// placeholder so we don't boot another server before this one is ready
|
||||
clients.set(folder.uri.toString(), null)
|
||||
|
||||
let debugOptions = {
|
||||
execArgv: ['--nolazy', `--inspect=${6011 + clients.size}`],
|
||||
}
|
||||
let serverOptions = {
|
||||
run: { module, transport: TransportKind.ipc },
|
||||
debug: {
|
||||
module,
|
||||
transport: TransportKind.ipc,
|
||||
options: debugOptions,
|
||||
},
|
||||
}
|
||||
let clientOptions: LanguageClientOptions = {
|
||||
documentSelector: languages.get(folder.uri.toString()).map((language) => ({
|
||||
scheme: 'file',
|
||||
language,
|
||||
pattern: `${folder.uri.fsPath}/**/*`,
|
||||
})),
|
||||
diagnosticCollectionName: CLIENT_ID,
|
||||
workspaceFolder: folder,
|
||||
outputChannel: outputChannel,
|
||||
middleware: {
|
||||
async resolveCompletionItem(item, token, next) {
|
||||
let result = await next(item, token)
|
||||
let selections = Window.activeTextEditor.selections
|
||||
if (selections.length > 1 && result.additionalTextEdits?.length > 0) {
|
||||
let length =
|
||||
selections[0].start.character - result.additionalTextEdits[0].range.start.character
|
||||
let prefixLength =
|
||||
result.additionalTextEdits[0].range.end.character -
|
||||
result.additionalTextEdits[0].range.start.character
|
||||
|
||||
let ranges = selections.map((selection) => {
|
||||
return new Range(
|
||||
new Position(selection.start.line, selection.start.character - length),
|
||||
new Position(
|
||||
selection.start.line,
|
||||
selection.start.character - length + prefixLength
|
||||
)
|
||||
)
|
||||
})
|
||||
if (
|
||||
ranges
|
||||
.map((range) => Window.activeTextEditor.document.getText(range))
|
||||
.every((text, _index, arr) => arr.indexOf(text) === 0)
|
||||
) {
|
||||
// all the same
|
||||
result.additionalTextEdits = ranges.map((range) => {
|
||||
return { range, newText: result.additionalTextEdits[0].newText }
|
||||
})
|
||||
} else {
|
||||
result.insertText = result.label
|
||||
result.additionalTextEdits = []
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
async provideDocumentColors(document, token, next) {
|
||||
let colors = await next(document, token)
|
||||
let editableColors = colors.filter((color) => {
|
||||
let text =
|
||||
Workspace.textDocuments.find((doc) => doc === document)?.getText(color.range) ?? ''
|
||||
return new RegExp(
|
||||
`-\\[(${colorNames.join('|')}|((?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$`
|
||||
).test(text)
|
||||
})
|
||||
let nonEditableColors = colors.filter((color) => !editableColors.includes(color))
|
||||
|
||||
if (!colorDecorationType) {
|
||||
colorDecorationType = Window.createTextEditorDecorationType({
|
||||
before: {
|
||||
width: '0.8em',
|
||||
height: '0.8em',
|
||||
contentText: ' ',
|
||||
border: '0.1em solid',
|
||||
margin: '0.1em 0.2em 0',
|
||||
},
|
||||
dark: {
|
||||
before: {
|
||||
borderColor: '#eeeeee',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
before: {
|
||||
borderColor: '#000000',
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Window.visibleTextEditors
|
||||
.find((editor) => editor.document === document)
|
||||
?.setDecorations(
|
||||
colorDecorationType,
|
||||
nonEditableColors.map(({ range, color }) => ({
|
||||
range,
|
||||
renderOptions: {
|
||||
before: {
|
||||
backgroundColor: `rgba(${color.red * 255}, ${color.green * 255}, ${
|
||||
color.blue * 255
|
||||
}, ${color.alpha})`,
|
||||
},
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
return editableColors
|
||||
},
|
||||
workspace: {
|
||||
configuration: (params, token, next) => {
|
||||
try {
|
||||
return params.items.map(({ section, scopeUri }) => {
|
||||
if (section === 'tailwindCSS') {
|
||||
let scope = scopeUri
|
||||
? {
|
||||
languageId: Workspace.textDocuments.find(
|
||||
(doc) => doc.uri.toString() === scopeUri
|
||||
).languageId,
|
||||
}
|
||||
: folder
|
||||
let tabSize = Workspace.getConfiguration('editor', scope).get('tabSize') || 2
|
||||
return { tabSize, ...Workspace.getConfiguration(section, scope) }
|
||||
}
|
||||
throw Error()
|
||||
})
|
||||
} catch (_error) {
|
||||
return next(params, token)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
initializationOptions: {
|
||||
userLanguages: getUserLanguages(folder),
|
||||
configuration: Workspace.getConfiguration('tailwindCSS', folder),
|
||||
},
|
||||
synchronize: {
|
||||
configurationSection: ['editor', 'tailwindCSS'],
|
||||
},
|
||||
}
|
||||
let client = new LanguageClient(CLIENT_ID, CLIENT_NAME, serverOptions, clientOptions)
|
||||
|
||||
client.onReady().then(() => {
|
||||
client.onNotification('@/tailwindCSS/error', async ({ message }) => {
|
||||
let action = await Window.showErrorMessage(message, 'Go to output')
|
||||
if (action === 'Go to output') {
|
||||
commands.executeCommand('tailwindCSS.showOutput')
|
||||
}
|
||||
})
|
||||
|
||||
client.onNotification('@/tailwindCSS/clearColors', () => {
|
||||
if (colorDecorationType) {
|
||||
colorDecorationType.dispose()
|
||||
colorDecorationType = undefined
|
||||
}
|
||||
})
|
||||
|
||||
client.onRequest('@/tailwindCSS/getDocumentSymbols', async ({ uri }) => {
|
||||
return commands.executeCommand<SymbolInformation[]>(
|
||||
'vscode.executeDocumentSymbolProvider',
|
||||
Uri.parse(uri)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
client.start()
|
||||
clients.set(folder.uri.toString(), client)
|
||||
}
|
||||
|
||||
function didOpenTextDocument(document: TextDocument): void {
|
||||
// We are only interested in language mode text
|
||||
if (document.uri.scheme !== 'file') {
|
||||
return
|
||||
}
|
||||
|
||||
let uri = document.uri
|
||||
let folder = Workspace.getWorkspaceFolder(uri)
|
||||
// Files outside a folder can't be handled. This might depend on the language.
|
||||
// Single file languages like JSON might handle files outside the workspace folders.
|
||||
if (!folder) {
|
||||
return
|
||||
}
|
||||
// If we have nested workspace folders we only start a server on the outer most workspace folder.
|
||||
folder = getOuterMostWorkspaceFolder(folder)
|
||||
|
||||
if (!languages.has(folder.uri.toString())) {
|
||||
languages.set(
|
||||
folder.uri.toString(),
|
||||
dedupe([...DEFAULT_LANGUAGES, ...Object.keys(getUserLanguages())])
|
||||
)
|
||||
}
|
||||
|
||||
bootWorkspaceClient(folder)
|
||||
}
|
||||
|
||||
Workspace.onDidOpenTextDocument(didOpenTextDocument)
|
||||
Workspace.textDocuments.forEach(didOpenTextDocument)
|
||||
Workspace.onDidChangeWorkspaceFolders((event) => {
|
||||
for (let folder of event.removed) {
|
||||
let client = clients.get(folder.uri.toString())
|
||||
if (client) {
|
||||
clients.delete(folder.uri.toString())
|
||||
client.stop()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> {
|
||||
let promises: Thenable<void>[] = []
|
||||
for (let client of clients.values()) {
|
||||
promises.push(client.stop())
|
||||
}
|
||||
return Promise.all(promises).then(() => undefined)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import Module from 'module'
|
||||
import * as path from 'path'
|
||||
import resolveFrom from '../util/resolveFrom'
|
||||
import builtInModules from 'builtin-modules'
|
||||
|
||||
process.env.TAILWIND_MODE = 'build'
|
||||
process.env.TAILWIND_DISABLE_TOUCH = 'true'
|
||||
|
||||
let oldResolveFilename = (Module as any)._resolveFilename
|
||||
|
||||
;(Module as any)._resolveFilename = (id: any, parent: any) => {
|
||||
if (builtInModules.includes(id)) {
|
||||
return oldResolveFilename(id, parent)
|
||||
}
|
||||
return resolveFrom(path.dirname(parent.id), id)
|
||||
}
|
|
@ -1,39 +1,47 @@
|
|||
import selectorParser from 'postcss-selector-parser'
|
||||
import dset from 'dset'
|
||||
import dlv from 'dlv'
|
||||
import type { Container, Node, Root, AtRule } from 'postcss'
|
||||
|
||||
function isAtRule(node: Node): node is AtRule {
|
||||
return node.type === 'atrule'
|
||||
}
|
||||
|
||||
function createSelectorFromNodes(nodes) {
|
||||
if (nodes.length === 0) return null
|
||||
const selector = selectorParser.selector()
|
||||
const selector = selectorParser.selector({ value: '' })
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
selector.append(nodes[i])
|
||||
}
|
||||
return String(selector).trim()
|
||||
}
|
||||
|
||||
function getClassNamesFromSelector(selector) {
|
||||
function getClassNamesFromSelector(selector: string) {
|
||||
const classNames = []
|
||||
const { nodes: subSelectors } = selectorParser().astSync(selector)
|
||||
|
||||
for (let i = 0; i < subSelectors.length; i++) {
|
||||
let subSelector = subSelectors[i]
|
||||
if (subSelector.type !== 'selector') continue
|
||||
|
||||
let scope = []
|
||||
for (let j = 0; j < subSelectors[i].nodes.length; j++) {
|
||||
let node = subSelectors[i].nodes[j]
|
||||
for (let j = 0; j < subSelector.nodes.length; j++) {
|
||||
let node = subSelector.nodes[j]
|
||||
let pseudo = []
|
||||
|
||||
if (node.type === 'class') {
|
||||
let next = subSelectors[i].nodes[j + 1]
|
||||
let next = subSelector.nodes[j + 1]
|
||||
|
||||
while (next && next.type === 'pseudo') {
|
||||
pseudo.push(next)
|
||||
j++
|
||||
next = subSelectors[i].nodes[j + 1]
|
||||
next = subSelector.nodes[j + 1]
|
||||
}
|
||||
|
||||
classNames.push({
|
||||
className: node.value.trim(),
|
||||
scope: createSelectorFromNodes(scope),
|
||||
__rule: j === subSelectors[i].nodes.length - 1,
|
||||
__rule: j === subSelector.nodes.length - 1,
|
||||
__pseudo: pseudo.map(String),
|
||||
})
|
||||
}
|
||||
|
@ -44,7 +52,7 @@ function getClassNamesFromSelector(selector) {
|
|||
return classNames
|
||||
}
|
||||
|
||||
async function process(root) {
|
||||
async function process(root: Root) {
|
||||
const tree = {}
|
||||
const commonContext = {}
|
||||
|
||||
|
@ -68,9 +76,7 @@ async function process(root) {
|
|||
rule.walkDecls((decl) => {
|
||||
if (decls[decl.prop]) {
|
||||
decls[decl.prop] = [
|
||||
...(Array.isArray(decls[decl.prop])
|
||||
? decls[decl.prop]
|
||||
: [decls[decl.prop]]),
|
||||
...(Array.isArray(decls[decl.prop]) ? decls[decl.prop] : [decls[decl.prop]]),
|
||||
decl.value,
|
||||
]
|
||||
} else {
|
||||
|
@ -78,11 +84,11 @@ async function process(root) {
|
|||
}
|
||||
})
|
||||
|
||||
let p = rule
|
||||
let p: Container = rule
|
||||
const keys = []
|
||||
while (p.parent.type !== 'root') {
|
||||
p = p.parent
|
||||
if (p.type === 'atrule') {
|
||||
if (isAtRule(p)) {
|
||||
keys.push(`@${p.name} ${p.params}`)
|
||||
}
|
||||
}
|
||||
|
@ -104,25 +110,13 @@ async function process(root) {
|
|||
}
|
||||
if (classNames[i].__rule) {
|
||||
dset(tree, [...baseKeys, '__info', ...index, '__rule'], true)
|
||||
dset(tree, [...baseKeys, '__info', ...index, '__source'], layer)
|
||||
|
||||
dsetEach(tree, [...baseKeys, '__info', ...index], decls)
|
||||
}
|
||||
dset(
|
||||
tree,
|
||||
[...baseKeys, '__info', ...index, '__pseudo'],
|
||||
classNames[i].__pseudo
|
||||
)
|
||||
dset(
|
||||
tree,
|
||||
[...baseKeys, '__info', ...index, '__scope'],
|
||||
classNames[i].scope
|
||||
)
|
||||
dset(
|
||||
tree,
|
||||
[...baseKeys, '__info', ...index, '__context'],
|
||||
context.concat([]).reverse()
|
||||
)
|
||||
dset(tree, [...baseKeys, '__info', ...index, '__source'], layer)
|
||||
dset(tree, [...baseKeys, '__info', ...index, '__pseudo'], classNames[i].__pseudo)
|
||||
dset(tree, [...baseKeys, '__info', ...index, '__scope'], classNames[i].scope)
|
||||
dset(tree, [...baseKeys, '__info', ...index, '__context'], context.concat([]).reverse())
|
||||
|
||||
// common context
|
||||
context.push(...classNames[i].__pseudo.map((x) => `&${x}`))
|
||||
|
@ -131,10 +125,7 @@ async function process(root) {
|
|||
if (typeof commonContext[contextKeys[i]] === 'undefined') {
|
||||
commonContext[contextKeys[i]] = context
|
||||
} else {
|
||||
commonContext[contextKeys[i]] = intersection(
|
||||
commonContext[contextKeys[i]],
|
||||
context
|
||||
)
|
||||
commonContext[contextKeys[i]] = intersection(commonContext[contextKeys[i]], context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,26 +134,15 @@ async function process(root) {
|
|||
return { classNames: tree, context: commonContext }
|
||||
}
|
||||
|
||||
function intersection(arr1, arr2) {
|
||||
function intersection<T>(arr1: T[], arr2: T[]): T[] {
|
||||
return arr1.filter((value) => arr2.indexOf(value) !== -1)
|
||||
}
|
||||
|
||||
function dsetEach(obj, keys, values) {
|
||||
function dsetEach(obj, keys: string[], values: Record<string, string>) {
|
||||
const k = Object.keys(values)
|
||||
for (let i = 0; i < k.length; i++) {
|
||||
dset(obj, [...keys, k[i]], values[k[i]])
|
||||
}
|
||||
}
|
||||
|
||||
function arraysEqual(a, b) {
|
||||
if (a === b) return true
|
||||
if (a == null || b == null) return false
|
||||
if (a.length !== b.length) return false
|
||||
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default process
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Adapted from: https://github.com/elastic/require-in-the-middle
|
||||
*/
|
||||
import Module from 'module'
|
||||
|
||||
export default class Hook {
|
||||
cache = {}
|
||||
deps: string[] = []
|
||||
private _watching: boolean = false
|
||||
private _unhooked: boolean = false
|
||||
private _origRequire = Module.prototype.require
|
||||
private _require: (req: string) => any
|
||||
|
||||
constructor(find: string, callback: (x) => {}) {
|
||||
// @ts-ignore
|
||||
if (typeof Module._resolveFilename !== 'function') {
|
||||
throw new Error(
|
||||
// @ts-ignore
|
||||
`Error: Expected Module._resolveFilename to be a function (was: ${typeof Module._resolveFilename}) - aborting!`
|
||||
)
|
||||
}
|
||||
|
||||
let self = this
|
||||
let patching = {}
|
||||
|
||||
// @ts-ignore
|
||||
this._require = Module.prototype.require = function (request) {
|
||||
if (self._unhooked) {
|
||||
// if the patched require function could not be removed because
|
||||
// someone else patched it after it was patched here, we just
|
||||
// abort and pass the request onwards to the original require
|
||||
return self._origRequire.apply(this, arguments)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
let filename = Module._resolveFilename(request, this)
|
||||
|
||||
// return known patched modules immediately
|
||||
if (self.cache.hasOwnProperty(filename)) {
|
||||
return self.cache[filename]
|
||||
}
|
||||
|
||||
// Check if this module has a patcher in-progress already.
|
||||
// Otherwise, mark this module as patching in-progress.
|
||||
let patched = patching[filename]
|
||||
if (!patched) {
|
||||
patching[filename] = true
|
||||
}
|
||||
|
||||
let exports = self._origRequire.apply(this, arguments)
|
||||
|
||||
if (filename !== find) {
|
||||
if (self._watching) {
|
||||
self.deps.push(filename)
|
||||
}
|
||||
return exports
|
||||
}
|
||||
|
||||
// If it's already patched, just return it as-is.
|
||||
if (patched) return exports
|
||||
|
||||
// The module has already been loaded,
|
||||
// so the patching mark can be cleaned up.
|
||||
delete patching[filename]
|
||||
|
||||
// only call onrequire the first time a module is loaded
|
||||
if (!self.cache.hasOwnProperty(filename)) {
|
||||
// ensure that the cache entry is assigned a value before calling
|
||||
// onrequire, in case calling onrequire requires the same module.
|
||||
self.cache[filename] = exports
|
||||
self.cache[filename] = callback(exports)
|
||||
}
|
||||
|
||||
return self.cache[filename]
|
||||
}
|
||||
}
|
||||
|
||||
unhook() {
|
||||
this._unhooked = true
|
||||
if (this._require === Module.prototype.require) {
|
||||
Module.prototype.require = this._origRequire
|
||||
}
|
||||
}
|
||||
|
||||
watch() {
|
||||
this._watching = true
|
||||
}
|
||||
|
||||
unwatch() {
|
||||
this._watching = false
|
||||
}
|
||||
}
|
|
@ -1,29 +1,29 @@
|
|||
import { TextDocument } from 'vscode-languageserver'
|
||||
import { State } from '../../util/state'
|
||||
import { doValidate } from 'tailwindcss-language-service'
|
||||
import { TextDocument } from 'vscode-languageserver/node'
|
||||
import { State } from 'tailwindcss-language-service/src/util/state'
|
||||
import { doValidate } from 'tailwindcss-language-service/src/diagnostics/diagnosticsProvider'
|
||||
|
||||
export async function provideDiagnostics(state: State, document: TextDocument) {
|
||||
state.editor.connection.sendDiagnostics({
|
||||
state.editor?.connection.sendDiagnostics({
|
||||
uri: document.uri,
|
||||
diagnostics: await doValidate(state, document),
|
||||
})
|
||||
}
|
||||
|
||||
export function clearDiagnostics(state: State, document: TextDocument): void {
|
||||
state.editor.connection.sendDiagnostics({
|
||||
state.editor?.connection.sendDiagnostics({
|
||||
uri: document.uri,
|
||||
diagnostics: [],
|
||||
})
|
||||
}
|
||||
|
||||
export function clearAllDiagnostics(state: State): void {
|
||||
state.editor.documents.all().forEach((document) => {
|
||||
state.editor?.documents.all().forEach((document) => {
|
||||
clearDiagnostics(state, document)
|
||||
})
|
||||
}
|
||||
|
||||
export function updateAllDiagnostics(state: State): void {
|
||||
state.editor.documents.all().forEach((document) => {
|
||||
state.editor?.documents.all().forEach((document) => {
|
||||
provideDiagnostics(state, document)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { Connection } from 'vscode-languageserver/node'
|
||||
|
||||
function toString(err: any, includeStack: boolean = true): string {
|
||||
if (err instanceof Error) {
|
||||
let error = <Error>err
|
||||
return `${error.message}${includeStack ? `\n${error.stack}` : ''}`
|
||||
} else if (typeof err === 'string') {
|
||||
return err
|
||||
} else {
|
||||
return err.toString()
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/vscode-langservers/vscode-json-languageserver/blob/master/src/utils/runner.ts
|
||||
export function formatError(message: string, err: any, includeStack: boolean = true): string {
|
||||
if (err) {
|
||||
return `${message}: ${toString(err, includeStack)}`
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
export function showError(
|
||||
connection: Connection,
|
||||
err: any,
|
||||
message: string = 'Tailwind CSS'
|
||||
): void {
|
||||
console.error(formatError(message, err))
|
||||
if (!(err instanceof SilentError)) {
|
||||
connection.sendNotification('@/tailwindCSS/error', {
|
||||
message: formatError(message, err, false),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function SilentError(message: string) {
|
||||
this.name = 'SilentError'
|
||||
this.message = message
|
||||
this.stack = new Error().stack
|
||||
}
|
||||
SilentError.prototype = new Error()
|
|
@ -0,0 +1,26 @@
|
|||
import * as fs from 'fs'
|
||||
import { CachedInputFileSystem, ResolverFactory, Resolver, ResolveOptions } from 'enhanced-resolve'
|
||||
|
||||
function createResolver(options: Partial<ResolveOptions> = {}): Resolver {
|
||||
return ResolverFactory.createResolver({
|
||||
fileSystem: new CachedInputFileSystem(fs, 4000),
|
||||
useSyncFileSystemCalls: true,
|
||||
// cachePredicate: () => false,
|
||||
exportsFields: [],
|
||||
conditionNames: ['node'],
|
||||
extensions: ['.js', '.json', '.node'],
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
let resolver = createResolver()
|
||||
|
||||
export function setPnpApi(pnpApi: any): void {
|
||||
resolver = createResolver({ pnpApi })
|
||||
}
|
||||
|
||||
export default function resolveFrom(from?: string, id?: string): string {
|
||||
let result = resolver.resolveSync({}, from, id)
|
||||
if (result === false) throw Error()
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"lib": ["ES2019"],
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"tailwindcss-language-service/*": ["packages/tailwindcss-language-service/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "packages/tailwindcss-language-service"]
|
||||
}
|