add jit support, refactor for general reliability

master
Brad Cornes 2021-05-03 18:00:04 +01:00
parent 8fce24db9c
commit 99297add4e
77 changed files with 16310 additions and 42286 deletions

View File

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 256 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 235 KiB

View File

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 267 KiB

2
.gitignore vendored
View File

@ -1 +1,3 @@
node_modules
dist
*.vsix

View File

@ -1,11 +1,10 @@
node_modules/**
.vscode/**
.github/**
src/**
packages/**
tests/**
**/*.ts
**/*.map
.gitignore
**/tsconfig.json
**/tsconfig.base.json
contributing.md
node_modules/**
src/**
tests/**
.github/**

View File

@ -1 +0,0 @@
packages/tailwindcss-intellisense/README.md

130
README.md 100644
View File

@ -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 youre 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 arent causing your Tailwind config file to be excluded from search, for example via the `search.exclude` setting.

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

16321
package-lock.json generated

File diff suppressed because it is too large Load Diff

239
package.json 100644 → 100755
View File

@ -1,10 +1,241 @@
{
"name": "vscode-tailwindcss",
"private": true,
"workspaces": [
"packages/*"
"displayName": "Tailwind CSS IntelliSense",
"description": "Intelligent Tailwind CSS tooling for VS Code",
"preview": true,
"author": "Brad Cornes <hello@bradley.dev>",
"license": "MIT",
"version": "0.5.9",
"homepage": "https://github.com/tailwindlabs/tailwindcss-intellisense",
"bugs": {
"url": "https://github.com/tailwindlabs/tailwindcss-intellisense/issues",
"email": "hello@bradley.dev"
},
"repository": {
"type": "git",
"url": "https://github.com/tailwindlabs/tailwindcss-intellisense.git"
},
"publisher": "bradlc",
"keywords": [
"tailwind",
"tailwindcss",
"css",
"intellisense",
"autocomplete",
"vscode"
],
"engines": {
"vscode": "^1.33.0"
},
"categories": [
"Linters",
"Other"
],
"galleryBanner": {
"color": "#f9fafb"
},
"icon": "media/icon.png",
"activationEvents": [
"onStartupFinished"
],
"main": "dist/extension/index.js",
"contributes": {
"commands": [
{
"command": "tailwindCSS.showOutput",
"title": "Tailwind CSS: Show Output"
}
],
"grammars": [
{
"scopeName": "tailwindcss.injection",
"path": "./syntaxes/tailwind.tmLanguage.json",
"injectTo": [
"source.css",
"source.css.scss",
"source.css.less",
"source.css.postcss",
"source.vue",
"source.svelte",
"text.html"
]
}
],
"configuration": {
"title": "Tailwind CSS IntelliSense",
"properties": {
"tailwindCSS.emmetCompletions": {
"type": "boolean",
"default": false,
"markdownDescription": "Enable class name completions when using Emmet-style syntax, for example `div.bg-red-500.uppercase`"
},
"tailwindCSS.includeLanguages": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"default": {},
"markdownDescription": "Enable features in languages that are not supported by default. Add a mapping here between the new language and an already supported language.\n E.g.: `{\"plaintext\": \"html\"}`"
},
"tailwindCSS.colorDecorators": {
"type": "string",
"enum": [
"inherit",
"on",
"off"
],
"markdownEnumDescriptions": [
"Color decorators are rendered if `editor.colorDecorators` is enabled.",
"Color decorators are rendered.",
"Color decorators are not rendered."
],
"default": "inherit",
"markdownDescription": "Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.",
"scope": "language-overridable"
},
"tailwindCSS.validate": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable linting. Rules can be configured individually using the `tailwindcss.lint.*` settings",
"scope": "language-overridable"
},
"tailwindCSS.lint.cssConflict": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"markdownDescription": "Class names on the same HTML element which apply the same CSS property or properties",
"scope": "language-overridable"
},
"tailwindCSS.lint.invalidApply": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"markdownDescription": "Unsupported use of the [`@apply` directive](https://tailwindcss.com/docs/functions-and-directives/#apply)",
"scope": "language-overridable"
},
"tailwindCSS.lint.invalidScreen": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"markdownDescription": "Unknown screen name used with the [`@screen` directive](https://tailwindcss.com/docs/functions-and-directives/#screen)",
"scope": "language-overridable"
},
"tailwindCSS.lint.invalidVariant": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"markdownDescription": "Unknown variant name used with the [`@variants` directive](https://tailwindcss.com/docs/functions-and-directives/#variants)",
"scope": "language-overridable"
},
"tailwindCSS.lint.invalidConfigPath": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"markdownDescription": "Unknown or invalid path used with the [`theme` helper](https://tailwindcss.com/docs/functions-and-directives/#theme)",
"scope": "language-overridable"
},
"tailwindCSS.lint.invalidTailwindDirective": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"markdownDescription": "Unknown value used with the [`@tailwind` directive](https://tailwindcss.com/docs/functions-and-directives/#tailwind)",
"scope": "language-overridable"
},
"tailwindCSS.experimental.classRegex": {
"type": "array",
"scope": "language-overridable"
},
"tailwindCSS.showPixelEquivalents": {
"type": "boolean",
"default": true,
"markdownDescription": "Show `px` equivalents for `rem` CSS values."
},
"tailwindCSS.rootFontSize": {
"type": "number",
"default": 16,
"markdownDescription": "Root font size in pixels. Used to convert `rem` CSS values to their `px` equivalents. See `#tailwindCSS.showPixelEquivalents#`."
}
}
}
},
"scripts": {
"dev": "glob-exec --foreach --parallel \"src/*.ts\" -- \"ncc build {{file}} --watch -o dist/{{file.toString().replace(/^src\\//, '').replace(/\\.ts$/, '')}}\"",
"build": "glob-exec --foreach --parallel \"src/*.ts\" -- \"ncc build {{file}} -o dist/{{file.toString().replace(/^src\\//, '').replace(/\\.ts$/, '')}}\"",
"minify": "glob-exec --foreach --parallel \"dist/**/*.js\" -- \"terser {{file}} --compress --mangle --output {{file.toString()}}\"",
"package": "vsce package",
"publish": "vsce publish",
"vscode:prepublish": "npm run clean && npm run build && npm run minify",
"clean": "rimraf dist",
"test": "jest"
},
"dependencies": {
"@ctrl/tinycolor": "3.1.4",
"@types/debounce": "1.2.0",
"@types/mocha": "5.2.0",
"@types/node": "14.14.34",
"@types/vscode": "1.54.0",
"@vercel/ncc": "0.28.4",
"builtin-modules": "3.2.0",
"chokidar": "3.5.1",
"debounce": "1.2.0",
"dlv": "1.1.3",
"dset": "2.0.1",
"enhanced-resolve": "5.8.0",
"fast-glob": "3.2.4",
"find-up": "5.0.0",
"glob-exec": "0.1.1",
"jest": "25.5.4",
"klona": "2.0.4",
"normalize-path": "3.0.0",
"pkg-up": "3.1.0",
"postcss": "8.2.6",
"postcss-load-config": "3.0.1",
"postcss-selector-parser": "6.0.2",
"prettier": "^2.2.1",
"rimraf": "3.0.2",
"semver": "7.3.2",
"stack-trace": "0.0.10",
"tailwindcss": "2.0.3",
"terser": "4.6.12",
"typescript": "4.2.4",
"vsce": "1.87.0",
"vscode-languageclient": "7.0.0",
"vscode-languageserver": "7.0.0",
"vscode-languageserver-textdocument": "1.0.1",
"vscode-uri": "3.0.2"
},
"devDependencies": {
"prettier": "^2.2.1"
"@types/moo": "0.5.3",
"css.escape": "1.5.1",
"detect-indent": "6.0.0",
"line-column": "1.0.2",
"moo": "0.5.1",
"multi-regexp2": "1.0.3",
"sift-string": "0.0.2",
"vscode-emmet-helper-bundled": "0.0.1"
}
}

