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
|
node_modules
|
||||||
|
dist
|
||||||
|
*.vsix
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
|
node_modules/**
|
||||||
.vscode/**
|
.vscode/**
|
||||||
|
.github/**
|
||||||
|
src/**
|
||||||
|
packages/**
|
||||||
|
tests/**
|
||||||
**/*.ts
|
**/*.ts
|
||||||
**/*.map
|
**/*.map
|
||||||
.gitignore
|
.gitignore
|
||||||
**/tsconfig.json
|
**/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",
|
"name": "vscode-tailwindcss",
|
||||||
"private": true,
|
"displayName": "Tailwind CSS IntelliSense",
|
||||||
"workspaces": [
|
"description": "Intelligent Tailwind CSS tooling for VS Code",
|
||||||
"packages/*"
|
"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": {
|
"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"
|
"lint": "tsdx lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/tinycolor": "^3.1.4",
|
"@ctrl/tinycolor": "3.1.4",
|
||||||
"@types/moo": "^0.5.3",
|
"@types/moo": "0.5.3",
|
||||||
"css.escape": "^1.5.1",
|
"css.escape": "1.5.1",
|
||||||
"detect-indent": "^6.0.0",
|
"detect-indent": "6.0.0",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "1.1.3",
|
||||||
"line-column": "^1.0.2",
|
"dset": "2.0.1",
|
||||||
"mitt": "^2.1.0",
|
"line-column": "1.0.2",
|
||||||
"moo": "^0.5.1",
|
"moo": "0.5.1",
|
||||||
"multi-regexp2": "^1.0.3",
|
"multi-regexp2": "1.0.3",
|
||||||
"semver": "^7.3.2",
|
"postcss-selector-parser": "6.0.2",
|
||||||
"sift-string": "^0.0.2",
|
"semver": "7.3.2",
|
||||||
"tsdx": "^0.13.3",
|
"sift-string": "0.0.2",
|
||||||
"tslib": "^2.0.1",
|
"vscode-emmet-helper-bundled": "0.0.1",
|
||||||
"typescript": "^4.0.2",
|
"vscode-languageclient": "7.0.0",
|
||||||
"vscode-emmet-helper-bundled": "^0.0.1",
|
"vscode-languageserver": "7.0.0",
|
||||||
"vscode-languageclient": "^6.1.3",
|
"vscode-languageserver-textdocument": "1.0.1"
|
||||||
"vscode-languageserver": "^6.1.1",
|
},
|
||||||
"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,
|
isInvalidTailwindDirectiveDiagnostic,
|
||||||
isInvalidScreenDiagnostic,
|
isInvalidScreenDiagnostic,
|
||||||
isInvalidVariantDiagnostic,
|
isInvalidVariantDiagnostic,
|
||||||
|
isIncorrectVariantOrderDiagnostic,
|
||||||
} from '../diagnostics/types'
|
} from '../diagnostics/types'
|
||||||
import { flatten, dedupeBy } from '../util/array'
|
import { flatten, dedupeBy } from '../util/array'
|
||||||
import { provideCssConflictCodeActions } from './provideCssConflictCodeActions'
|
import { provideCssConflictCodeActions } from './provideCssConflictCodeActions'
|
||||||
|
@ -38,10 +39,7 @@ async function getDiagnosticsFromCodeActionParams(
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doCodeActions(
|
export async function doCodeActions(state: State, params: CodeActionParams): Promise<CodeAction[]> {
|
||||||
state: State,
|
|
||||||
params: CodeActionParams
|
|
||||||
): Promise<CodeAction[]> {
|
|
||||||
let diagnostics = await getDiagnosticsFromCodeActionParams(
|
let diagnostics = await getDiagnosticsFromCodeActionParams(
|
||||||
state,
|
state,
|
||||||
params,
|
params,
|
||||||
|
@ -64,7 +62,8 @@ export async function doCodeActions(
|
||||||
isInvalidConfigPathDiagnostic(diagnostic) ||
|
isInvalidConfigPathDiagnostic(diagnostic) ||
|
||||||
isInvalidTailwindDirectiveDiagnostic(diagnostic) ||
|
isInvalidTailwindDirectiveDiagnostic(diagnostic) ||
|
||||||
isInvalidScreenDiagnostic(diagnostic) ||
|
isInvalidScreenDiagnostic(diagnostic) ||
|
||||||
isInvalidVariantDiagnostic(diagnostic)
|
isInvalidVariantDiagnostic(diagnostic) ||
|
||||||
|
isIncorrectVariantOrderDiagnostic(diagnostic)
|
||||||
) {
|
) {
|
||||||
return provideSuggestionCodeActions(state, params, diagnostic)
|
return provideSuggestionCodeActions(state, params, diagnostic)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
import type {
|
import type { CodeAction, CodeActionParams, TextEdit, Range } from 'vscode-languageserver'
|
||||||
CodeAction,
|
|
||||||
CodeActionParams,
|
|
||||||
TextEdit,
|
|
||||||
Range,
|
|
||||||
} from 'vscode-languageserver'
|
|
||||||
import { State } from '../util/state'
|
import { State } from '../util/state'
|
||||||
import { InvalidApplyDiagnostic } from '../diagnostics/types'
|
import { InvalidApplyDiagnostic } from '../diagnostics/types'
|
||||||
import { isCssDoc } from '../util/css'
|
import { isCssDoc } from '../util/css'
|
||||||
|
@ -13,7 +8,7 @@ import { getClassNameParts } from '../util/getClassNameAtPosition'
|
||||||
import { validateApply } from '../util/validateApply'
|
import { validateApply } from '../util/validateApply'
|
||||||
import { isWithinRange } from '../util/isWithinRange'
|
import { isWithinRange } from '../util/isWithinRange'
|
||||||
const dlv = require('dlv')
|
const dlv = require('dlv')
|
||||||
import type { Root, NodeSource } from 'postcss'
|
import type { Root, Source } from 'postcss'
|
||||||
import { absoluteRange } from '../util/absoluteRange'
|
import { absoluteRange } from '../util/absoluteRange'
|
||||||
import { removeRangesFromString } from '../util/removeRangesFromString'
|
import { removeRangesFromString } from '../util/removeRangesFromString'
|
||||||
import detectIndent from 'detect-indent'
|
import detectIndent from 'detect-indent'
|
||||||
|
@ -35,9 +30,7 @@ export async function provideInvalidApplyCodeActions(
|
||||||
const { postcss } = state.modules
|
const { postcss } = state.modules
|
||||||
let changes: TextEdit[] = []
|
let changes: TextEdit[] = []
|
||||||
|
|
||||||
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
|
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(/\s+/).length
|
||||||
/\s+/
|
|
||||||
).length
|
|
||||||
|
|
||||||
let className = diagnostic.className.className
|
let className = diagnostic.className.className
|
||||||
let classNameParts = getClassNameParts(state, className)
|
let classNameParts = getClassNameParts(state, className)
|
||||||
|
@ -50,90 +43,88 @@ export async function provideInvalidApplyCodeActions(
|
||||||
if (!isCssDoc(state, document)) {
|
if (!isCssDoc(state, document)) {
|
||||||
let languageBoundaries = getLanguageBoundaries(state, document)
|
let languageBoundaries = getLanguageBoundaries(state, document)
|
||||||
if (!languageBoundaries) return []
|
if (!languageBoundaries) return []
|
||||||
cssRange = languageBoundaries.css.find((range) =>
|
cssRange = languageBoundaries.css.find((range) => isWithinRange(diagnostic.range.start, range))
|
||||||
isWithinRange(diagnostic.range.start, range)
|
|
||||||
)
|
|
||||||
if (!cssRange) return []
|
if (!cssRange) return []
|
||||||
cssText = document.getText(cssRange)
|
cssText = document.getText(cssRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await postcss([
|
await postcss
|
||||||
postcss.plugin('', (_options = {}) => {
|
.module([
|
||||||
return (root: Root) => {
|
// TODO: use plain function?
|
||||||
root.walkRules((rule) => {
|
// @ts-ignore
|
||||||
if (changes.length) return false
|
postcss.module.plugin('', (_options = {}) => {
|
||||||
|
return (root: Root) => {
|
||||||
|
root.walkRules((rule) => {
|
||||||
|
if (changes.length) return false
|
||||||
|
|
||||||
rule.walkAtRules('apply', (atRule) => {
|
rule.walkAtRules('apply', (atRule) => {
|
||||||
let atRuleRange = postcssSourceToRange(atRule.source)
|
let atRuleRange = postcssSourceToRange(atRule.source)
|
||||||
if (cssRange) {
|
if (cssRange) {
|
||||||
atRuleRange = absoluteRange(atRuleRange, cssRange)
|
atRuleRange = absoluteRange(atRuleRange, cssRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWithinRange(diagnostic.range.start, atRuleRange))
|
if (!isWithinRange(diagnostic.range.start, atRuleRange)) return undefined // true
|
||||||
return true
|
|
||||||
|
|
||||||
let ast = classNameToAst(
|
let ast = classNameToAst(
|
||||||
state,
|
state,
|
||||||
classNameParts,
|
classNameParts,
|
||||||
rule.selector,
|
rule.selector,
|
||||||
diagnostic.className.classList.important
|
diagnostic.className.classList.important
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!ast) return false
|
if (!ast) return false
|
||||||
|
|
||||||
rule.after(ast.nodes)
|
rule.after(ast.nodes)
|
||||||
let insertedRule = rule.next()
|
let insertedRule = rule.next()
|
||||||
if (!insertedRule) return false
|
if (!insertedRule) return false
|
||||||
|
|
||||||
|
if (totalClassNamesInClassList === 1) {
|
||||||
|
atRule.remove()
|
||||||
|
} else {
|
||||||
|
changes.push({
|
||||||
|
range: diagnostic.className.classList.range,
|
||||||
|
newText: removeRangesFromString(
|
||||||
|
diagnostic.className.classList.classList,
|
||||||
|
diagnostic.className.relativeRange
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let ruleRange = postcssSourceToRange(rule.source)
|
||||||
|
if (cssRange) {
|
||||||
|
ruleRange = absoluteRange(ruleRange, cssRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputIndent: string
|
||||||
|
let documentIndent = detectIndent(cssText)
|
||||||
|
|
||||||
if (totalClassNamesInClassList === 1) {
|
|
||||||
atRule.remove()
|
|
||||||
} else {
|
|
||||||
changes.push({
|
changes.push({
|
||||||
range: diagnostic.className.classList.range,
|
range: ruleRange,
|
||||||
newText: removeRangesFromString(
|
newText:
|
||||||
diagnostic.className.classList.classList,
|
rule.toString() +
|
||||||
diagnostic.className.relativeRange
|
(insertedRule.raws.before || '\n\n') +
|
||||||
),
|
insertedRule
|
||||||
|
.toString()
|
||||||
|
.replace(/\n\s*\n/g, '\n')
|
||||||
|
.replace(/(@apply [^;\n]+)$/gm, '$1;')
|
||||||
|
.replace(/([^\s^]){$/gm, '$1 {')
|
||||||
|
.replace(/^\s+/gm, (m: string) => {
|
||||||
|
if (typeof outputIndent === 'undefined') outputIndent = m
|
||||||
|
return m.replace(new RegExp(outputIndent, 'g'), documentIndent.indent)
|
||||||
|
})
|
||||||
|
.replace(/^(\s+)(.*?[^{}]\n)([^\s}])/gm, '$1$2$1$3'),
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
let ruleRange = postcssSourceToRange(rule.source)
|
return false
|
||||||
if (cssRange) {
|
|
||||||
ruleRange = absoluteRange(ruleRange, cssRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
let outputIndent: string
|
|
||||||
let documentIndent = detectIndent(cssText)
|
|
||||||
|
|
||||||
changes.push({
|
|
||||||
range: ruleRange,
|
|
||||||
newText:
|
|
||||||
rule.toString() +
|
|
||||||
(insertedRule.raws.before || '\n\n') +
|
|
||||||
insertedRule
|
|
||||||
.toString()
|
|
||||||
.replace(/\n\s*\n/g, '\n')
|
|
||||||
.replace(/(@apply [^;\n]+)$/gm, '$1;')
|
|
||||||
.replace(/([^\s^]){$/gm, '$1 {')
|
|
||||||
.replace(/^\s+/gm, (m: string) => {
|
|
||||||
if (typeof outputIndent === 'undefined') outputIndent = m
|
|
||||||
return m.replace(
|
|
||||||
new RegExp(outputIndent, 'g'),
|
|
||||||
documentIndent.indent
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.replace(/^(\s+)(.*?[^{}]\n)([^\s}])/gm, '$1$2$1$3'),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return false
|
return undefined // true
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return true
|
}),
|
||||||
})
|
])
|
||||||
}
|
.process(cssText, { from: undefined })
|
||||||
}),
|
|
||||||
]).process(cssText, { from: undefined })
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -156,7 +147,7 @@ export async function provideInvalidApplyCodeActions(
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function postcssSourceToRange(source: NodeSource): Range {
|
function postcssSourceToRange(source: Source): Range {
|
||||||
return {
|
return {
|
||||||
start: {
|
start: {
|
||||||
line: source.start.line - 1,
|
line: source.start.line - 1,
|
||||||
|
@ -177,10 +168,7 @@ function classNameToAst(
|
||||||
) {
|
) {
|
||||||
const baseClassName = classNameParts[classNameParts.length - 1]
|
const baseClassName = classNameParts[classNameParts.length - 1]
|
||||||
const validatedBaseClassName = validateApply(state, [baseClassName])
|
const validatedBaseClassName = validateApply(state, [baseClassName])
|
||||||
if (
|
if (validatedBaseClassName === null || validatedBaseClassName.isApplyable === false) {
|
||||||
validatedBaseClassName === null ||
|
|
||||||
validatedBaseClassName.isApplyable === false
|
|
||||||
) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const meta = getClassNameMeta(state, classNameParts)
|
const meta = getClassNameMeta(state, classNameParts)
|
||||||
|
@ -188,11 +176,7 @@ function classNameToAst(
|
||||||
let context = meta.context
|
let context = meta.context
|
||||||
let pseudo = meta.pseudo
|
let pseudo = meta.pseudo
|
||||||
const globalContexts = state.classNames.context
|
const globalContexts = state.classNames.context
|
||||||
let screens = dlv(
|
let screens = dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
|
||||||
state.config,
|
|
||||||
'theme.screens',
|
|
||||||
dlv(state.config, 'screens', {})
|
|
||||||
)
|
|
||||||
if (!isObject(screens)) screens = {}
|
if (!isObject(screens)) screens = {}
|
||||||
screens = Object.keys(screens)
|
screens = Object.keys(screens)
|
||||||
const path = []
|
const path = []
|
||||||
|
@ -231,10 +215,7 @@ function classNameToAst(
|
||||||
return cssObjToAst(obj, state.modules.postcss)
|
return cssObjToAst(obj, state.modules.postcss)
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendPseudosToSelector(
|
function appendPseudosToSelector(selector: string, pseudos: string[]): string | null {
|
||||||
selector: string,
|
|
||||||
pseudos: string[]
|
|
||||||
): string | null {
|
|
||||||
if (pseudos.length === 0) return selector
|
if (pseudos.length === 0) return selector
|
||||||
|
|
||||||
let canTransform = true
|
let canTransform = true
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { State } from '../util/state'
|
import { State } from '../util/state'
|
||||||
import type {
|
import type { CodeActionParams, CodeAction } from 'vscode-languageserver'
|
||||||
CodeActionParams,
|
|
||||||
CodeAction,
|
|
||||||
} from 'vscode-languageserver'
|
|
||||||
import {
|
import {
|
||||||
InvalidConfigPathDiagnostic,
|
InvalidConfigPathDiagnostic,
|
||||||
InvalidTailwindDirectiveDiagnostic,
|
InvalidTailwindDirectiveDiagnostic,
|
||||||
InvalidScreenDiagnostic,
|
InvalidScreenDiagnostic,
|
||||||
InvalidVariantDiagnostic,
|
InvalidVariantDiagnostic,
|
||||||
|
IncorrectVariantOrderDiagnostic,
|
||||||
} from '../diagnostics/types'
|
} from '../diagnostics/types'
|
||||||
|
|
||||||
export function provideSuggestionCodeActions(
|
export function provideSuggestionCodeActions(
|
||||||
|
@ -18,6 +16,7 @@ export function provideSuggestionCodeActions(
|
||||||
| InvalidTailwindDirectiveDiagnostic
|
| InvalidTailwindDirectiveDiagnostic
|
||||||
| InvalidScreenDiagnostic
|
| InvalidScreenDiagnostic
|
||||||
| InvalidVariantDiagnostic
|
| InvalidVariantDiagnostic
|
||||||
|
| IncorrectVariantOrderDiagnostic
|
||||||
): CodeAction[] {
|
): CodeAction[] {
|
||||||
return diagnostic.suggestions.map((suggestion) => ({
|
return diagnostic.suggestions.map((suggestion) => ({
|
||||||
title: `Replace with '${suggestion}'`,
|
title: `Replace with '${suggestion}'`,
|
||||||
|
|
|
@ -19,26 +19,31 @@ import { stringifyScreen, Screen } from './util/screens'
|
||||||
import isObject from './util/isObject'
|
import isObject from './util/isObject'
|
||||||
import * as emmetHelper from 'vscode-emmet-helper-bundled'
|
import * as emmetHelper from 'vscode-emmet-helper-bundled'
|
||||||
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
|
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
|
||||||
import { getDocumentSettings } from './util/getDocumentSettings'
|
|
||||||
import { isJsContext } from './util/js'
|
import { isJsContext } from './util/js'
|
||||||
import { naturalExpand } from './util/naturalExpand'
|
import { naturalExpand } from './util/naturalExpand'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
import { docsUrl } from './util/docsUrl'
|
import { docsUrl } from './util/docsUrl'
|
||||||
import { ensureArray } from './util/array'
|
import { ensureArray } from './util/array'
|
||||||
import {
|
import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers'
|
||||||
getClassAttributeLexer,
|
|
||||||
getComputedClassAttributeLexer,
|
|
||||||
} from './util/lexers'
|
|
||||||
import { validateApply } from './util/validateApply'
|
import { validateApply } from './util/validateApply'
|
||||||
import { flagEnabled } from './util/flagEnabled'
|
import { flagEnabled } from './util/flagEnabled'
|
||||||
import { remToPx } from './util/remToPx'
|
import { remToPx } from './util/remToPx'
|
||||||
import { createMultiRegexp } from './util/createMultiRegexp'
|
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(
|
export function completionsFromClassList(
|
||||||
state: State,
|
state: State,
|
||||||
classList: string,
|
classList: string,
|
||||||
classListRange: Range,
|
classListRange: Range,
|
||||||
filter?: (item: CompletionItem) => boolean
|
filter?: (item: CompletionItem) => boolean,
|
||||||
|
document?: TextDocument
|
||||||
): CompletionList {
|
): CompletionList {
|
||||||
let classNames = classList.split(/[\s+]/)
|
let classNames = classList.split(/[\s+]/)
|
||||||
const partialClassName = classNames[classNames.length - 1]
|
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--) {
|
for (let i = parts.length - 1; i > 0; i--) {
|
||||||
let keys = parts.slice(0, i).filter(Boolean)
|
let keys = parts.slice(0, i).filter(Boolean)
|
||||||
subset = dlv(state.classNames.classNames, keys)
|
subset = dlv(state.classNames.classNames, keys)
|
||||||
if (
|
if (typeof subset !== 'undefined' && typeof dlv(subset, ['__info', '__rule']) === 'undefined') {
|
||||||
typeof subset !== 'undefined' &&
|
|
||||||
typeof dlv(subset, ['__info', '__rule']) === 'undefined'
|
|
||||||
) {
|
|
||||||
isSubset = true
|
isSubset = true
|
||||||
subsetKey = keys
|
subsetKey = keys
|
||||||
replacementRange = {
|
replacementRange = {
|
||||||
...replacementRange,
|
...replacementRange,
|
||||||
start: {
|
start: {
|
||||||
...replacementRange.start,
|
...replacementRange.start,
|
||||||
character:
|
character: replacementRange.start.character + keys.join(sep).length + sep.length,
|
||||||
replacementRange.start.character +
|
|
||||||
keys.join(sep).length +
|
|
||||||
sep.length,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -106,17 +214,13 @@ export function completionsFromClassList(
|
||||||
.concat(
|
.concat(
|
||||||
Object.keys(isSubset ? subset : state.classNames.classNames)
|
Object.keys(isSubset ? subset : state.classNames.classNames)
|
||||||
.filter((className) =>
|
.filter((className) =>
|
||||||
dlv(state.classNames.classNames, [
|
dlv(state.classNames.classNames, [...subsetKey, className, '__info'])
|
||||||
...subsetKey,
|
|
||||||
className,
|
|
||||||
'__info',
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
.map((className, index) => {
|
.map((className, index) => {
|
||||||
let kind: CompletionItemKind = 21
|
let kind: CompletionItemKind = 21
|
||||||
let documentation: string = null
|
let documentation: string = null
|
||||||
|
|
||||||
const color = getColor(state, [className])
|
const color = getColor(state, className)
|
||||||
if (color !== null) {
|
if (color !== null) {
|
||||||
kind = 16
|
kind = 16
|
||||||
if (typeof color !== 'string' && color.a !== 0) {
|
if (typeof color !== 'string' && color.a !== 0) {
|
||||||
|
@ -159,10 +263,7 @@ function provideClassAttributeCompletions(
|
||||||
end: position,
|
end: position,
|
||||||
})
|
})
|
||||||
|
|
||||||
const match = findLast(
|
const match = findLast(/(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi, str)
|
||||||
/(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi,
|
|
||||||
str
|
|
||||||
)
|
|
||||||
|
|
||||||
if (match === null) {
|
if (match === null) {
|
||||||
return null
|
return null
|
||||||
|
@ -187,13 +288,19 @@ function provideClassAttributeCompletions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return completionsFromClassList(state, classList, {
|
return completionsFromClassList(
|
||||||
start: {
|
state,
|
||||||
line: position.line,
|
classList,
|
||||||
character: position.character - classList.length,
|
{
|
||||||
|
start: {
|
||||||
|
line: position.line,
|
||||||
|
character: position.character - classList.length,
|
||||||
|
},
|
||||||
|
end: position,
|
||||||
},
|
},
|
||||||
end: position,
|
undefined,
|
||||||
})
|
document
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
@ -205,7 +312,7 @@ async function provideCustomClassNameCompletions(
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
position: Position
|
position: Position
|
||||||
): Promise<CompletionList> {
|
): Promise<CompletionList> {
|
||||||
const settings = await getDocumentSettings(state, document)
|
const settings = await state.editor.getConfiguration(document.uri)
|
||||||
const regexes = dlv(settings, 'experimental.classRegex', [])
|
const regexes = dlv(settings, 'experimental.classRegex', [])
|
||||||
if (regexes.length === 0) return null
|
if (regexes.length === 0) return null
|
||||||
|
|
||||||
|
@ -220,9 +327,7 @@ async function provideCustomClassNameCompletions(
|
||||||
|
|
||||||
for (let i = 0; i < regexes.length; i++) {
|
for (let i = 0; i < regexes.length; i++) {
|
||||||
try {
|
try {
|
||||||
let [containerRegex, classRegex] = Array.isArray(regexes[i])
|
let [containerRegex, classRegex] = Array.isArray(regexes[i]) ? regexes[i] : [regexes[i]]
|
||||||
? regexes[i]
|
|
||||||
: [regexes[i]]
|
|
||||||
|
|
||||||
containerRegex = createMultiRegexp(containerRegex)
|
containerRegex = createMultiRegexp(containerRegex)
|
||||||
let containerMatch
|
let containerMatch
|
||||||
|
@ -239,9 +344,7 @@ async function provideCustomClassNameCompletions(
|
||||||
classRegex = createMultiRegexp(classRegex)
|
classRegex = createMultiRegexp(classRegex)
|
||||||
let classMatch
|
let classMatch
|
||||||
|
|
||||||
while (
|
while ((classMatch = classRegex.exec(containerMatch.match)) !== null) {
|
||||||
(classMatch = classRegex.exec(containerMatch.match)) !== null
|
|
||||||
) {
|
|
||||||
const classMatchStart = matchStart + classMatch.start
|
const classMatchStart = matchStart + classMatch.start
|
||||||
const classMatchEnd = matchStart + classMatch.end
|
const classMatchEnd = matchStart + classMatch.end
|
||||||
if (cursor >= classMatchStart && cursor <= classMatchEnd) {
|
if (cursor >= classMatchStart && cursor <= classMatchEnd) {
|
||||||
|
@ -302,8 +405,7 @@ function provideAtApplyCompletions(
|
||||||
(item) => {
|
(item) => {
|
||||||
if (item.kind === 9) {
|
if (item.kind === 9) {
|
||||||
return (
|
return (
|
||||||
semver.gte(state.version, '2.0.0-alpha.1') ||
|
semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')
|
||||||
flagEnabled(state, 'applyComplexClasses')
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let validated = validateApply(state, item.data)
|
let validated = validateApply(state, item.data)
|
||||||
|
@ -317,10 +419,7 @@ function provideClassNameCompletions(
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
position: Position
|
position: Position
|
||||||
): CompletionList {
|
): CompletionList {
|
||||||
if (
|
if (isHtmlContext(state, document, position) || isJsContext(state, document, position)) {
|
||||||
isHtmlContext(state, document, position) ||
|
|
||||||
isJsContext(state, document, position)
|
|
||||||
) {
|
|
||||||
return provideClassAttributeCompletions(state, document, position)
|
return provideClassAttributeCompletions(state, document, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,10 +453,7 @@ function provideCssHelperCompletions(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let base =
|
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
|
||||||
match.groups.helper === 'config'
|
|
||||||
? state.config
|
|
||||||
: dlv(state.config, 'theme', {})
|
|
||||||
let parts = match.groups.keys.split(/([\[\].]+)/)
|
let parts = match.groups.keys.split(/([\[\].]+)/)
|
||||||
let keys = parts.filter((_, i) => i % 2 === 0)
|
let keys = parts.filter((_, i) => i % 2 === 0)
|
||||||
let separators = parts.filter((_, i) => i % 2 !== 0)
|
let separators = parts.filter((_, i) => i % 2 !== 0)
|
||||||
|
@ -372,9 +468,7 @@ function provideCssHelperCompletions(
|
||||||
|
|
||||||
let obj: any
|
let obj: any
|
||||||
let offset: number = 0
|
let offset: number = 0
|
||||||
let separator: string = separators.length
|
let separator: string = separators.length ? separators[separators.length - 1] : null
|
||||||
? separators[separators.length - 1]
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (keys.length === 1) {
|
if (keys.length === 1) {
|
||||||
obj = base
|
obj = base
|
||||||
|
@ -396,8 +490,7 @@ function provideCssHelperCompletions(
|
||||||
isIncomplete: false,
|
isIncomplete: false,
|
||||||
items: Object.keys(obj).map((item, index) => {
|
items: Object.keys(obj).map((item, index) => {
|
||||||
let color = getColorFromValue(obj[item])
|
let color = getColorFromValue(obj[item])
|
||||||
const replaceDot: boolean =
|
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
|
||||||
item.indexOf('.') !== -1 && separator && separator.endsWith('.')
|
|
||||||
const insertClosingBrace: boolean =
|
const insertClosingBrace: boolean =
|
||||||
text.charAt(text.length - 1) !== ']' &&
|
text.charAt(text.length - 1) !== ']' &&
|
||||||
(replaceDot || (separator && separator.endsWith('[')))
|
(replaceDot || (separator && separator.endsWith('[')))
|
||||||
|
@ -408,21 +501,16 @@ function provideCssHelperCompletions(
|
||||||
filterText: `${replaceDot ? '.' : ''}${item}`,
|
filterText: `${replaceDot ? '.' : ''}${item}`,
|
||||||
sortText: naturalExpand(index),
|
sortText: naturalExpand(index),
|
||||||
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
|
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
|
||||||
// VS Code bug causes '0' to not display in some cases
|
// VS Code bug causes some values to not display in some cases
|
||||||
detail: detail === '0' ? '0 ' : detail,
|
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
|
||||||
documentation: color,
|
documentation: color instanceof TinyColor && color.a !== 0 ? color.toRgbString() : null,
|
||||||
textEdit: {
|
textEdit: {
|
||||||
newText: `${replaceDot ? '[' : ''}${item}${
|
newText: `${replaceDot ? '[' : ''}${item}${insertClosingBrace ? ']' : ''}`,
|
||||||
insertClosingBrace ? ']' : ''
|
|
||||||
}`,
|
|
||||||
range: {
|
range: {
|
||||||
start: {
|
start: {
|
||||||
line: position.line,
|
line: position.line,
|
||||||
character:
|
character:
|
||||||
position.character -
|
position.character - keys[keys.length - 1].length - (replaceDot ? 1 : 0) - offset,
|
||||||
keys[keys.length - 1].length -
|
|
||||||
(replaceDot ? 1 : 0) -
|
|
||||||
offset,
|
|
||||||
},
|
},
|
||||||
end: position,
|
end: position,
|
||||||
},
|
},
|
||||||
|
@ -433,7 +521,6 @@ function provideCssHelperCompletions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: vary docs links based on Tailwind version
|
|
||||||
function provideTailwindDirectiveCompletions(
|
function provideTailwindDirectiveCompletions(
|
||||||
state: State,
|
state: State,
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
|
@ -550,13 +637,15 @@ function provideVariantsDirectiveCompletions(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isIncomplete: false,
|
isIncomplete: false,
|
||||||
items: state.variants
|
items: Object.keys(state.variants)
|
||||||
.filter((v) => existingVariants.indexOf(v) === -1)
|
.filter((v) => existingVariants.indexOf(v) === -1)
|
||||||
.map((variant) => ({
|
.map((variant, index) => ({
|
||||||
// TODO: detail
|
// TODO: detail
|
||||||
label: variant,
|
label: variant,
|
||||||
|
detail: state.variants[variant],
|
||||||
kind: 21,
|
kind: 21,
|
||||||
data: 'variant',
|
data: 'variant',
|
||||||
|
sortText: naturalExpand(index),
|
||||||
textEdit: {
|
textEdit: {
|
||||||
newText: variant,
|
newText: variant,
|
||||||
range: {
|
range: {
|
||||||
|
@ -628,11 +717,7 @@ function provideScreenDirectiveCompletions(
|
||||||
|
|
||||||
if (match === null) return null
|
if (match === null) return null
|
||||||
|
|
||||||
const screens = dlv(
|
const screens = dlv(state.config, ['screens'], dlv(state.config, ['theme', 'screens'], {}))
|
||||||
state.config,
|
|
||||||
['screens'],
|
|
||||||
dlv(state.config, ['theme', 'screens'], {})
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isObject(screens)) return null
|
if (!isObject(screens)) return null
|
||||||
|
|
||||||
|
@ -767,7 +852,7 @@ async function provideEmmetCompletions(
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
position: Position
|
position: Position
|
||||||
): Promise<CompletionList> {
|
): Promise<CompletionList> {
|
||||||
let settings = await getDocumentSettings(state, document)
|
let settings = await state.editor.getConfiguration(document.uri)
|
||||||
if (settings.emmetCompletions !== true) return null
|
if (settings.emmetCompletions !== true) return null
|
||||||
|
|
||||||
const isHtml = isHtmlContext(state, document, position)
|
const isHtml = isHtmlContext(state, document, position)
|
||||||
|
@ -779,26 +864,16 @@ async function provideEmmetCompletions(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractAbbreviationResults = emmetHelper.extractAbbreviation(
|
const extractAbbreviationResults = emmetHelper.extractAbbreviation(document, position, true)
|
||||||
document,
|
|
||||||
position,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
!extractAbbreviationResults ||
|
!extractAbbreviationResults ||
|
||||||
!emmetHelper.isAbbreviationValid(
|
!emmetHelper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)
|
||||||
syntax,
|
|
||||||
extractAbbreviationResults.abbreviation
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isValidLocationForEmmetAbbreviation(
|
!isValidLocationForEmmetAbbreviation(document, extractAbbreviationResults.abbreviationRange)
|
||||||
document,
|
|
||||||
extractAbbreviationResults.abbreviationRange
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -808,16 +883,13 @@ async function provideEmmetCompletions(
|
||||||
if (abbreviation.startsWith('this.')) {
|
if (abbreviation.startsWith('this.')) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const { symbols } = await state.emitter.emit('getDocumentSymbols', {
|
const symbols = await state.editor.getDocumentSymbols(document.uri)
|
||||||
uri: document.uri,
|
|
||||||
})
|
|
||||||
if (
|
if (
|
||||||
symbols &&
|
symbols &&
|
||||||
symbols.find(
|
symbols.find(
|
||||||
(symbol) =>
|
(symbol) =>
|
||||||
abbreviation === symbol.name ||
|
abbreviation === symbol.name ||
|
||||||
(abbreviation.startsWith(symbol.name + '.') &&
|
(abbreviation.startsWith(symbol.name + '.') && !/>|\*|\+/.test(abbreviation))
|
||||||
!/>|\*|\+/.test(abbreviation))
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
|
@ -847,11 +919,7 @@ async function provideEmmetCompletions(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doComplete(
|
export async function doComplete(state: State, document: TextDocument, position: Position) {
|
||||||
state: State,
|
|
||||||
document: TextDocument,
|
|
||||||
position: Position
|
|
||||||
) {
|
|
||||||
if (state === null) return { items: [], isIncomplete: false }
|
if (state === null) return { items: [], isIncomplete: false }
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
|
@ -873,32 +941,44 @@ export async function resolveCompletionItem(
|
||||||
state: State,
|
state: State,
|
||||||
item: CompletionItem
|
item: CompletionItem
|
||||||
): Promise<CompletionItem> {
|
): Promise<CompletionItem> {
|
||||||
if (
|
if (['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)) {
|
||||||
['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)
|
|
||||||
) {
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.data === 'screen') {
|
if (item.data === 'screen') {
|
||||||
let screens = dlv(
|
let screens = dlv(state.config, ['theme', 'screens'], dlv(state.config, ['screens'], {}))
|
||||||
state.config,
|
|
||||||
['theme', 'screens'],
|
|
||||||
dlv(state.config, ['screens'], {})
|
|
||||||
)
|
|
||||||
if (!isObject(screens)) screens = {}
|
if (!isObject(screens)) screens = {}
|
||||||
item.detail = stringifyScreen(screens[item.label] as Screen)
|
item.detail = stringifyScreen(screens[item.label] as Screen)
|
||||||
return item
|
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'])
|
const className = dlv(state.classNames.classNames, [...item.data, '__info'])
|
||||||
if (item.kind === 9) {
|
if (item.kind === 9) {
|
||||||
item.detail = state.classNames.context[
|
item.detail = state.classNames.context[item.data[item.data.length - 1]].join(', ')
|
||||||
item.data[item.data.length - 1]
|
|
||||||
].join(', ')
|
|
||||||
} else {
|
} else {
|
||||||
item.detail = await getCssDetail(state, className)
|
item.detail = await getCssDetail(state, className)
|
||||||
if (!item.documentation) {
|
if (!item.documentation) {
|
||||||
const settings = await getDocumentSettings(state)
|
const settings = await state.editor.getConfiguration()
|
||||||
const css = stringifyCss(item.data.join(':'), className, {
|
const css = stringifyCss(item.data.join(':'), className, {
|
||||||
tabSize: dlv(settings, 'tabSize', 2),
|
tabSize: dlv(settings, 'tabSize', 2),
|
||||||
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
|
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
|
||||||
|
@ -949,9 +1029,7 @@ function stringifyDecls(
|
||||||
.map((prop) =>
|
.map((prop) =>
|
||||||
ensureArray(obj[prop])
|
ensureArray(obj[prop])
|
||||||
.map((value) => {
|
.map((value) => {
|
||||||
const px = showPixelEquivalents
|
const px = showPixelEquivalents ? remToPx(value, rootFontSize) : undefined
|
||||||
? remToPx(value, rootFontSize)
|
|
||||||
: undefined
|
|
||||||
return `${prop}: ${value}${px ? `/* ${px} */` : ''};`
|
return `${prop}: ${value}${px ? `/* ${px} */` : ''};`
|
||||||
})
|
})
|
||||||
.join(' ')
|
.join(' ')
|
||||||
|
@ -964,7 +1042,7 @@ async function getCssDetail(state: State, className: any): Promise<string> {
|
||||||
return `${className.length} rules`
|
return `${className.length} rules`
|
||||||
}
|
}
|
||||||
if (className.__rule === true) {
|
if (className.__rule === true) {
|
||||||
const settings = await getDocumentSettings(state)
|
const settings = await state.editor.getConfiguration()
|
||||||
return stringifyDecls(removeMeta(className), {
|
return stringifyDecls(removeMeta(className), {
|
||||||
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
|
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
|
||||||
rootFontSize: dlv(settings, 'rootFontSize', 16),
|
rootFontSize: dlv(settings, 'rootFontSize', 16),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { TextDocument } from 'vscode-languageserver'
|
import type { TextDocument } from 'vscode-languageserver'
|
||||||
import { State } from '../util/state'
|
import { State } from '../util/state'
|
||||||
import { getDocumentSettings } from '../util/getDocumentSettings'
|
|
||||||
import { DiagnosticKind, AugmentedDiagnostic } from './types'
|
import { DiagnosticKind, AugmentedDiagnostic } from './types'
|
||||||
import { getCssConflictDiagnostics } from './getCssConflictDiagnostics'
|
import { getCssConflictDiagnostics } from './getCssConflictDiagnostics'
|
||||||
import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics'
|
import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics'
|
||||||
|
@ -8,6 +7,7 @@ import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics'
|
||||||
import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics'
|
import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics'
|
||||||
import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics'
|
import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics'
|
||||||
import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics'
|
import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics'
|
||||||
|
import { getIncorrectVariantOrderDiagnostics } from './getIncorrectVariantOrderDiagnostics'
|
||||||
|
|
||||||
export async function doValidate(
|
export async function doValidate(
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -19,9 +19,10 @@ export async function doValidate(
|
||||||
DiagnosticKind.InvalidVariant,
|
DiagnosticKind.InvalidVariant,
|
||||||
DiagnosticKind.InvalidConfigPath,
|
DiagnosticKind.InvalidConfigPath,
|
||||||
DiagnosticKind.InvalidTailwindDirective,
|
DiagnosticKind.InvalidTailwindDirective,
|
||||||
|
DiagnosticKind.IncorrectVariantOrder,
|
||||||
]
|
]
|
||||||
): Promise<AugmentedDiagnostic[]> {
|
): Promise<AugmentedDiagnostic[]> {
|
||||||
const settings = await getDocumentSettings(state, document)
|
const settings = await state.editor.getConfiguration(document.uri)
|
||||||
|
|
||||||
return settings.validate
|
return settings.validate
|
||||||
? [
|
? [
|
||||||
|
@ -43,22 +44,25 @@ export async function doValidate(
|
||||||
...(only.includes(DiagnosticKind.InvalidTailwindDirective)
|
...(only.includes(DiagnosticKind.InvalidTailwindDirective)
|
||||||
? getInvalidTailwindDirectiveDiagnostics(state, document, settings)
|
? getInvalidTailwindDirectiveDiagnostics(state, document, settings)
|
||||||
: []),
|
: []),
|
||||||
|
...(only.includes(DiagnosticKind.IncorrectVariantOrder)
|
||||||
|
? await getIncorrectVariantOrderDiagnostics(state, document, settings)
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function provideDiagnostics(state: State, document: TextDocument) {
|
export async function provideDiagnostics(state: State, document: TextDocument) {
|
||||||
state.editor.connection.sendDiagnostics({
|
// state.editor.connection.sendDiagnostics({
|
||||||
uri: document.uri,
|
// uri: document.uri,
|
||||||
diagnostics: await doValidate(state, document),
|
// diagnostics: await doValidate(state, document),
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearDiagnostics(state: State, document: TextDocument): void {
|
export function clearDiagnostics(state: State, document: TextDocument): void {
|
||||||
state.editor.connection.sendDiagnostics({
|
// state.editor.connection.sendDiagnostics({
|
||||||
uri: document.uri,
|
// uri: document.uri,
|
||||||
diagnostics: [],
|
// diagnostics: [],
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAllDiagnostics(state: State): void {
|
export function clearAllDiagnostics(state: State): void {
|
||||||
|
|
|
@ -2,13 +2,11 @@ import { joinWithAnd } from '../util/joinWithAnd'
|
||||||
import { State, Settings } from '../util/state'
|
import { State, Settings } from '../util/state'
|
||||||
import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
|
import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
|
||||||
import { CssConflictDiagnostic, DiagnosticKind } from './types'
|
import { CssConflictDiagnostic, DiagnosticKind } from './types'
|
||||||
import {
|
import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'
|
||||||
findClassListsInDocument,
|
|
||||||
getClassNamesInClassList,
|
|
||||||
} from '../util/find'
|
|
||||||
import { getClassNameDecls } from '../util/getClassNameDecls'
|
import { getClassNameDecls } from '../util/getClassNameDecls'
|
||||||
import { getClassNameMeta } from '../util/getClassNameMeta'
|
import { getClassNameMeta } from '../util/getClassNameMeta'
|
||||||
import { equal } from '../util/array'
|
import { equal } from '../util/array'
|
||||||
|
import * as jit from '../util/jit'
|
||||||
|
|
||||||
export async function getCssConflictDiagnostics(
|
export async function getCssConflictDiagnostics(
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -25,6 +23,81 @@ export async function getCssConflictDiagnostics(
|
||||||
const classNames = getClassNamesInClassList(classList)
|
const classNames = getClassNamesInClassList(classList)
|
||||||
|
|
||||||
classNames.forEach((className, index) => {
|
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)
|
let decls = getClassNameDecls(state, className.className)
|
||||||
if (!decls) return
|
if (!decls) return
|
||||||
|
|
||||||
|
@ -63,21 +136,17 @@ export async function getCssConflictDiagnostics(
|
||||||
message: `'${className.className}' applies the same CSS ${
|
message: `'${className.className}' applies the same CSS ${
|
||||||
properties.length === 1 ? 'property' : 'properties'
|
properties.length === 1 ? 'property' : 'properties'
|
||||||
} as ${joinWithAnd(
|
} as ${joinWithAnd(
|
||||||
conflictingClassNames.map(
|
conflictingClassNames.map((conflictingClassName) => `'${conflictingClassName.className}'`)
|
||||||
(conflictingClassName) => `'${conflictingClassName.className}'`
|
|
||||||
)
|
|
||||||
)}.`,
|
)}.`,
|
||||||
relatedInformation: conflictingClassNames.map(
|
relatedInformation: conflictingClassNames.map((conflictingClassName) => {
|
||||||
(conflictingClassName) => {
|
return {
|
||||||
return {
|
message: conflictingClassName.className,
|
||||||
message: conflictingClassName.className,
|
location: {
|
||||||
location: {
|
uri: document.uri,
|
||||||
uri: document.uri,
|
range: conflictingClassName.range,
|
||||||
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,
|
code: DiagnosticKind.InvalidTailwindDirective,
|
||||||
range: absoluteRange(
|
range: absoluteRange(
|
||||||
{
|
{
|
||||||
start: indexToPosition(
|
start: indexToPosition(text, match.index + match[0].length - match.groups.value.length),
|
||||||
text,
|
|
||||||
match.index + match[0].length - match.groups.value.length
|
|
||||||
),
|
|
||||||
end: indexToPosition(text, match.index + match[0].length),
|
end: indexToPosition(text, match.index + match[0].length),
|
||||||
},
|
},
|
||||||
range
|
range
|
||||||
|
|
|
@ -37,13 +37,13 @@ export function getInvalidVariantDiagnostics(
|
||||||
|
|
||||||
for (let i = 0; i < variants.length; i += 2) {
|
for (let i = 0; i < variants.length; i += 2) {
|
||||||
let variant = variants[i].trim()
|
let variant = variants[i].trim()
|
||||||
if (state.variants.includes(variant)) {
|
if (Object.keys(state.variants).includes(variant)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = `The variant '${variant}' does not exist.`
|
let message = `The variant '${variant}' does not exist.`
|
||||||
let suggestions: string[] = []
|
let suggestions: string[] = []
|
||||||
let suggestion = closest(variant, state.variants)
|
let suggestion = closest(variant, Object.keys(state.variants))
|
||||||
|
|
||||||
if (suggestion) {
|
if (suggestion) {
|
||||||
suggestions.push(suggestion)
|
suggestions.push(suggestion)
|
||||||
|
|
|
@ -8,6 +8,7 @@ export enum DiagnosticKind {
|
||||||
InvalidVariant = 'invalidVariant',
|
InvalidVariant = 'invalidVariant',
|
||||||
InvalidConfigPath = 'invalidConfigPath',
|
InvalidConfigPath = 'invalidConfigPath',
|
||||||
InvalidTailwindDirective = 'invalidTailwindDirective',
|
InvalidTailwindDirective = 'invalidTailwindDirective',
|
||||||
|
IncorrectVariantOrder = 'incorrectVariantOrder',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CssConflictDiagnostic = Diagnostic & {
|
export type CssConflictDiagnostic = Diagnostic & {
|
||||||
|
@ -77,6 +78,17 @@ export function isInvalidTailwindDirectiveDiagnostic(
|
||||||
return diagnostic.code === DiagnosticKind.InvalidTailwindDirective
|
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 =
|
export type AugmentedDiagnostic =
|
||||||
| CssConflictDiagnostic
|
| CssConflictDiagnostic
|
||||||
| InvalidApplyDiagnostic
|
| InvalidApplyDiagnostic
|
||||||
|
@ -84,3 +96,4 @@ export type AugmentedDiagnostic =
|
||||||
| InvalidVariantDiagnostic
|
| InvalidVariantDiagnostic
|
||||||
| InvalidConfigPathDiagnostic
|
| InvalidConfigPathDiagnostic
|
||||||
| InvalidTailwindDirectiveDiagnostic
|
| InvalidTailwindDirectiveDiagnostic
|
||||||
|
| IncorrectVariantOrderDiagnostic
|
||||||
|
|
|
@ -4,27 +4,34 @@ import {
|
||||||
getClassNamesInClassList,
|
getClassNamesInClassList,
|
||||||
findHelperFunctionsInDocument,
|
findHelperFunctionsInDocument,
|
||||||
} from './util/find'
|
} from './util/find'
|
||||||
import { getClassNameParts } from './util/getClassNameAtPosition'
|
import { getColor, getColorFromValue, tinyColorToVscodeColor } from './util/color'
|
||||||
import { getColor, getColorFromValue } from './util/color'
|
|
||||||
import { stringToPath } from './util/stringToPath'
|
import { stringToPath } from './util/stringToPath'
|
||||||
import type { TextDocument } from 'vscode-languageserver'
|
import type { TextDocument, ColorInformation } from 'vscode-languageserver'
|
||||||
const dlv = require('dlv')
|
import { TinyColor } from '@ctrl/tinycolor'
|
||||||
|
import dlv from 'dlv'
|
||||||
|
|
||||||
export async function getDocumentColors(state: State, document: TextDocument) {
|
export async function getDocumentColors(
|
||||||
let colors = []
|
state: State,
|
||||||
|
document: TextDocument
|
||||||
|
): Promise<ColorInformation[]> {
|
||||||
|
let colors: ColorInformation[] = []
|
||||||
if (!state.enabled) return colors
|
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)
|
let classLists = await findClassListsInDocument(state, document)
|
||||||
classLists.forEach((classList) => {
|
classLists.forEach((classList) => {
|
||||||
let classNames = getClassNamesInClassList(classList)
|
let classNames = getClassNamesInClassList(classList)
|
||||||
classNames.forEach((className) => {
|
classNames.forEach((className) => {
|
||||||
let parts = getClassNameParts(state, className.className)
|
let color = getColor(state, className.className)
|
||||||
if (!parts) return
|
|
||||||
let color = getColor(state, parts)
|
|
||||||
if (color === null || typeof color === 'string' || color.a === 0) {
|
if (color === null || typeof color === 'string' || color.a === 0) {
|
||||||
return
|
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 base = fn.helper === 'theme' ? ['theme'] : []
|
||||||
let value = dlv(state.config, [...base, ...keys])
|
let value = dlv(state.config, [...base, ...keys])
|
||||||
let color = getColorFromValue(value)
|
let color = getColorFromValue(value)
|
||||||
if (color) {
|
if (color instanceof TinyColor && color.a !== 0) {
|
||||||
colors.push({ range: fn.valueRange, color })
|
colors.push({ range: fn.valueRange, color: tinyColorToVscodeColor(color) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { isCssContext } from './util/css'
|
||||||
import { findClassNameAtPosition } from './util/find'
|
import { findClassNameAtPosition } from './util/find'
|
||||||
import { validateApply } from './util/validateApply'
|
import { validateApply } from './util/validateApply'
|
||||||
import { getClassNameParts } from './util/getClassNameAtPosition'
|
import { getClassNameParts } from './util/getClassNameAtPosition'
|
||||||
import { getDocumentSettings } from './util/getDocumentSettings'
|
import * as jit from './util/jit'
|
||||||
|
|
||||||
export async function doHover(
|
export async function doHover(
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -19,11 +19,7 @@ export async function doHover(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function provideCssHelperHover(
|
function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover {
|
||||||
state: State,
|
|
||||||
document: TextDocument,
|
|
||||||
position: Position
|
|
||||||
): Hover {
|
|
||||||
if (!isCssContext(state, document, position)) return null
|
if (!isCssContext(state, document, position)) return null
|
||||||
|
|
||||||
const line = document.getText({
|
const line = document.getText({
|
||||||
|
@ -31,9 +27,7 @@ function provideCssHelperHover(
|
||||||
end: { line: position.line + 1, character: 0 },
|
end: { line: position.line + 1, character: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
const match = line.match(
|
const match = line.match(/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/)
|
||||||
/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/
|
|
||||||
)
|
|
||||||
|
|
||||||
if (match === null) return null
|
if (match === null) return null
|
||||||
|
|
||||||
|
@ -80,6 +74,22 @@ async function provideClassNameHover(
|
||||||
let className = await findClassNameAtPosition(state, document, position)
|
let className = await findClassNameAtPosition(state, document, position)
|
||||||
if (className === null) return null
|
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)
|
const parts = getClassNameParts(state, className.className)
|
||||||
if (!parts) return null
|
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(
|
const css = stringifyCss(
|
||||||
className.className,
|
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)
|
return arr.filter((value, index, self) => self.indexOf(value) === index)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dedupeBy<T>(
|
export function dedupeBy<T>(arr: Array<T>, transform: (item: T) => any): Array<T> {
|
||||||
arr: Array<T>,
|
return arr.filter((value, index, self) => self.map(transform).indexOf(transform(value)) === index)
|
||||||
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[] {
|
export function ensureArray<T>(value: T | T[]): T[] {
|
||||||
|
@ -20,9 +14,27 @@ export function flatten<T>(arrays: T[][]): T[] {
|
||||||
return [].concat.apply([], arrays)
|
return [].concat.apply([], arrays)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function equal(arr1: any[], arr2: any[]): boolean {
|
export function equal(a: any[], b: any[]): boolean {
|
||||||
return (
|
if (a === b) return true
|
||||||
JSON.stringify(arr1.concat([]).sort()) ===
|
if (a.length !== b.length) return false
|
||||||
JSON.stringify(arr2.concat([]).sort())
|
|
||||||
)
|
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 removeMeta from './removeMeta'
|
||||||
import { TinyColor, names as colorNames } from '@ctrl/tinycolor'
|
import { TinyColor, names as colorNames } from '@ctrl/tinycolor'
|
||||||
import { ensureArray, dedupe, flatten } from './array'
|
import { ensureArray, dedupe, flatten } from './array'
|
||||||
|
import type { Color } from 'vscode-languageserver'
|
||||||
|
import { getClassNameParts } from './getClassNameAtPosition'
|
||||||
|
import * as jit from './jit'
|
||||||
|
|
||||||
const COLOR_PROPS = [
|
const COLOR_PROPS = [
|
||||||
'caret-color',
|
'caret-color',
|
||||||
|
@ -21,58 +24,78 @@ const COLOR_PROPS = [
|
||||||
'text-decoration-color',
|
'text-decoration-color',
|
||||||
]
|
]
|
||||||
|
|
||||||
function isKeyword(value: string): boolean {
|
type KeywordColor = 'transparent' | 'currentColor'
|
||||||
return ['transparent', 'currentcolor'].includes(value.toLowerCase())
|
|
||||||
|
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(
|
// https://github.com/khalilgharbaoui/coloregex
|
||||||
state: State,
|
const colorRegex = new RegExp(
|
||||||
keys: string[]
|
`(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\\((-?[\\d.]+%?[,\\s]+){2,3}\\s*([\\d.]+%?|var\\([^)]+\\))?\\)|transparent|currentColor|${Object.keys(
|
||||||
): TinyColor | string | null {
|
colorNames
|
||||||
const item = dlv(state.classNames.classNames, [...keys, '__info'])
|
).join('|')})`,
|
||||||
if (!item.__rule) return null
|
'gi'
|
||||||
const props = Object.keys(removeMeta(item))
|
)
|
||||||
|
|
||||||
|
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
|
if (props.length === 0) return null
|
||||||
|
|
||||||
const nonCustomProps = props.filter((prop) => !prop.startsWith('--'))
|
const nonCustomProps = props.filter((prop) => !prop.startsWith('--'))
|
||||||
|
|
||||||
const areAllCustom = nonCustomProps.length === 0
|
const areAllCustom = nonCustomProps.length === 0
|
||||||
|
|
||||||
if (
|
if (!areAllCustom && nonCustomProps.some((prop) => !COLOR_PROPS.includes(prop))) {
|
||||||
!areAllCustom &&
|
|
||||||
nonCustomProps.some((prop) => !COLOR_PROPS.includes(prop))
|
|
||||||
) {
|
|
||||||
// they should all be color-based props
|
// they should all be color-based props
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const propsToCheck = areAllCustom ? props : nonCustomProps
|
const propsToCheck = areAllCustom ? props : nonCustomProps
|
||||||
|
|
||||||
const colors = flatten(
|
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap(getColorsInString))
|
||||||
propsToCheck.map((prop) => ensureArray(item[prop]).map(createColor))
|
|
||||||
)
|
|
||||||
|
|
||||||
// check that all of the values are valid colors
|
// check that all of the values are valid colors
|
||||||
if (colors.some((color) => typeof color !== 'string' && !color.isValid)) {
|
// if (colors.some((color) => color instanceof TinyColor && !color.isValid)) {
|
||||||
return null
|
// return null
|
||||||
}
|
// }
|
||||||
|
|
||||||
// check that all of the values are the same color, ignoring alpha
|
// check that all of the values are the same color, ignoring alpha
|
||||||
const colorStrings = dedupe(
|
const colorStrings = dedupe(
|
||||||
colors.map((color) =>
|
colors.map((color) => (color instanceof TinyColor ? `${color.r}-${color.g}-${color.b}` : color))
|
||||||
typeof color === 'string' ? color : `${color.r}-${color.g}-${color.b}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if (colorStrings.length !== 1) {
|
if (colorStrings.length !== 1) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isKeyword(colorStrings[0])) {
|
let keyword = getKeywordColor(colorStrings[0])
|
||||||
return colorStrings[0]
|
if (keyword) {
|
||||||
|
return keyword
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonKeywordColors = colors.filter(
|
const nonKeywordColors = colors.filter((color): color is TinyColor => typeof color !== 'string')
|
||||||
(color): color is TinyColor => typeof color !== 'string'
|
|
||||||
)
|
|
||||||
|
|
||||||
const alphas = dedupe(nonKeywordColors.map((color) => color.a))
|
const alphas = dedupe(nonKeywordColors.map((color) => color.a))
|
||||||
|
|
||||||
|
@ -87,11 +110,48 @@ export function getColor(
|
||||||
return null
|
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
|
if (typeof value !== 'string') return null
|
||||||
const trimmedValue = value.trim()
|
const trimmedValue = value.trim()
|
||||||
if (trimmedValue === 'transparent') {
|
if (trimmedValue.toLowerCase() === 'transparent') {
|
||||||
return 'rgba(0, 0, 0, 0.01)'
|
return 'transparent'
|
||||||
|
}
|
||||||
|
if (trimmedValue.toLowerCase() === 'currentcolor') {
|
||||||
|
return 'currentColor'
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!/^\s*(?:rgba?|hsla?)\s*\([^)]+\)\s*$/.test(trimmedValue) &&
|
!/^\s*(?:rgba?|hsla?)\s*\([^)]+\)\s*$/.test(trimmedValue) &&
|
||||||
|
@ -102,20 +162,22 @@ export function getColorFromValue(value: unknown): string {
|
||||||
}
|
}
|
||||||
const color = new TinyColor(trimmedValue)
|
const color = new TinyColor(trimmedValue)
|
||||||
if (color.isValid) {
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function createColor(str: string): TinyColor | string {
|
function createColor(str: string): TinyColor | KeywordColor {
|
||||||
if (isKeyword(str)) {
|
let keyword = getKeywordColor(str)
|
||||||
return str
|
if (keyword) {
|
||||||
|
return keyword
|
||||||
}
|
}
|
||||||
|
|
||||||
// matches: rgba(<r>, <g>, <b>, var(--bg-opacity))
|
// matches: rgba(<r>, <g>, <b>, var(--bg-opacity))
|
||||||
// TODO: support other formats? e.g. hsla, css level 4
|
// TODO: support other formats? e.g. hsla, css level 4
|
||||||
const match = str.match(
|
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) {
|
if (match) {
|
||||||
|
@ -128,3 +190,7 @@ function createColor(str: string): TinyColor | string {
|
||||||
|
|
||||||
return new TinyColor(str)
|
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'
|
} from './lexers'
|
||||||
import { getLanguageBoundaries } from './getLanguageBoundaries'
|
import { getLanguageBoundaries } from './getLanguageBoundaries'
|
||||||
import { resolveRange } from './resolveRange'
|
import { resolveRange } from './resolveRange'
|
||||||
import { getDocumentSettings } from './getDocumentSettings'
|
|
||||||
const dlv = require('dlv')
|
const dlv = require('dlv')
|
||||||
import { createMultiRegexp } from './createMultiRegexp'
|
import { createMultiRegexp } from './createMultiRegexp'
|
||||||
|
|
||||||
|
@ -146,7 +145,7 @@ async function findCustomClassLists(
|
||||||
doc: TextDocument,
|
doc: TextDocument,
|
||||||
range?: Range
|
range?: Range
|
||||||
): Promise<DocumentClassList[]> {
|
): Promise<DocumentClassList[]> {
|
||||||
const settings = await getDocumentSettings(state, doc)
|
const settings = await state.editor.getConfiguration(doc.uri)
|
||||||
const regexes = dlv(settings, 'experimental.classRegex', [])
|
const regexes = dlv(settings, 'experimental.classRegex', [])
|
||||||
|
|
||||||
if (!Array.isArray(regexes) || regexes.length === 0) return []
|
if (!Array.isArray(regexes) || regexes.length === 0) return []
|
||||||
|
|
|
@ -21,6 +21,10 @@ export function getClassNameMeta(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info === undefined) {
|
||||||
|
console.log({ classNameOrParts })
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: info.__source,
|
source: info.__source,
|
||||||
pseudo: info.__pseudo,
|
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(
|
export function remToPx(value: string, rootSize: number = 16): string | undefined {
|
||||||
value: string,
|
|
||||||
rootSize: number = 16
|
|
||||||
): string | undefined {
|
|
||||||
if (/^-?[0-9.]+rem$/.test(value)) {
|
if (/^-?[0-9.]+rem$/.test(value)) {
|
||||||
let number = parseFloat(value.substr(0, value.length - 3))
|
let number = parseFloat(value.substr(0, value.length - 3))
|
||||||
if (!isNaN(number)) {
|
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 { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
|
import type { Postcss } from 'postcss'
|
||||||
|
|
||||||
export type ClassNamesTree = {
|
export type ClassNamesTree = {
|
||||||
[key: string]: ClassNamesTree
|
[key: string]: ClassNamesTree
|
||||||
|
@ -17,13 +18,14 @@ export type ClassNames = {
|
||||||
export type EditorState = {
|
export type EditorState = {
|
||||||
connection: Connection
|
connection: Connection
|
||||||
documents: TextDocuments<TextDocument>
|
documents: TextDocuments<TextDocument>
|
||||||
documentSettings: Map<string, Settings>
|
|
||||||
globalSettings: Settings
|
globalSettings: Settings
|
||||||
userLanguages: Record<string, string>
|
userLanguages: Record<string, string>
|
||||||
capabilities: {
|
capabilities: {
|
||||||
configuration: boolean
|
configuration: boolean
|
||||||
diagnosticRelatedInformation: boolean
|
diagnosticRelatedInformation: boolean
|
||||||
}
|
}
|
||||||
|
getConfiguration: (uri?: string) => Promise<Settings>
|
||||||
|
getDocumentSymbols: (uri: string) => Promise<SymbolInformation[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiagnosticSeveritySetting = 'ignore' | 'warning' | 'error'
|
type DiagnosticSeveritySetting = 'ignore' | 'warning' | 'error'
|
||||||
|
@ -35,6 +37,7 @@ export type Settings = {
|
||||||
validate: boolean
|
validate: boolean
|
||||||
showPixelEquivalents: boolean
|
showPixelEquivalents: boolean
|
||||||
rootFontSize: number
|
rootFontSize: number
|
||||||
|
colorDecorators: 'inherit' | 'on' | 'off'
|
||||||
lint: {
|
lint: {
|
||||||
cssConflict: DiagnosticSeveritySetting
|
cssConflict: DiagnosticSeveritySetting
|
||||||
invalidApply: DiagnosticSeveritySetting
|
invalidApply: DiagnosticSeveritySetting
|
||||||
|
@ -42,36 +45,41 @@ export type Settings = {
|
||||||
invalidVariant: DiagnosticSeveritySetting
|
invalidVariant: DiagnosticSeveritySetting
|
||||||
invalidConfigPath: DiagnosticSeveritySetting
|
invalidConfigPath: DiagnosticSeveritySetting
|
||||||
invalidTailwindDirective: DiagnosticSeveritySetting
|
invalidTailwindDirective: DiagnosticSeveritySetting
|
||||||
|
incorrectVariantOrder: DiagnosticSeveritySetting
|
||||||
}
|
}
|
||||||
experimental: {
|
experimental: {
|
||||||
classRegex: string[]
|
classRegex: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationEmitter {
|
export interface FeatureFlags {
|
||||||
on: (name: string, handler: (args: any) => void) => void
|
future: string[]
|
||||||
off: (name: string, handler: (args: any) => void) => void
|
experimental: string[]
|
||||||
emit: (name: string, args: any) => Promise<any>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = null | {
|
export interface State {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
emitter?: NotificationEmitter
|
|
||||||
version?: string
|
|
||||||
configPath?: string
|
configPath?: string
|
||||||
config?: any
|
config?: any
|
||||||
modules?: {
|
version?: string
|
||||||
tailwindcss: any
|
|
||||||
postcss: any
|
|
||||||
}
|
|
||||||
separator?: string
|
separator?: string
|
||||||
plugins?: any[]
|
|
||||||
variants?: string[]
|
|
||||||
classNames?: ClassNames
|
|
||||||
dependencies?: string[]
|
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
|
editor?: EditorState
|
||||||
error?: Error
|
jit?: boolean
|
||||||
|
jitContext?: any
|
||||||
|
// postcssPlugins?: { before: any[]; after: any[] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentClassList = {
|
export type DocumentClassList = {
|
||||||
|
|
|
@ -7,13 +7,14 @@ export function validateApply(
|
||||||
state: State,
|
state: State,
|
||||||
classNameOrParts: string | string[]
|
classNameOrParts: string | string[]
|
||||||
): { isApplyable: true } | { isApplyable: false; reason: string } | null {
|
): { isApplyable: true } | { isApplyable: false; reason: string } | null {
|
||||||
|
if (state.jit) {
|
||||||
|
return { isApplyable: true }
|
||||||
|
}
|
||||||
|
|
||||||
const meta = getClassNameMeta(state, classNameOrParts)
|
const meta = getClassNameMeta(state, classNameOrParts)
|
||||||
if (!meta) return null
|
if (!meta) return null
|
||||||
|
|
||||||
if (
|
if (semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')) {
|
||||||
semver.gte(state.version, '2.0.0-alpha.1') ||
|
|
||||||
flagEnabled(state, 'applyComplexClasses')
|
|
||||||
) {
|
|
||||||
return { isApplyable: true }
|
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 selectorParser from 'postcss-selector-parser'
|
||||||
import dset from 'dset'
|
import dset from 'dset'
|
||||||
import dlv from 'dlv'
|
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) {
|
function createSelectorFromNodes(nodes) {
|
||||||
if (nodes.length === 0) return null
|
if (nodes.length === 0) return null
|
||||||
const selector = selectorParser.selector()
|
const selector = selectorParser.selector({ value: '' })
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
selector.append(nodes[i])
|
selector.append(nodes[i])
|
||||||
}
|
}
|
||||||
return String(selector).trim()
|
return String(selector).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClassNamesFromSelector(selector) {
|
function getClassNamesFromSelector(selector: string) {
|
||||||
const classNames = []
|
const classNames = []
|
||||||
const { nodes: subSelectors } = selectorParser().astSync(selector)
|
const { nodes: subSelectors } = selectorParser().astSync(selector)
|
||||||
|
|
||||||
for (let i = 0; i < subSelectors.length; i++) {
|
for (let i = 0; i < subSelectors.length; i++) {
|
||||||
|
let subSelector = subSelectors[i]
|
||||||
|
if (subSelector.type !== 'selector') continue
|
||||||
|
|
||||||
let scope = []
|
let scope = []
|
||||||
for (let j = 0; j < subSelectors[i].nodes.length; j++) {
|
for (let j = 0; j < subSelector.nodes.length; j++) {
|
||||||
let node = subSelectors[i].nodes[j]
|
let node = subSelector.nodes[j]
|
||||||
let pseudo = []
|
let pseudo = []
|
||||||
|
|
||||||
if (node.type === 'class') {
|
if (node.type === 'class') {
|
||||||
let next = subSelectors[i].nodes[j + 1]
|
let next = subSelector.nodes[j + 1]
|
||||||
|
|
||||||
while (next && next.type === 'pseudo') {
|
while (next && next.type === 'pseudo') {
|
||||||
pseudo.push(next)
|
pseudo.push(next)
|
||||||
j++
|
j++
|
||||||
next = subSelectors[i].nodes[j + 1]
|
next = subSelector.nodes[j + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
classNames.push({
|
classNames.push({
|
||||||
className: node.value.trim(),
|
className: node.value.trim(),
|
||||||
scope: createSelectorFromNodes(scope),
|
scope: createSelectorFromNodes(scope),
|
||||||
__rule: j === subSelectors[i].nodes.length - 1,
|
__rule: j === subSelector.nodes.length - 1,
|
||||||
__pseudo: pseudo.map(String),
|
__pseudo: pseudo.map(String),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,7 +52,7 @@ function getClassNamesFromSelector(selector) {
|
||||||
return classNames
|
return classNames
|
||||||
}
|
}
|
||||||
|
|
||||||
async function process(root) {
|
async function process(root: Root) {
|
||||||
const tree = {}
|
const tree = {}
|
||||||
const commonContext = {}
|
const commonContext = {}
|
||||||
|
|
||||||
|
@ -68,9 +76,7 @@ async function process(root) {
|
||||||
rule.walkDecls((decl) => {
|
rule.walkDecls((decl) => {
|
||||||
if (decls[decl.prop]) {
|
if (decls[decl.prop]) {
|
||||||
decls[decl.prop] = [
|
decls[decl.prop] = [
|
||||||
...(Array.isArray(decls[decl.prop])
|
...(Array.isArray(decls[decl.prop]) ? decls[decl.prop] : [decls[decl.prop]]),
|
||||||
? decls[decl.prop]
|
|
||||||
: [decls[decl.prop]]),
|
|
||||||
decl.value,
|
decl.value,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
|
@ -78,11 +84,11 @@ async function process(root) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let p = rule
|
let p: Container = rule
|
||||||
const keys = []
|
const keys = []
|
||||||
while (p.parent.type !== 'root') {
|
while (p.parent.type !== 'root') {
|
||||||
p = p.parent
|
p = p.parent
|
||||||
if (p.type === 'atrule') {
|
if (isAtRule(p)) {
|
||||||
keys.push(`@${p.name} ${p.params}`)
|
keys.push(`@${p.name} ${p.params}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,25 +110,13 @@ async function process(root) {
|
||||||
}
|
}
|
||||||
if (classNames[i].__rule) {
|
if (classNames[i].__rule) {
|
||||||
dset(tree, [...baseKeys, '__info', ...index, '__rule'], true)
|
dset(tree, [...baseKeys, '__info', ...index, '__rule'], true)
|
||||||
dset(tree, [...baseKeys, '__info', ...index, '__source'], layer)
|
|
||||||
|
|
||||||
dsetEach(tree, [...baseKeys, '__info', ...index], decls)
|
dsetEach(tree, [...baseKeys, '__info', ...index], decls)
|
||||||
}
|
}
|
||||||
dset(
|
dset(tree, [...baseKeys, '__info', ...index, '__source'], layer)
|
||||||
tree,
|
dset(tree, [...baseKeys, '__info', ...index, '__pseudo'], classNames[i].__pseudo)
|
||||||
[...baseKeys, '__info', ...index, '__pseudo'],
|
dset(tree, [...baseKeys, '__info', ...index, '__scope'], classNames[i].scope)
|
||||||
classNames[i].__pseudo
|
dset(tree, [...baseKeys, '__info', ...index, '__context'], context.concat([]).reverse())
|
||||||
)
|
|
||||||
dset(
|
|
||||||
tree,
|
|
||||||
[...baseKeys, '__info', ...index, '__scope'],
|
|
||||||
classNames[i].scope
|
|
||||||
)
|
|
||||||
dset(
|
|
||||||
tree,
|
|
||||||
[...baseKeys, '__info', ...index, '__context'],
|
|
||||||
context.concat([]).reverse()
|
|
||||||
)
|
|
||||||
|
|
||||||
// common context
|
// common context
|
||||||
context.push(...classNames[i].__pseudo.map((x) => `&${x}`))
|
context.push(...classNames[i].__pseudo.map((x) => `&${x}`))
|
||||||
|
@ -131,10 +125,7 @@ async function process(root) {
|
||||||
if (typeof commonContext[contextKeys[i]] === 'undefined') {
|
if (typeof commonContext[contextKeys[i]] === 'undefined') {
|
||||||
commonContext[contextKeys[i]] = context
|
commonContext[contextKeys[i]] = context
|
||||||
} else {
|
} else {
|
||||||
commonContext[contextKeys[i]] = intersection(
|
commonContext[contextKeys[i]] = intersection(commonContext[contextKeys[i]], context)
|
||||||
commonContext[contextKeys[i]],
|
|
||||||
context
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,26 +134,15 @@ async function process(root) {
|
||||||
return { classNames: tree, context: commonContext }
|
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)
|
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)
|
const k = Object.keys(values)
|
||||||
for (let i = 0; i < k.length; i++) {
|
for (let i = 0; i < k.length; i++) {
|
||||||
dset(obj, [...keys, k[i]], values[k[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
|
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 { TextDocument } from 'vscode-languageserver/node'
|
||||||
import { State } from '../../util/state'
|
import { State } from 'tailwindcss-language-service/src/util/state'
|
||||||
import { doValidate } from 'tailwindcss-language-service'
|
import { doValidate } from 'tailwindcss-language-service/src/diagnostics/diagnosticsProvider'
|
||||||
|
|
||||||
export async function provideDiagnostics(state: State, document: TextDocument) {
|
export async function provideDiagnostics(state: State, document: TextDocument) {
|
||||||
state.editor.connection.sendDiagnostics({
|
state.editor?.connection.sendDiagnostics({
|
||||||
uri: document.uri,
|
uri: document.uri,
|
||||||
diagnostics: await doValidate(state, document),
|
diagnostics: await doValidate(state, document),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearDiagnostics(state: State, document: TextDocument): void {
|
export function clearDiagnostics(state: State, document: TextDocument): void {
|
||||||
state.editor.connection.sendDiagnostics({
|
state.editor?.connection.sendDiagnostics({
|
||||||
uri: document.uri,
|
uri: document.uri,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAllDiagnostics(state: State): void {
|
export function clearAllDiagnostics(state: State): void {
|
||||||
state.editor.documents.all().forEach((document) => {
|
state.editor?.documents.all().forEach((document) => {
|
||||||
clearDiagnostics(state, document)
|
clearDiagnostics(state, document)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAllDiagnostics(state: State): void {
|
export function updateAllDiagnostics(state: State): void {
|
||||||
state.editor.documents.all().forEach((document) => {
|
state.editor?.documents.all().forEach((document) => {
|
||||||
provideDiagnostics(state, 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"]
|
||||||
|
}
|