View File

@ -1,3 +0,0 @@
node_modules
dist
*.vsix

View File

@ -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 youre 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 arent causing your Tailwind config file to be excluded from search, for example via the `search.exclude` setting.

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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

View File

@ -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)
},
})
}

View File

@ -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,
}
})
}

View File

@ -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 {}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 })
}
)
}

View File

@ -1,3 +0,0 @@
export function isObject(thing) {
return Object.prototype.toString.call(thing) === '[object Object]'
}

View File

@ -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)
})
}

View File

@ -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 (_) {}
}

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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)
})
}

View File

@ -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)
),
})
}
})
}

View File

@ -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)),
})
})
}

View File

@ -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) }
}
)
}

View File

@ -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()

View File

@ -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
}

View File

@ -1 +0,0 @@
export * from 'tailwindcss-language-service'

View File

@ -1 +0,0 @@
import './lsp/server'

View File

@ -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())
)
}

View File

@ -1,12 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"rootDir": "../",
"sourceMap": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowJs": true
},
"include": ["src", "../tailwindcss-language-service"]
}

File diff suppressed because it is too large Load Diff

View File

@ -14,23 +14,27 @@
"lint": "tsdx lint"
},
"dependencies": {
"@ctrl/tinycolor": "^3.1.4",
"@types/moo": "^0.5.3",
"css.escape": "^1.5.1",
"detect-indent": "^6.0.0",
"dlv": "^1.1.3",
"line-column": "^1.0.2",
"mitt": "^2.1.0",
"moo": "^0.5.1",
"multi-regexp2": "^1.0.3",
"semver": "^7.3.2",
"sift-string": "^0.0.2",
"tsdx": "^0.13.3",
"tslib": "^2.0.1",
"typescript": "^4.0.2",
"vscode-emmet-helper-bundled": "^0.0.1",
"vscode-languageclient": "^6.1.3",
"vscode-languageserver": "^6.1.1",
"vscode-languageserver-textdocument": "^1.0.1"
"@ctrl/tinycolor": "3.1.4",
"@types/moo": "0.5.3",
"css.escape": "1.5.1",
"detect-indent": "6.0.0",
"dlv": "1.1.3",
"dset": "2.0.1",
"line-column": "1.0.2",
"moo": "0.5.1",
"multi-regexp2": "1.0.3",
"postcss-selector-parser": "6.0.2",
"semver": "7.3.2",
"sift-string": "0.0.2",
"vscode-emmet-helper-bundled": "0.0.1",
"vscode-languageclient": "7.0.0",
"vscode-languageserver": "7.0.0",
"vscode-languageserver-textdocument": "1.0.1"
},
"devDependencies": {
"postcss": "8.2.6",
"tsdx": "0.14.1",
"tslib": "2.2.0",
"typescript": "4.2.4"
}
}

View File

@ -11,6 +11,7 @@ import {
isInvalidTailwindDirectiveDiagnostic,
isInvalidScreenDiagnostic,
isInvalidVariantDiagnostic,
isIncorrectVariantOrderDiagnostic,
} from '../diagnostics/types'
import { flatten, dedupeBy } from '../util/array'
import { provideCssConflictCodeActions } from './provideCssConflictCodeActions'
@ -38,10 +39,7 @@ async function getDiagnosticsFromCodeActionParams(
.filter(Boolean)
}
export async function doCodeActions(
state: State,
params: CodeActionParams
): Promise<CodeAction[]> {
export async function doCodeActions(state: State, params: CodeActionParams): Promise<CodeAction[]> {
let diagnostics = await getDiagnosticsFromCodeActionParams(
state,
params,
@ -64,7 +62,8 @@ export async function doCodeActions(
isInvalidConfigPathDiagnostic(diagnostic) ||
isInvalidTailwindDirectiveDiagnostic(diagnostic) ||
isInvalidScreenDiagnostic(diagnostic) ||
isInvalidVariantDiagnostic(diagnostic)
isInvalidVariantDiagnostic(diagnostic) ||
isIncorrectVariantOrderDiagnostic(diagnostic)
) {
return provideSuggestionCodeActions(state, params, diagnostic)
}

View File

@ -1,9 +1,4 @@
import type {
CodeAction,
CodeActionParams,
TextEdit,
Range,
} from 'vscode-languageserver'
import type { CodeAction, CodeActionParams, TextEdit, Range } from 'vscode-languageserver'
import { State } from '../util/state'
import { InvalidApplyDiagnostic } from '../diagnostics/types'
import { isCssDoc } from '../util/css'
@ -13,7 +8,7 @@ import { getClassNameParts } from '../util/getClassNameAtPosition'
import { validateApply } from '../util/validateApply'
import { isWithinRange } from '../util/isWithinRange'
const dlv = require('dlv')
import type { Root, NodeSource } from 'postcss'
import type { Root, Source } from 'postcss'
import { absoluteRange } from '../util/absoluteRange'
import { removeRangesFromString } from '../util/removeRangesFromString'
import detectIndent from 'detect-indent'
@ -35,9 +30,7 @@ export async function provideInvalidApplyCodeActions(
const { postcss } = state.modules
let changes: TextEdit[] = []
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
/\s+/
).length
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(/\s+/).length
let className = diagnostic.className.className
let classNameParts = getClassNameParts(state, className)
@ -50,16 +43,17 @@ export async function provideInvalidApplyCodeActions(
if (!isCssDoc(state, document)) {
let languageBoundaries = getLanguageBoundaries(state, document)
if (!languageBoundaries) return []
cssRange = languageBoundaries.css.find((range) =>
isWithinRange(diagnostic.range.start, range)
)
cssRange = languageBoundaries.css.find((range) => isWithinRange(diagnostic.range.start, range))
if (!cssRange) return []
cssText = document.getText(cssRange)
}
try {
await postcss([
postcss.plugin('', (_options = {}) => {
await postcss
.module([
// TODO: use plain function?
// @ts-ignore
postcss.module.plugin('', (_options = {}) => {
return (root: Root) => {
root.walkRules((rule) => {
if (changes.length) return false
@ -70,8 +64,7 @@ export async function provideInvalidApplyCodeActions(
atRuleRange = absoluteRange(atRuleRange, cssRange)
}
if (!isWithinRange(diagnostic.range.start, atRuleRange))
return true
if (!isWithinRange(diagnostic.range.start, atRuleRange)) return undefined // true
let ast = classNameToAst(
state,
@ -118,10 +111,7 @@ export async function provideInvalidApplyCodeActions(
.replace(/([^\s^]){$/gm, '$1 {')
.replace(/^\s+/gm, (m: string) => {
if (typeof outputIndent === 'undefined') outputIndent = m
return m.replace(
new RegExp(outputIndent, 'g'),
documentIndent.indent
)
return m.replace(new RegExp(outputIndent, 'g'), documentIndent.indent)
})
.replace(/^(\s+)(.*?[^{}]\n)([^\s}])/gm, '$1$2$1$3'),
})
@ -129,11 +119,12 @@ export async function provideInvalidApplyCodeActions(
return false
})
return true
return undefined // true
})
}
}),
]).process(cssText, { from: undefined })
])
.process(cssText, { from: undefined })
} catch (_) {
return []
}
@ -156,7 +147,7 @@ export async function provideInvalidApplyCodeActions(
]
}
function postcssSourceToRange(source: NodeSource): Range {
function postcssSourceToRange(source: Source): Range {
return {
start: {
line: source.start.line - 1,
@ -177,10 +168,7 @@ function classNameToAst(
) {
const baseClassName = classNameParts[classNameParts.length - 1]
const validatedBaseClassName = validateApply(state, [baseClassName])
if (
validatedBaseClassName === null ||
validatedBaseClassName.isApplyable === false
) {
if (validatedBaseClassName === null || validatedBaseClassName.isApplyable === false) {
return null
}
const meta = getClassNameMeta(state, classNameParts)
@ -188,11 +176,7 @@ function classNameToAst(
let context = meta.context
let pseudo = meta.pseudo
const globalContexts = state.classNames.context
let screens = dlv(
state.config,
'theme.screens',
dlv(state.config, 'screens', {})
)
let screens = dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
if (!isObject(screens)) screens = {}
screens = Object.keys(screens)
const path = []
@ -231,10 +215,7 @@ function classNameToAst(
return cssObjToAst(obj, state.modules.postcss)
}
function appendPseudosToSelector(
selector: string,
pseudos: string[]
): string | null {
function appendPseudosToSelector(selector: string, pseudos: string[]): string | null {
if (pseudos.length === 0) return selector
let canTransform = true

View File

@ -1,13 +1,11 @@
import { State } from '../util/state'
import type {
CodeActionParams,
CodeAction,
} from 'vscode-languageserver'
import type { CodeActionParams, CodeAction } from 'vscode-languageserver'
import {
InvalidConfigPathDiagnostic,
InvalidTailwindDirectiveDiagnostic,
InvalidScreenDiagnostic,
InvalidVariantDiagnostic,
IncorrectVariantOrderDiagnostic,
} from '../diagnostics/types'
export function provideSuggestionCodeActions(
@ -18,6 +16,7 @@ export function provideSuggestionCodeActions(
| InvalidTailwindDirectiveDiagnostic
| InvalidScreenDiagnostic
| InvalidVariantDiagnostic
| IncorrectVariantOrderDiagnostic
): CodeAction[] {
return diagnostic.suggestions.map((suggestion) => ({
title: `Replace with '${suggestion}'`,

View File

@ -19,26 +19,31 @@ import { stringifyScreen, Screen } from './util/screens'
import isObject from './util/isObject'
import * as emmetHelper from 'vscode-emmet-helper-bundled'
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
import { getDocumentSettings } from './util/getDocumentSettings'
import { isJsContext } from './util/js'
import { naturalExpand } from './util/naturalExpand'
import semver from 'semver'
import { docsUrl } from './util/docsUrl'
import { ensureArray } from './util/array'
import {
getClassAttributeLexer,
getComputedClassAttributeLexer,
} from './util/lexers'
import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers'
import { validateApply } from './util/validateApply'
import { flagEnabled } from './util/flagEnabled'
import { remToPx } from './util/remToPx'
import { createMultiRegexp } from './util/createMultiRegexp'
import * as jit from './util/jit'
import { TinyColor } from '@ctrl/tinycolor'
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
let isUtil = (className) =>
Array.isArray(className.__info)
? className.__info.some((x) => x.__source === 'utilities')
: className.__info.__source === 'utilities'
export function completionsFromClassList(
state: State,
classList: string,
classListRange: Range,
filter?: (item: CompletionItem) => boolean
filter?: (item: CompletionItem) => boolean,
document?: TextDocument
): CompletionList {
let classNames = classList.split(/[\s+]/)
const partialClassName = classNames[classNames.length - 1]
@ -56,23 +61,126 @@ export function completionsFromClassList(
},
}
if (state.jit) {
let allVariants = Object.keys(state.variants)
let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName)
replacementRange.start.character += offset
let important = partialClassName.substr(offset).startsWith('!')
if (important) {
replacementRange.start.character += 1
}
let items: CompletionItem[] = []
if (!important) {
items.push(
...Object.entries(state.variants)
.filter(([variant]) => !existingVariants.includes(variant))
.map(([variant, definition], index) => {
let resultingVariants = [...existingVariants, variant].sort(
(a, b) => allVariants.indexOf(b) - allVariants.indexOf(a)
)
return {
label: variant + sep,
kind: 9,
detail: definition,
data: 'variant',
command: {
title: '',
command: 'editor.action.triggerSuggest',
},
sortText: '-' + naturalExpand(index),
textEdit: {
newText: resultingVariants[resultingVariants.length - 1] + sep,
range: replacementRange,
},
additionalTextEdits:
resultingVariants.length > 1
? [
{
newText:
resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep,
range: {
start: {
...classListRange.start,
character: classListRange.end.character - partialClassName.length,
},
end: {
...replacementRange.start,
character: replacementRange.start.character,
},
},
},
]
: [],
} as CompletionItem
})
)
}
return {
isIncomplete: false,
items: items
.concat(
Object.keys(state.classNames.classNames)
.filter((className) => {
let item = state.classNames.classNames[className]
if (existingVariants.length === 0) {
return item.__info
}
return item.__info && isUtil(item)
})
.map((className, index) => {
let kind: CompletionItemKind = 21
let documentation: string = null
const color = getColor(state, className)
if (color !== null) {
kind = 16
if (typeof color !== 'string' && color.a !== 0) {
documentation = color.toRgbString()
}
}
return {
label: className,
kind,
documentation,
sortText: naturalExpand(index),
data: [...existingVariants, important ? `!${className}` : className],
textEdit: {
newText: className,
range: replacementRange,
},
} as CompletionItem
})
)
.filter((item) => {
if (item === null) {
return false
}
if (filter && !filter(item)) {
return false
}
return true
}),
}
}
for (let i = parts.length - 1; i > 0; i--) {
let keys = parts.slice(0, i).filter(Boolean)
subset = dlv(state.classNames.classNames, keys)
if (
typeof subset !== 'undefined' &&
typeof dlv(subset, ['__info', '__rule']) === 'undefined'
) {
if (typeof subset !== 'undefined' && typeof dlv(subset, ['__info', '__rule']) === 'undefined') {
isSubset = true
subsetKey = keys
replacementRange = {
...replacementRange,
start: {
...replacementRange.start,
character:
replacementRange.start.character +
keys.join(sep).length +
sep.length,
character: replacementRange.start.character + keys.join(sep).length + sep.length,
},
}
break
@ -106,17 +214,13 @@ export function completionsFromClassList(
.concat(
Object.keys(isSubset ? subset : state.classNames.classNames)
.filter((className) =>
dlv(state.classNames.classNames, [
...subsetKey,
className,
'__info',
])
dlv(state.classNames.classNames, [...subsetKey, className, '__info'])
)
.map((className, index) => {
let kind: CompletionItemKind = 21
let documentation: string = null
const color = getColor(state, [className])
const color = getColor(state, className)
if (color !== null) {
kind = 16
if (typeof color !== 'string' && color.a !== 0) {
@ -159,10 +263,7 @@ function provideClassAttributeCompletions(
end: position,
})
const match = findLast(
/(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi,
str
)
const match = findLast(/(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi, str)
if (match === null) {
return null
@ -187,13 +288,19 @@ function provideClassAttributeCompletions(
}
}
return completionsFromClassList(state, classList, {
return completionsFromClassList(
state,
classList,
{
start: {
line: position.line,
character: position.character - classList.length,
},
end: position,
})
},
undefined,
document
)
}
} catch (_) {}
@ -205,7 +312,7 @@ async function provideCustomClassNameCompletions(
document: TextDocument,
position: Position
): Promise<CompletionList> {
const settings = await getDocumentSettings(state, document)
const settings = await state.editor.getConfiguration(document.uri)
const regexes = dlv(settings, 'experimental.classRegex', [])
if (regexes.length === 0) return null
@ -220,9 +327,7 @@ async function provideCustomClassNameCompletions(
for (let i = 0; i < regexes.length; i++) {
try {
let [containerRegex, classRegex] = Array.isArray(regexes[i])
? regexes[i]
: [regexes[i]]
let [containerRegex, classRegex] = Array.isArray(regexes[i]) ? regexes[i] : [regexes[i]]
containerRegex = createMultiRegexp(containerRegex)
let containerMatch
@ -239,9 +344,7 @@ async function provideCustomClassNameCompletions(
classRegex = createMultiRegexp(classRegex)
let classMatch
while (
(classMatch = classRegex.exec(containerMatch.match)) !== null
) {
while ((classMatch = classRegex.exec(containerMatch.match)) !== null) {
const classMatchStart = matchStart + classMatch.start
const classMatchEnd = matchStart + classMatch.end
if (cursor >= classMatchStart && cursor <= classMatchEnd) {
@ -302,8 +405,7 @@ function provideAtApplyCompletions(
(item) => {
if (item.kind === 9) {
return (
semver.gte(state.version, '2.0.0-alpha.1') ||
flagEnabled(state, 'applyComplexClasses')
semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')
)
}
let validated = validateApply(state, item.data)
@ -317,10 +419,7 @@ function provideClassNameCompletions(
document: TextDocument,
position: Position
): CompletionList {
if (
isHtmlContext(state, document, position) ||
isJsContext(state, document, position)
) {
if (isHtmlContext(state, document, position) || isJsContext(state, document, position)) {
return provideClassAttributeCompletions(state, document, position)
}
@ -354,10 +453,7 @@ function provideCssHelperCompletions(
return null
}
let base =
match.groups.helper === 'config'
? state.config
: dlv(state.config, 'theme', {})
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
let parts = match.groups.keys.split(/([\[\].]+)/)
let keys = parts.filter((_, i) => i % 2 === 0)
let separators = parts.filter((_, i) => i % 2 !== 0)
@ -372,9 +468,7 @@ function provideCssHelperCompletions(
let obj: any
let offset: number = 0
let separator: string = separators.length
? separators[separators.length - 1]
: null
let separator: string = separators.length ? separators[separators.length - 1] : null
if (keys.length === 1) {
obj = base
@ -396,8 +490,7 @@ function provideCssHelperCompletions(
isIncomplete: false,
items: Object.keys(obj).map((item, index) => {
let color = getColorFromValue(obj[item])
const replaceDot: boolean =
item.indexOf('.') !== -1 && separator && separator.endsWith('.')
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
const insertClosingBrace: boolean =
text.charAt(text.length - 1) !== ']' &&
(replaceDot || (separator && separator.endsWith('[')))
@ -408,21 +501,16 @@ function provideCssHelperCompletions(
filterText: `${replaceDot ? '.' : ''}${item}`,
sortText: naturalExpand(index),
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
// VS Code bug causes '0' to not display in some cases
detail: detail === '0' ? '0 ' : detail,
documentation: color,
// VS Code bug causes some values to not display in some cases
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
documentation: color instanceof TinyColor && color.a !== 0 ? color.toRgbString() : null,
textEdit: {
newText: `${replaceDot ? '[' : ''}${item}${
insertClosingBrace ? ']' : ''
}`,
newText: `${replaceDot ? '[' : ''}${item}${insertClosingBrace ? ']' : ''}`,
range: {
start: {
line: position.line,
character:
position.character -
keys[keys.length - 1].length -
(replaceDot ? 1 : 0) -
offset,
position.character - keys[keys.length - 1].length - (replaceDot ? 1 : 0) - offset,
},
end: position,
},
@ -433,7 +521,6 @@ function provideCssHelperCompletions(
}
}
// TODO: vary docs links based on Tailwind version
function provideTailwindDirectiveCompletions(
state: State,
document: TextDocument,
@ -550,13 +637,15 @@ function provideVariantsDirectiveCompletions(
return {
isIncomplete: false,
items: state.variants
items: Object.keys(state.variants)
.filter((v) => existingVariants.indexOf(v) === -1)
.map((variant) => ({
.map((variant, index) => ({
// TODO: detail
label: variant,
detail: state.variants[variant],
kind: 21,
data: 'variant',
sortText: naturalExpand(index),
textEdit: {
newText: variant,
range: {
@ -628,11 +717,7 @@ function provideScreenDirectiveCompletions(
if (match === null) return null
const screens = dlv(
state.config,
['screens'],
dlv(state.config, ['theme', 'screens'], {})
)
const screens = dlv(state.config, ['screens'], dlv(state.config, ['theme', 'screens'], {}))
if (!isObject(screens)) return null
@ -767,7 +852,7 @@ async function provideEmmetCompletions(
document: TextDocument,
position: Position
): Promise<CompletionList> {
let settings = await getDocumentSettings(state, document)
let settings = await state.editor.getConfiguration(document.uri)
if (settings.emmetCompletions !== true) return null
const isHtml = isHtmlContext(state, document, position)
@ -779,26 +864,16 @@ async function provideEmmetCompletions(
return null
}
const extractAbbreviationResults = emmetHelper.extractAbbreviation(
document,
position,
true
)
const extractAbbreviationResults = emmetHelper.extractAbbreviation(document, position, true)
if (
!extractAbbreviationResults ||
!emmetHelper.isAbbreviationValid(
syntax,
extractAbbreviationResults.abbreviation
)
!emmetHelper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)
) {
return null
}
if (
!isValidLocationForEmmetAbbreviation(
document,
extractAbbreviationResults.abbreviationRange
)
!isValidLocationForEmmetAbbreviation(document, extractAbbreviationResults.abbreviationRange)
) {
return null
}
@ -808,16 +883,13 @@ async function provideEmmetCompletions(
if (abbreviation.startsWith('this.')) {
return null
}
const { symbols } = await state.emitter.emit('getDocumentSymbols', {
uri: document.uri,
})
const symbols = await state.editor.getDocumentSymbols(document.uri)
if (
symbols &&
symbols.find(
(symbol) =>
abbreviation === symbol.name ||
(abbreviation.startsWith(symbol.name + '.') &&
!/>|\*|\+/.test(abbreviation))
(abbreviation.startsWith(symbol.name + '.') && !/>|\*|\+/.test(abbreviation))
)
) {
return null
@ -847,11 +919,7 @@ async function provideEmmetCompletions(
})
}
export async function doComplete(
state: State,
document: TextDocument,
position: Position
) {
export async function doComplete(state: State, document: TextDocument, position: Position) {
if (state === null) return { items: [], isIncomplete: false }
const result =
@ -873,32 +941,44 @@ export async function resolveCompletionItem(
state: State,
item: CompletionItem
): Promise<CompletionItem> {
if (
['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)
) {
if (['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)) {
return item
}
if (item.data === 'screen') {
let screens = dlv(
state.config,
['theme', 'screens'],
dlv(state.config, ['screens'], {})
)
let screens = dlv(state.config, ['theme', 'screens'], dlv(state.config, ['screens'], {}))
if (!isObject(screens)) screens = {}
item.detail = stringifyScreen(screens[item.label] as Screen)
return item
}
if (state.jit) {
if (item.kind === 9) return item
let { root, rules } = jit.generateRules(state, [item.data.join(state.separator)])
if (rules.length === 0) return item
if (!item.detail) {
if (rules.length === 1) {
item.detail = jit.stringifyDecls(rules[0])
} else {
item.detail = `${rules.length} rules`
}
}
if (!item.documentation) {
item.documentation = {
kind: 'markdown' as typeof MarkupKind.Markdown,
value: ['```css', await jit.stringifyRoot(state, root), '```'].join('\n'),
}
}
return item
}
const className = dlv(state.classNames.classNames, [...item.data, '__info'])
if (item.kind === 9) {
item.detail = state.classNames.context[
item.data[item.data.length - 1]
].join(', ')
item.detail = state.classNames.context[item.data[item.data.length - 1]].join(', ')
} else {
item.detail = await getCssDetail(state, className)
if (!item.documentation) {
const settings = await getDocumentSettings(state)
const settings = await state.editor.getConfiguration()
const css = stringifyCss(item.data.join(':'), className, {
tabSize: dlv(settings, 'tabSize', 2),
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
@ -949,9 +1029,7 @@ function stringifyDecls(
.map((prop) =>
ensureArray(obj[prop])
.map((value) => {
const px = showPixelEquivalents
? remToPx(value, rootFontSize)
: undefined
const px = showPixelEquivalents ? remToPx(value, rootFontSize) : undefined
return `${prop}: ${value}${px ? `/* ${px} */` : ''};`
})
.join(' ')
@ -964,7 +1042,7 @@ async function getCssDetail(state: State, className: any): Promise<string> {
return `${className.length} rules`
}
if (className.__rule === true) {
const settings = await getDocumentSettings(state)
const settings = await state.editor.getConfiguration()
return stringifyDecls(removeMeta(className), {
showPixelEquivalents: dlv(settings, 'showPixelEquivalents', true),
rootFontSize: dlv(settings, 'rootFontSize', 16),

View File

@ -1,6 +1,5 @@
import type { TextDocument } from 'vscode-languageserver'
import { State } from '../util/state'
import { getDocumentSettings } from '../util/getDocumentSettings'
import { DiagnosticKind, AugmentedDiagnostic } from './types'
import { getCssConflictDiagnostics } from './getCssConflictDiagnostics'
import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics'
@ -8,6 +7,7 @@ import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics'
import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics'
import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics'
import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics'
import { getIncorrectVariantOrderDiagnostics } from './getIncorrectVariantOrderDiagnostics'
export async function doValidate(
state: State,
@ -19,9 +19,10 @@ export async function doValidate(
DiagnosticKind.InvalidVariant,
DiagnosticKind.InvalidConfigPath,
DiagnosticKind.InvalidTailwindDirective,
DiagnosticKind.IncorrectVariantOrder,
]
): Promise<AugmentedDiagnostic[]> {
const settings = await getDocumentSettings(state, document)
const settings = await state.editor.getConfiguration(document.uri)
return settings.validate
? [
@ -43,22 +44,25 @@ export async function doValidate(
...(only.includes(DiagnosticKind.InvalidTailwindDirective)
? getInvalidTailwindDirectiveDiagnostics(state, document, settings)
: []),
...(only.includes(DiagnosticKind.IncorrectVariantOrder)
? await getIncorrectVariantOrderDiagnostics(state, document, settings)
: []),
]
: []
}
export async function provideDiagnostics(state: State, document: TextDocument) {
state.editor.connection.sendDiagnostics({
uri: document.uri,
diagnostics: await doValidate(state, document),
})
// state.editor.connection.sendDiagnostics({
// uri: document.uri,
// diagnostics: await doValidate(state, document),
// })
}
export function clearDiagnostics(state: State, document: TextDocument): void {
state.editor.connection.sendDiagnostics({
uri: document.uri,
diagnostics: [],
})
// state.editor.connection.sendDiagnostics({
// uri: document.uri,
// diagnostics: [],
// })
}
export function clearAllDiagnostics(state: State): void {

View File

@ -2,13 +2,11 @@ import { joinWithAnd } from '../util/joinWithAnd'
import { State, Settings } from '../util/state'
import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
import { CssConflictDiagnostic, DiagnosticKind } from './types'
import {
findClassListsInDocument,
getClassNamesInClassList,
} from '../util/find'
import { findClassListsInDocument, getClassNamesInClassList } from '../util/find'
import { getClassNameDecls } from '../util/getClassNameDecls'
import { getClassNameMeta } from '../util/getClassNameMeta'
import { equal } from '../util/array'
import * as jit from '../util/jit'
export async function getCssConflictDiagnostics(
state: State,
@ -25,6 +23,81 @@ export async function getCssConflictDiagnostics(
const classNames = getClassNamesInClassList(classList)
classNames.forEach((className, index) => {
if (state.jit) {
let { rules } = jit.generateRules(state, [className.className])
if (rules.length !== 1) {
return
}
let rule = rules[0]
let context: string[]
let properties = []
rule.walkDecls(({ prop }) => {
properties.push(prop)
})
let otherClassNames = classNames.filter((_className, i) => i !== index)
let conflictingClassNames = otherClassNames.filter((otherClassName) => {
let { rules } = jit.generateRules(state, [otherClassName.className])
if (rules.length !== 1) {
return false
}
let otherRule = rules[0]
let otherProperties = []
otherRule.walkDecls(({ prop }) => {
otherProperties.push(prop)
})
if (!equal(properties, otherProperties)) {
return false
}
if (!context) {
context = jit.getRuleContext(state, rule, className.className)
}
let otherContext = jit.getRuleContext(state, otherRule, otherClassName.className)
if (!equal(context, otherContext)) {
return false
}
return true
})
if (conflictingClassNames.length === 0) return
diagnostics.push({
code: DiagnosticKind.CssConflict,
className,
otherClassNames: conflictingClassNames,
range: className.range,
severity:
severity === 'error'
? 1 /* DiagnosticSeverity.Error */
: 2 /* DiagnosticSeverity.Warning */,
message: `'${className.className}' applies the same CSS ${
properties.length === 1 ? 'property' : 'properties'
} as ${joinWithAnd(
conflictingClassNames.map(
(conflictingClassName) => `'${conflictingClassName.className}'`
)
)}.`,
relatedInformation: conflictingClassNames.map((conflictingClassName) => {
return {
message: conflictingClassName.className,
location: {
uri: document.uri,
range: conflictingClassName.range,
},
}
}),
})
return
}
let decls = getClassNameDecls(state, className.className)
if (!decls) return
@ -63,12 +136,9 @@ export async function getCssConflictDiagnostics(
message: `'${className.className}' applies the same CSS ${
properties.length === 1 ? 'property' : 'properties'
} as ${joinWithAnd(
conflictingClassNames.map(
(conflictingClassName) => `'${conflictingClassName.className}'`
)
conflictingClassNames.map((conflictingClassName) => `'${conflictingClassName.className}'`)
)}.`,
relatedInformation: conflictingClassNames.map(
(conflictingClassName) => {
relatedInformation: conflictingClassNames.map((conflictingClassName) => {
return {
message: conflictingClassName.className,
location: {
@ -76,8 +146,7 @@ export async function getCssConflictDiagnostics(
range: conflictingClassName.range,
},
}
}
),
}),
})
})
})

View File

@ -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
}

View File

@ -61,10 +61,7 @@ export function getInvalidTailwindDirectiveDiagnostics(
code: DiagnosticKind.InvalidTailwindDirective,
range: absoluteRange(
{
start: indexToPosition(
text,
match.index + match[0].length - match.groups.value.length
),
start: indexToPosition(text, match.index + match[0].length - match.groups.value.length),
end: indexToPosition(text, match.index + match[0].length),
},
range

View File

@ -37,13 +37,13 @@ export function getInvalidVariantDiagnostics(
for (let i = 0; i < variants.length; i += 2) {
let variant = variants[i].trim()
if (state.variants.includes(variant)) {
if (Object.keys(state.variants).includes(variant)) {
continue
}
let message = `The variant '${variant}' does not exist.`
let suggestions: string[] = []
let suggestion = closest(variant, state.variants)
let suggestion = closest(variant, Object.keys(state.variants))
if (suggestion) {
suggestions.push(suggestion)

View File

@ -8,6 +8,7 @@ export enum DiagnosticKind {
InvalidVariant = 'invalidVariant',
InvalidConfigPath = 'invalidConfigPath',
InvalidTailwindDirective = 'invalidTailwindDirective',
IncorrectVariantOrder = 'incorrectVariantOrder',
}
export type CssConflictDiagnostic = Diagnostic & {
@ -77,6 +78,17 @@ export function isInvalidTailwindDirectiveDiagnostic(
return diagnostic.code === DiagnosticKind.InvalidTailwindDirective
}
export type IncorrectVariantOrderDiagnostic = Diagnostic & {
code: DiagnosticKind.IncorrectVariantOrder
suggestions: string[]
}
export function isIncorrectVariantOrderDiagnostic(
diagnostic: AugmentedDiagnostic
): diagnostic is IncorrectVariantOrderDiagnostic {
return diagnostic.code === DiagnosticKind.IncorrectVariantOrder
}
export type AugmentedDiagnostic =
| CssConflictDiagnostic
| InvalidApplyDiagnostic
@ -84,3 +96,4 @@ export type AugmentedDiagnostic =
| InvalidVariantDiagnostic
| InvalidConfigPathDiagnostic
| InvalidTailwindDirectiveDiagnostic
| IncorrectVariantOrderDiagnostic

View File

@ -4,27 +4,34 @@ import {
getClassNamesInClassList,
findHelperFunctionsInDocument,
} from './util/find'
import { getClassNameParts } from './util/getClassNameAtPosition'
import { getColor, getColorFromValue } from './util/color'
import { getColor, getColorFromValue, tinyColorToVscodeColor } from './util/color'
import { stringToPath } from './util/stringToPath'
import type { TextDocument } from 'vscode-languageserver'
const dlv = require('dlv')
import type { TextDocument, ColorInformation } from 'vscode-languageserver'
import { TinyColor } from '@ctrl/tinycolor'
import dlv from 'dlv'
export async function getDocumentColors(state: State, document: TextDocument) {
let colors = []
export async function getDocumentColors(
state: State,
document: TextDocument
): Promise<ColorInformation[]> {
let colors: ColorInformation[] = []
if (!state.enabled) return colors
let settings = await state.editor.getConfiguration(document.uri)
if (settings.colorDecorators === 'off') return colors
let classLists = await findClassListsInDocument(state, document)
classLists.forEach((classList) => {
let classNames = getClassNamesInClassList(classList)
classNames.forEach((className) => {
let parts = getClassNameParts(state, className.className)
if (!parts) return
let color = getColor(state, parts)
let color = getColor(state, className.className)
if (color === null || typeof color === 'string' || color.a === 0) {
return
}
colors.push({ range: className.range, color: color.toRgbString() })
colors.push({
range: className.range,
color: tinyColorToVscodeColor(color),
})
})
})
@ -34,8 +41,8 @@ export async function getDocumentColors(state: State, document: TextDocument) {
let base = fn.helper === 'theme' ? ['theme'] : []
let value = dlv(state.config, [...base, ...keys])
let color = getColorFromValue(value)
if (color) {
colors.push({ range: fn.valueRange, color })
if (color instanceof TinyColor && color.a !== 0) {
colors.push({ range: fn.valueRange, color: tinyColorToVscodeColor(color) })
}
})

View File

@ -6,7 +6,7 @@ import { isCssContext } from './util/css'
import { findClassNameAtPosition } from './util/find'
import { validateApply } from './util/validateApply'
import { getClassNameParts } from './util/getClassNameAtPosition'
import { getDocumentSettings } from './util/getDocumentSettings'
import * as jit from './util/jit'
export async function doHover(
state: State,
@ -19,11 +19,7 @@ export async function doHover(
)
}
function provideCssHelperHover(
state: State,
document: TextDocument,
position: Position
): Hover {
function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover {
if (!isCssContext(state, document, position)) return null
const line = document.getText({
@ -31,9 +27,7 @@ function provideCssHelperHover(
end: { line: position.line + 1, character: 0 },
})
const match = line.match(
/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/
)
const match = line.match(/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/)
if (match === null) return null
@ -80,6 +74,22 @@ async function provideClassNameHover(
let className = await findClassNameAtPosition(state, document, position)
if (className === null) return null
if (state.jit) {
let { root, rules } = jit.generateRules(state, [className.className])
if (rules.length === 0) {
return null
}
return {
contents: {
language: 'css',
value: await jit.stringifyRoot(state, root, document.uri),
},
range: className.range,
}
}
const parts = getClassNameParts(state, className.className)
if (!parts) return null
@ -90,7 +100,7 @@ async function provideClassNameHover(
}
}
const settings = await getDocumentSettings(state, document)
const settings = await state.editor.getConfiguration(document.uri)
const css = stringifyCss(
className.className,

View File

@ -2,14 +2,8 @@ export function dedupe<T>(arr: Array<T>): Array<T> {
return arr.filter((value, index, self) => self.indexOf(value) === index)
}
export function dedupeBy<T>(
arr: Array<T>,
transform: (item: T) => any
): Array<T> {
return arr.filter(
(value, index, self) =>
self.map(transform).indexOf(transform(value)) === index
)
export function dedupeBy<T>(arr: Array<T>, transform: (item: T) => any): Array<T> {
return arr.filter((value, index, self) => self.map(transform).indexOf(transform(value)) === index)
}
export function ensureArray<T>(value: T | T[]): T[] {
@ -20,9 +14,27 @@ export function flatten<T>(arrays: T[][]): T[] {
return [].concat.apply([], arrays)
}
export function equal(arr1: any[], arr2: any[]): boolean {
return (
JSON.stringify(arr1.concat([]).sort()) ===
JSON.stringify(arr2.concat([]).sort())
)
export function equal(a: any[], b: any[]): boolean {
if (a === b) return true
if (a.length !== b.length) return false
let aSorted = a.concat().sort()
let bSorted = b.concat().sort()
for (let i = 0; i < aSorted.length; ++i) {
if (aSorted[i] !== bSorted[i]) return false
}
return true
}
export function equalExact(a: any[], b: any[]): boolean {
if (a === b) return true
if (a.length !== b.length) return false
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false
}
return true
}

View File

@ -3,6 +3,9 @@ import { State } from './state'
import removeMeta from './removeMeta'
import { TinyColor, names as colorNames } from '@ctrl/tinycolor'
import { ensureArray, dedupe, flatten } from './array'
import type { Color } from 'vscode-languageserver'
import { getClassNameParts } from './getClassNameAtPosition'
import * as jit from './jit'
const COLOR_PROPS = [
'caret-color',
@ -21,58 +24,78 @@ const COLOR_PROPS = [
'text-decoration-color',
]
function isKeyword(value: string): boolean {
return ['transparent', 'currentcolor'].includes(value.toLowerCase())
type KeywordColor = 'transparent' | 'currentColor'
function getKeywordColor(value: unknown): KeywordColor | null {
if (typeof value !== 'string') return null
let lowercased = value.toLowerCase()
if (lowercased === 'transparent') {
return 'transparent'
}
if (lowercased === 'currentcolor') {
return 'currentColor'
}
return null
}
export function getColor(
state: State,
keys: string[]
): TinyColor | string | null {
const item = dlv(state.classNames.classNames, [...keys, '__info'])
if (!item.__rule) return null
const props = Object.keys(removeMeta(item))
// https://github.com/khalilgharbaoui/coloregex
const colorRegex = new RegExp(
`(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\\((-?[\\d.]+%?[,\\s]+){2,3}\\s*([\\d.]+%?|var\\([^)]+\\))?\\)|transparent|currentColor|${Object.keys(
colorNames
).join('|')})`,
'gi'
)
function getColorsInString(str: string): (TinyColor | KeywordColor)[] {
if (/(?:box|drop)-shadow/.test(str)) return []
return (
str
.match(colorRegex)
?.map((color) => color.replace(/var\([^)]+\)/, '1'))
.map((color) => getKeywordColor(color) ?? new TinyColor(color))
.filter((color) => (color instanceof TinyColor ? color.isValid : true)) ?? []
)
}
function getColorFromDecls(
decls: Record<string, string | string[]>
): TinyColor | KeywordColor | null {
let props = Object.keys(decls)
if (props.length === 0) return null
const nonCustomProps = props.filter((prop) => !prop.startsWith('--'))
const areAllCustom = nonCustomProps.length === 0
if (
!areAllCustom &&
nonCustomProps.some((prop) => !COLOR_PROPS.includes(prop))
) {
if (!areAllCustom && nonCustomProps.some((prop) => !COLOR_PROPS.includes(prop))) {
// they should all be color-based props
return null
}
const propsToCheck = areAllCustom ? props : nonCustomProps
const colors = flatten(
propsToCheck.map((prop) => ensureArray(item[prop]).map(createColor))
)
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap(getColorsInString))
// check that all of the values are valid colors
if (colors.some((color) => typeof color !== 'string' && !color.isValid)) {
return null
}
// if (colors.some((color) => color instanceof TinyColor && !color.isValid)) {
// return null
// }
// check that all of the values are the same color, ignoring alpha
const colorStrings = dedupe(
colors.map((color) =>
typeof color === 'string' ? color : `${color.r}-${color.g}-${color.b}`
)
colors.map((color) => (color instanceof TinyColor ? `${color.r}-${color.g}-${color.b}` : color))
)
if (colorStrings.length !== 1) {
return null
}
if (isKeyword(colorStrings[0])) {
return colorStrings[0]
let keyword = getKeywordColor(colorStrings[0])
if (keyword) {
return keyword
}
const nonKeywordColors = colors.filter(
(color): color is TinyColor => typeof color !== 'string'
)
const nonKeywordColors = colors.filter((color): color is TinyColor => typeof color !== 'string')
const alphas = dedupe(nonKeywordColors.map((color) => color.a))
@ -87,11 +110,48 @@ export function getColor(
return null
}
export function getColorFromValue(value: unknown): string {
export function getColor(state: State, className: string): TinyColor | KeywordColor | null {
if (state.jit) {
const item = dlv(state.classNames.classNames, [className, '__info'])
if (item && item.__rule) {
return getColorFromDecls(removeMeta(item))
}
let { root, rules } = jit.generateRules(state, [className])
if (rules.length === 0) return null
let decls: Record<string, string | string[]> = {}
root.walkDecls((decl) => {
let value = decls[decl.prop]
if (value) {
if (Array.isArray(value)) {
value.push(decl.value)
} else {
decls[decl.prop] = [value, decl.value]
}
} else {
decls[decl.prop] = decl.value
}
})
return getColorFromDecls(decls)
}
let parts = getClassNameParts(state, className)
if (!parts) return null
const item = dlv(state.classNames.classNames, [...parts, '__info'])
if (!item.__rule) return null
return getColorFromDecls(removeMeta(item))
}
export function getColorFromValue(value: unknown): TinyColor | KeywordColor | null {
if (typeof value !== 'string') return null
const trimmedValue = value.trim()
if (trimmedValue === 'transparent') {
return 'rgba(0, 0, 0, 0.01)'
if (trimmedValue.toLowerCase() === 'transparent') {
return 'transparent'
}
if (trimmedValue.toLowerCase() === 'currentcolor') {
return 'currentColor'
}
if (
!/^\s*(?:rgba?|hsla?)\s*\([^)]+\)\s*$/.test(trimmedValue) &&
@ -102,20 +162,22 @@ export function getColorFromValue(value: unknown): string {
}
const color = new TinyColor(trimmedValue)
if (color.isValid) {
return color.toRgbString()
return color
// return { red: color.r / 255, green: color.g / 255, blue: color.b / 255, alpha: color.a }
}
return null
}
function createColor(str: string): TinyColor | string {
if (isKeyword(str)) {
return str
function createColor(str: string): TinyColor | KeywordColor {
let keyword = getKeywordColor(str)
if (keyword) {
return keyword
}
// matches: rgba(<r>, <g>, <b>, var(--bg-opacity))
// TODO: support other formats? e.g. hsla, css level 4
const match = str.match(
/^\s*rgba\(\s*(?<r>[0-9]{1,3})\s*,\s*(?<g>[0-9]{1,3})\s*,\s*(?<b>[0-9]{1,3})\s*,\s*var/
/^\s*rgba\(\s*(?<r>[0-9.]+)\s*,\s*(?<g>[0-9.]+)\s*,\s*(?<b>[0-9.]+)\s*,\s*var/
)
if (match) {
@ -128,3 +190,7 @@ function createColor(str: string): TinyColor | string {
return new TinyColor(str)
}
export function tinyColorToVscodeColor(color: TinyColor): Color {
return { red: color.r / 255, green: color.g / 255, blue: color.b / 255, alpha: color.a }
}

View File

@ -17,7 +17,6 @@ import {
} from './lexers'
import { getLanguageBoundaries } from './getLanguageBoundaries'
import { resolveRange } from './resolveRange'
import { getDocumentSettings } from './getDocumentSettings'
const dlv = require('dlv')
import { createMultiRegexp } from './createMultiRegexp'
@ -146,7 +145,7 @@ async function findCustomClassLists(
doc: TextDocument,
range?: Range
): Promise<DocumentClassList[]> {
const settings = await getDocumentSettings(state, doc)
const settings = await state.editor.getConfiguration(doc.uri)
const regexes = dlv(settings, 'experimental.classRegex', [])
if (!Array.isArray(regexes) || regexes.length === 0) return []

View File

@ -21,6 +21,10 @@ export function getClassNameMeta(
}))
}
if (info === undefined) {
console.log({ classNameOrParts })
}
return {
source: info.__source,
pseudo: info.__pseudo,

View File

@ -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
}

View File

@ -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 }
}

View File

@ -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
}

View File

@ -1,7 +1,4 @@
export function remToPx(
value: string,
rootSize: number = 16
): string | undefined {
export function remToPx(value: string, rootSize: number = 16): string | undefined {
if (/^-?[0-9.]+rem$/.test(value)) {
let number = parseFloat(value.substr(0, value.length - 3))
if (!isNaN(number)) {

View File

@ -1,5 +1,6 @@
import type { TextDocuments, Connection, Range } from 'vscode-languageserver'
import type { TextDocuments, Connection, Range, SymbolInformation } from 'vscode-languageserver'
import type { TextDocument } from 'vscode-languageserver-textdocument'
import type { Postcss } from 'postcss'
export type ClassNamesTree = {
[key: string]: ClassNamesTree
@ -17,13 +18,14 @@ export type ClassNames = {
export type EditorState = {
connection: Connection
documents: TextDocuments<TextDocument>
documentSettings: Map<string, Settings>
globalSettings: Settings
userLanguages: Record<string, string>
capabilities: {
configuration: boolean
diagnosticRelatedInformation: boolean
}
getConfiguration: (uri?: string) => Promise<Settings>
getDocumentSymbols: (uri: string) => Promise<SymbolInformation[]>
}
type DiagnosticSeveritySetting = 'ignore' | 'warning' | 'error'
@ -35,6 +37,7 @@ export type Settings = {
validate: boolean
showPixelEquivalents: boolean
rootFontSize: number
colorDecorators: 'inherit' | 'on' | 'off'
lint: {
cssConflict: DiagnosticSeveritySetting
invalidApply: DiagnosticSeveritySetting
@ -42,36 +45,41 @@ export type Settings = {
invalidVariant: DiagnosticSeveritySetting
invalidConfigPath: DiagnosticSeveritySetting
invalidTailwindDirective: DiagnosticSeveritySetting
incorrectVariantOrder: DiagnosticSeveritySetting
}
experimental: {
classRegex: string[]
}
}
interface NotificationEmitter {
on: (name: string, handler: (args: any) => void) => void
off: (name: string, handler: (args: any) => void) => void
emit: (name: string, args: any) => Promise<any>
export interface FeatureFlags {
future: string[]
experimental: string[]
}
export type State = null | {
export interface State {
enabled: boolean
emitter?: NotificationEmitter
version?: string
configPath?: string
config?: any
modules?: {
tailwindcss: any
postcss: any
}
version?: string
separator?: string
plugins?: any[]
variants?: string[]
classNames?: ClassNames
dependencies?: string[]
featureFlags?: { future: string[]; experimental: string[] }
plugins?: any
variants?: Record<string, string | null>
modules?: {
tailwindcss?: { version: string; module: any }
postcss?: { version: string; module: Postcss }
postcssSelectorParser?: { module: any }
resolveConfig?: { module: any }
jit?: { generateRules: { module: any } }
}
browserslist?: string[]
featureFlags?: FeatureFlags
classNames?: ClassNames
editor?: EditorState
error?: Error
jit?: boolean
jitContext?: any
// postcssPlugins?: { before: any[]; after: any[] }
}
export type DocumentClassList = {

View File

@ -7,13 +7,14 @@ export function validateApply(
state: State,
classNameOrParts: string | string[]
): { isApplyable: true } | { isApplyable: false; reason: string } | null {
if (state.jit) {
return { isApplyable: true }
}
const meta = getClassNameMeta(state, classNameOrParts)
if (!meta) return null
if (
semver.gte(state.version, '2.0.0-alpha.1') ||
flagEnabled(state, 'applyComplexClasses')
) {
if (semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')) {
return { isApplyable: true }
}

332
src/extension.ts 100755
View File

@ -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)
}

16
src/lib/env.ts 100644
View File

@ -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)
}

View File

@ -1,39 +1,47 @@
import selectorParser from 'postcss-selector-parser'
import dset from 'dset'
import dlv from 'dlv'
import type { Container, Node, Root, AtRule } from 'postcss'
function isAtRule(node: Node): node is AtRule {
return node.type === 'atrule'
}
function createSelectorFromNodes(nodes) {
if (nodes.length === 0) return null
const selector = selectorParser.selector()
const selector = selectorParser.selector({ value: '' })
for (let i = 0; i < nodes.length; i++) {
selector.append(nodes[i])
}
return String(selector).trim()
}
function getClassNamesFromSelector(selector) {
function getClassNamesFromSelector(selector: string) {
const classNames = []
const { nodes: subSelectors } = selectorParser().astSync(selector)
for (let i = 0; i < subSelectors.length; i++) {
let subSelector = subSelectors[i]
if (subSelector.type !== 'selector') continue
let scope = []
for (let j = 0; j < subSelectors[i].nodes.length; j++) {
let node = subSelectors[i].nodes[j]
for (let j = 0; j < subSelector.nodes.length; j++) {
let node = subSelector.nodes[j]
let pseudo = []
if (node.type === 'class') {
let next = subSelectors[i].nodes[j + 1]
let next = subSelector.nodes[j + 1]
while (next && next.type === 'pseudo') {
pseudo.push(next)
j++
next = subSelectors[i].nodes[j + 1]
next = subSelector.nodes[j + 1]
}
classNames.push({
className: node.value.trim(),
scope: createSelectorFromNodes(scope),
__rule: j === subSelectors[i].nodes.length - 1,
__rule: j === subSelector.nodes.length - 1,
__pseudo: pseudo.map(String),
})
}
@ -44,7 +52,7 @@ function getClassNamesFromSelector(selector) {
return classNames
}
async function process(root) {
async function process(root: Root) {
const tree = {}
const commonContext = {}
@ -68,9 +76,7 @@ async function process(root) {
rule.walkDecls((decl) => {
if (decls[decl.prop]) {
decls[decl.prop] = [
...(Array.isArray(decls[decl.prop])
? decls[decl.prop]
: [decls[decl.prop]]),
...(Array.isArray(decls[decl.prop]) ? decls[decl.prop] : [decls[decl.prop]]),
decl.value,
]
} else {
@ -78,11 +84,11 @@ async function process(root) {
}
})
let p = rule
let p: Container = rule
const keys = []
while (p.parent.type !== 'root') {
p = p.parent
if (p.type === 'atrule') {
if (isAtRule(p)) {
keys.push(`@${p.name} ${p.params}`)
}
}
@ -104,25 +110,13 @@ async function process(root) {
}
if (classNames[i].__rule) {
dset(tree, [...baseKeys, '__info', ...index, '__rule'], true)
dset(tree, [...baseKeys, '__info', ...index, '__source'], layer)
dsetEach(tree, [...baseKeys, '__info', ...index], decls)
}
dset(
tree,
[...baseKeys, '__info', ...index, '__pseudo'],
classNames[i].__pseudo
)
dset(
tree,
[...baseKeys, '__info', ...index, '__scope'],
classNames[i].scope
)
dset(
tree,
[...baseKeys, '__info', ...index, '__context'],
context.concat([]).reverse()
)
dset(tree, [...baseKeys, '__info', ...index, '__source'], layer)
dset(tree, [...baseKeys, '__info', ...index, '__pseudo'], classNames[i].__pseudo)
dset(tree, [...baseKeys, '__info', ...index, '__scope'], classNames[i].scope)
dset(tree, [...baseKeys, '__info', ...index, '__context'], context.concat([]).reverse())
// common context
context.push(...classNames[i].__pseudo.map((x) => `&${x}`))
@ -131,10 +125,7 @@ async function process(root) {
if (typeof commonContext[contextKeys[i]] === 'undefined') {
commonContext[contextKeys[i]] = context
} else {
commonContext[contextKeys[i]] = intersection(
commonContext[contextKeys[i]],
context
)
commonContext[contextKeys[i]] = intersection(commonContext[contextKeys[i]], context)
}
}
}
@ -143,26 +134,15 @@ async function process(root) {
return { classNames: tree, context: commonContext }
}
function intersection(arr1, arr2) {
function intersection<T>(arr1: T[], arr2: T[]): T[] {
return arr1.filter((value) => arr2.indexOf(value) !== -1)
}
function dsetEach(obj, keys, values) {
function dsetEach(obj, keys: string[], values: Record<string, string>) {
const k = Object.keys(values)
for (let i = 0; i < k.length; i++) {
dset(obj, [...keys, k[i]], values[k[i]])
}
}
function arraysEqual(a, b) {
if (a === b) return true
if (a == null || b == null) return false
if (a.length !== b.length) return false
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false
}
return true
}
export default process

92
src/lib/hook.ts 100644
View File

@ -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
}
}

View File

@ -1,29 +1,29 @@
import { TextDocument } from 'vscode-languageserver'
import { State } from '../../util/state'
import { doValidate } from 'tailwindcss-language-service'
import { TextDocument } from 'vscode-languageserver/node'
import { State } from 'tailwindcss-language-service/src/util/state'
import { doValidate } from 'tailwindcss-language-service/src/diagnostics/diagnosticsProvider'
export async function provideDiagnostics(state: State, document: TextDocument) {
state.editor.connection.sendDiagnostics({
state.editor?.connection.sendDiagnostics({
uri: document.uri,
diagnostics: await doValidate(state, document),
})
}
export function clearDiagnostics(state: State, document: TextDocument): void {
state.editor.connection.sendDiagnostics({
state.editor?.connection.sendDiagnostics({
uri: document.uri,
diagnostics: [],
})
}
export function clearAllDiagnostics(state: State): void {
state.editor.documents.all().forEach((document) => {
state.editor?.documents.all().forEach((document) => {
clearDiagnostics(state, document)
})
}
export function updateAllDiagnostics(state: State): void {
state.editor.documents.all().forEach((document) => {
state.editor?.documents.all().forEach((document) => {
provideDiagnostics(state, document)
})
}

1157
src/server.ts 100644

File diff suppressed because it is too large Load Diff

40
src/util/error.ts 100644
View File

@ -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()

View File

@ -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
}

18
tsconfig.json 100755
View File

@ -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"]
}