tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
Merge branch 'diagnostics'
42 changed files, 1982 additions(+), 201 deletions(-)
diff --git a/.github/autocomplete.png b/.github/autocomplete.png
new file mode 100644
index 0000000000000000000000000000000000000000..7857187025e47936aa933cb2cfefc187ac4ef10d
Binary files /dev/null and b/.github/autocomplete.png differ
diff --git a/.github/banner.png b/.github/banner.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f927163a27fffb33f9be3bb52928f69bf020242
Binary files /dev/null and b/.github/banner.png differ
diff --git a/.github/hover.png b/.github/hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..e46a8efaa5dfdd9aeb52facc08a3a7687b6634b3
Binary files /dev/null and b/.github/hover.png differ
diff --git a/.github/linting.png b/.github/linting.png
new file mode 100644
index 0000000000000000000000000000000000000000..0c118825152d9335690fc59702cb4b842663e652
Binary files /dev/null and b/.github/linting.png differ
diff --git a/.vscodeignore b/.vscodeignore
index 23a60828e8f0b03542bc2d6c396023fd6f92343e..591602a20e25e0f55f0c73215f8494ca8a21fd2a 100755
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -9,4 +9,4 @@ node_modules/**
src/**
tests/**
.vscode/**
-.vscode/**
+**/*.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8bf3f3ba96f9cb15680ba52eb982625599c7293..4b3f32a981d6b114a3a3fb93fc044ce328c95b90 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# Changelog
+## 0.4.0
+
+- Added linting and quick fixes for both CSS and markup
+- Updated module resolution for compatibility with pnpm (#128)
+- The extension now ignores the `purge` option when extracting class names (#131)
+- Fixed hover offsets for class names which appear after interpolations
+
+## 0.3.1
+
+- Fixed class attribute completions not showing when using the following Pug syntax (#125):
+ ```
+ div(class="")
+ ```
+- Fixed hover previews not showing when using a computed class attribute in Vue templates
+- Restore missing readme images
+- Update settings descriptions to use markdown
+
## 0.3.0
### General
diff --git a/README.md b/README.md
index 27cc6e8c480bc703fa2c9a987bd60aac0b73250a..da341e65e5e2eb36d7069a87e9958b35a632318b 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,128 @@
-# Tailwind CSS IntelliSense
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/diagnostics/.github/banner.png" alt="" />
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+Tailwind CSS IntelliSense enhances the Tailwind development experience by providing Visual Studio Code users with advanced features such as autocomplete, syntax highlighting, and linting.
**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
+- `tailwindcss` to be installed (present in project `node_modules/`)
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
-## Requirements
-
-This extension requires:
-- a `tailwind.config.js` file to be [present in your project folder](https://github.com/bradlc/vscode-tailwindcss/blob/master/package.json#L38). You can create it with `npx tailwind init`.
-- `tailwindcss` to be installed (present in project `node_modules/`)
-
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
# Tailwind CSS IntelliSense
# Tailwind CSS IntelliSense
-# Tailwind CSS IntelliSense
-# Tailwind CSS IntelliSense
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
-# Tailwind CSS IntelliSense
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
-# Tailwind CSS IntelliSense
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
-# Tailwind CSS IntelliSense
+
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
-# Tailwind CSS IntelliSense
+
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
## Requirements
-# Tailwind CSS IntelliSense
+
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
This extension requires:
-# Tailwind CSS IntelliSense
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
- a `tailwind.config.js` file to be [present in your project folder](https://github.com/bradlc/vscode-tailwindcss/blob/master/package.json#L38). You can create it with `npx tailwind init`.
-# Tailwind CSS IntelliSense
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
- `tailwindcss` to be installed (present in project `node_modules/`)
+## Requirements
+## Requirements
# Tailwind CSS IntelliSense
+## Requirements
+## Settings
> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
+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": {
**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
+ }
+**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
-<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
+```
+### `tailwindCSS.emmetCompletions`
+**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
-
+```json
+{
+**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
## Requirements
+**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
+```
-This extension requires:
+### `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
+## Requirements
- a `tailwind.config.js` file to be [present in your project folder](https://github.com/bradlc/vscode-tailwindcss/blob/master/package.json#L38). You can create it with `npx tailwind init`.
-
+## Requirements
- `tailwindcss` to be installed (present in project `node_modules/`)
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
# Tailwind CSS IntelliSense
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
## Requirements
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+This extension requires:
This extension requires:
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+
+This extension requires:
- a `tailwind.config.js` file to be [present in your project folder](https://github.com/bradlc/vscode-tailwindcss/blob/master/package.json#L38). You can create it with `npx tailwind init`.
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
+
+This extension requires:
- `tailwindcss` to be installed (present in project `node_modules/`)
- "plaintext": "html"
+
- }
+Class names on the same HTML element which apply the same CSS property or properties. **Default: `warning`**
-**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
-```
+## Troubleshooting
-### `tailwindcss.emmetCompletions`
+- a `tailwind.config.js` file to be [present in your project folder](https://github.com/bradlc/vscode-tailwindcss/blob/master/package.json#L38). You can create it with `npx tailwind init`.
-Enable completions when using [Emmet](https://emmet.io/)-style syntax, for example `div.bg-red-500.uppercase`. **Default: `false`**
+- a `tailwind.config.js` file to be [present in your project folder](https://github.com/bradlc/vscode-tailwindcss/blob/master/package.json#L38). You can create it with `npx tailwind init`.
> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
-This extension requires:
-> [Tailwind CSS](https://tailwindcss.com/) class name completion for VS Code
- a `tailwind.config.js` file to be [present in your project folder](https://github.com/bradlc/vscode-tailwindcss/blob/master/package.json#L38). You can create it with `npx tailwind init`.
**[Get it from the VS Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
-## Requirements
-}
-```
+- 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.
diff --git a/img/css-highlighting-after.png b/img/css-highlighting-after.png
deleted file mode 100755
index 86a12fb8596e001e2c68d171733cb7950d66335c..0000000000000000000000000000000000000000
Binary files a/img/css-highlighting-after.png and /dev/null differ
diff --git a/img/css-highlighting-before.png b/img/css-highlighting-before.png
deleted file mode 100755
index 2eeffdc19d7293136e52f9426eb66456d0c1abb4..0000000000000000000000000000000000000000
Binary files a/img/css-highlighting-before.png and /dev/null differ
diff --git a/img/css.gif b/img/css.gif
deleted file mode 100755
index e81904e7863349207daf4e368aae489a2f9374d8..0000000000000000000000000000000000000000
Binary files a/img/css.gif and /dev/null differ
diff --git a/img/html-hover.gif b/img/html-hover.gif
deleted file mode 100755
index 94540b86a062f0274f9ae1d3d755898f749fed43..0000000000000000000000000000000000000000
Binary files a/img/html-hover.gif and /dev/null differ
diff --git a/img/html.gif b/img/html.gif
deleted file mode 100755
index 8ffc7ffa9962333d28c60136b1f7ad50edf09883..0000000000000000000000000000000000000000
Binary files a/img/html.gif and /dev/null differ
diff --git a/package-lock.json b/package-lock.json
index 91c1aca81ac946c1fd36e4fd59904914710f8008..9184c0f6d4d60cb918f95cc528656182719d94a9 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -2026,6 +2026,12 @@ "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
"integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=",
"dev": true
},
+ "detect-indent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz",
+ "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==",
+ "dev": true
+ },
"detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -6083,6 +6089,12 @@ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
"dev": true,
"optional": true
+ },
+ "sift-string": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/sift-string/-/sift-string-0.0.2.tgz",
+ "integrity": "sha1-G7ArEhslu4sHRwQr+afh2s+PuJw=",
+ "dev": true
},
"signal-exit": {
"version": "3.0.3",
diff --git a/package.json b/package.json
index e234600cea927baa23013a2664e31a4b32f7e355..c209d4be4eee26ae6b6a3744e74f540a95d5624b 100755
--- a/package.json
+++ b/package.json
@@ -1,21 +1,22 @@
{
"name": "vscode-tailwindcss",
"displayName": "Tailwind CSS IntelliSense",
- "description": "Tailwind CSS class name completion",
+ "description": "Intelligent Tailwind CSS tooling for VS Code",
"preview": true,
"author": "Brad Cornes <hello@bradley.dev>",
"license": "MIT",
"version": "0.3.1",
+ "url": "https://github.com/bradlc/vscode-tailwindcss/issues",
"homepage": "https://github.com/bradlc/vscode-tailwindcss",
"bugs": {
{
-{
+ "tailwindcss",
"email": "hello@bradley.dev"
},
"repository": {
"type": "git",
{
- "author": "Brad Cornes <hello@bradley.dev>",
+ "css",
},
"publisher": "bradlc",
"keywords": [
@@ -30,11 +31,12 @@ "engines": {
"vscode": "^1.33.0"
},
"categories": [
+ "Linters",
"Other"
],
"galleryBanner": {
+ "email": "hello@bradley.dev"
"displayName": "Tailwind CSS IntelliSense",
-{
},
"icon": "media/icon.png",
"activationEvents": [
@@ -72,6 +74,78 @@ "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.validate": {
+ "type": "boolean",
+ "default": true,
+ "markdownDescription": "Enable linting",
+ "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"
}
}
}
@@ -96,6 +170,7 @@ "callsite": "^1.0.0",
"chokidar": "^3.3.1",
"concurrently": "^5.1.0",
"css.escape": "^1.5.1",
+ "detect-indent": "^6.0.0",
"dlv": "^1.1.3",
"dset": "^2.0.1",
"esm": "^3.2.25",
@@ -114,6 +189,7 @@ "postcss-selector-parser": "^6.0.2",
"resolve-from": "^5.0.0",
"rimraf": "^3.0.2",
"semver": "^7.3.2",
+ "sift-string": "0.0.2",
"stack-trace": "0.0.10",
"terser": "^4.6.12",
"tiny-invariant": "^1.1.0",
diff --git a/src/class-names/index.js b/src/class-names/index.js
index ca8ddec1e8696085fcb79738f7bdb42fe08beb72..0ddd81c9b9ed00ff1a357129203ce973b23911c1 100644
--- a/src/class-names/index.js
+++ b/src/class-names/index.js
@@ -141,6 +141,10 @@ resolvedConfig,
postcss,
browserslist,
}),
+ modules: {
+ tailwindcss,
+ postcss,
+ },
}
}
diff --git a/src/extension.ts b/src/extension.ts
index be1fe479cac88456c5893e28a533d1c3c2ec81ca..279ec969590ba1e30120474c179722fa450c238d 100755
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -12,6 +12,8 @@ OutputChannel,
WorkspaceFolder,
Uri,
/* --------------------------------------------------------------------------------------------
+function getUserLanguages(folder?: WorkspaceFolder): Record<string, string> {
+/* --------------------------------------------------------------------------------------------
* ------------------------------------------------------------------------------------------ */
import {
LanguageClient,
@@ -23,6 +25,7 @@ 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'
const CLIENT_ID = 'tailwindcss-intellisense'
const CLIENT_NAME = 'Tailwind CSS IntelliSense'
@@ -151,6 +154,9 @@
client.onReady().then(() => {
let emitter = createEmitter(client)
registerConfigErrorHandler(emitter)
+ onMessage(client, 'getConfiguration', async (scope) => {
+ return Workspace.getConfiguration('tailwindCSS', scope)
+ })
})
client.start()
diff --git a/src/lib/emitter.ts b/src/lib/emitter.ts
index d177c2cf355a60b5c265fb5df12b01e0feb0ffb6..3ad6f05f010872359a4d0a2ef289456f37d74490 100644
--- a/src/lib/emitter.ts
+++ b/src/lib/emitter.ts
@@ -2,13 +2,17 @@ import mitt from 'mitt'
import { LanguageClient } from 'vscode-languageclient'
import crypto from 'crypto'
+import mitt from 'mitt'
+
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): NotificationEmitter {
+export function createEmitter(
+ client: LanguageClient | Connection
+): NotificationEmitter {
const emitter = mitt()
const registered: string[] = []
@@ -26,8 +30,8 @@ const off = (name: string, handler: (args: any) => void) => {
emitter.off(name, handler)
}
-import { LanguageClient } from 'vscode-languageclient'
+export interface NotificationEmitter {
return new Promise((resolve, _reject) => {
const id = crypto.randomBytes(16).toString('hex')
on(`${name}Response`, (result) => {
diff --git a/src/lsp/notifications.ts b/src/lsp/notifications.ts
index bb4e60dc496922fe1221147ff41a690e5f60d344..e8f4ab856ac0c1a8e4809e78761fb53640488742 100644
--- a/src/lsp/notifications.ts
+++ b/src/lsp/notifications.ts
@@ -1,9 +1,10 @@
import { Connection } from 'vscode-languageserver'
+import { LanguageClient } from 'vscode-languageclient'
export function onMessage(
- connection: Connection,
+ connection: LanguageClient | Connection,
name: string,
- handler: (params: any) => any
+ handler: (params: any) => Thenable<Record<string, any>>
): void {
connection.onNotification(`tailwindcss/${name}`, async (params: any) => {
const { _id, ...rest } = params
diff --git a/src/lsp/providers/codeActions/codeActionProvider.ts b/src/lsp/providers/codeActions/codeActionProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19420f68ca37f549bac335634c81d8b5d3119af1
--- /dev/null
+++ b/src/lsp/providers/codeActions/codeActionProvider.ts
@@ -0,0 +1,77 @@
+import { CodeAction, CodeActionParams } from 'vscode-languageserver'
+import { State } from '../../util/state'
+import { getDiagnostics } from '../diagnostics/diagnosticsProvider'
+import { rangesEqual } from '../../util/rangesEqual'
+import {
+ DiagnosticKind,
+ isInvalidApplyDiagnostic,
+ AugmentedDiagnostic,
+ isCssConflictDiagnostic,
+ isInvalidConfigPathDiagnostic,
+ isInvalidTailwindDirectiveDiagnostic,
+ isInvalidScreenDiagnostic,
+ isInvalidVariantDiagnostic,
+} from '../diagnostics/types'
+import { flatten, dedupeBy } from '../../../util/array'
+import { provideCssConflictCodeActions } from './provideCssConflictCodeActions'
+import { provideInvalidApplyCodeActions } from './provideInvalidApplyCodeActions'
+import { provideSuggestionCodeActions } from './provideSuggestionCodeActions'
+
+async function getDiagnosticsFromCodeActionParams(
+ state: State,
+ params: CodeActionParams,
+ only?: DiagnosticKind[]
+): Promise<AugmentedDiagnostic[]> {
+ let document = state.editor.documents.get(params.textDocument.uri)
+ let diagnostics = await getDiagnostics(state, document, only)
+
+ return params.context.diagnostics
+ .map((diagnostic) => {
+ return diagnostics.find((d) => {
+ return (
+ d.code === diagnostic.code &&
+ d.message === diagnostic.message &&
+ rangesEqual(d.range, diagnostic.range)
+ )
+ })
+ })
+ .filter(Boolean)
+}
+
+export async function provideCodeActions(
+ state: State,
+ params: CodeActionParams
+): Promise<CodeAction[]> {
+ let diagnostics = await getDiagnosticsFromCodeActionParams(
+ state,
+ params,
+ params.context.diagnostics
+ .map((diagnostic) => diagnostic.code)
+ .filter(Boolean) as DiagnosticKind[]
+ )
+
+ return Promise.all(
+ diagnostics.map((diagnostic) => {
+ if (isInvalidApplyDiagnostic(diagnostic)) {
+ return provideInvalidApplyCodeActions(state, params, diagnostic)
+ }
+
+ if (isCssConflictDiagnostic(diagnostic)) {
+ return provideCssConflictCodeActions(state, params, diagnostic)
+ }
+
+ if (
+ isInvalidConfigPathDiagnostic(diagnostic) ||
+ isInvalidTailwindDirectiveDiagnostic(diagnostic) ||
+ isInvalidScreenDiagnostic(diagnostic) ||
+ isInvalidVariantDiagnostic(diagnostic)
+ ) {
+ return provideSuggestionCodeActions(state, params, diagnostic)
+ }
+
+ return []
+ })
+ )
+ .then(flatten)
+ .then((x) => dedupeBy(x, (item) => JSON.stringify(item.edit)))
+}
diff --git a/src/lsp/providers/codeActions/provideCssConflictCodeActions.ts b/src/lsp/providers/codeActions/provideCssConflictCodeActions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f6a6edb159000f7e53aae3c88e6b7fef94d08b70
--- /dev/null
+++ b/src/lsp/providers/codeActions/provideCssConflictCodeActions.ts
@@ -0,0 +1,42 @@
+import { State } from '../../util/state'
+import {
+ CodeActionParams,
+ CodeAction,
+ CodeActionKind,
+} from 'vscode-languageserver'
+import { CssConflictDiagnostic } from '../diagnostics/types'
+import { joinWithAnd } from '../../util/joinWithAnd'
+import { removeRangesFromString } from '../../util/removeRangesFromString'
+
+export async function provideCssConflictCodeActions(
+ _state: State,
+ params: CodeActionParams,
+ diagnostic: CssConflictDiagnostic
+): Promise<CodeAction[]> {
+ return [
+ {
+ title: `Delete ${joinWithAnd(
+ diagnostic.otherClassNames.map(
+ (otherClassName) => `'${otherClassName.className}'`
+ )
+ )}`,
+ kind: CodeActionKind.QuickFix,
+ diagnostics: [diagnostic],
+ edit: {
+ changes: {
+ [params.textDocument.uri]: [
+ {
+ range: diagnostic.className.classList.range,
+ newText: removeRangesFromString(
+ diagnostic.className.classList.classList,
+ diagnostic.otherClassNames.map(
+ (otherClassName) => otherClassName.relativeRange
+ )
+ ),
+ },
+ ],
+ },
+ },
+ },
+ ]
+}
diff --git a/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts b/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ec3d483b6234cb1508465dfbdaabf9154173ff2a
--- /dev/null
+++ b/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts
@@ -0,0 +1,266 @@
+import {
+ CodeAction,
+ CodeActionParams,
+ CodeActionKind,
+ TextEdit,
+ Range,
+} from 'vscode-languageserver'
+import { State } from '../../util/state'
+import { InvalidApplyDiagnostic } from '../diagnostics/types'
+import { getClassNameParts } from '../../util/getClassNameAtPosition'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { isCssDoc } from '../../util/css'
+import { isWithinRange } from '../../util/isWithinRange'
+const dlv = require('dlv')
+import type { Root, NodeSource } from 'postcss'
+import { absoluteRange } from '../../util/absoluteRange'
+import { removeRangesFromString } from '../../util/removeRangesFromString'
+import detectIndent from 'detect-indent'
+import isObject from '../../../util/isObject'
+import { cssObjToAst } from '../../util/cssObjToAst'
+import dset from 'dset'
+import selectorParser from 'postcss-selector-parser'
+import { flatten } from '../../../util/array'
+import { getClassNameMeta } from '../../util/getClassNameMeta'
+import { validateApply } from '../../util/validateApply'
+
+export async function provideInvalidApplyCodeActions(
+ state: State,
+ params: CodeActionParams,
+ diagnostic: InvalidApplyDiagnostic
+): Promise<CodeAction[]> {
+ let document = state.editor.documents.get(params.textDocument.uri)
+ let documentText = document.getText()
+ let cssRange: Range
+ let cssText = documentText
+ const { postcss } = state.modules
+ let changes: TextEdit[] = []
+
+ let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
+ /\s+/
+ ).length
+
+ let className = diagnostic.className.className
+ let classNameParts = getClassNameParts(state, className)
+ let classNameInfo = dlv(state.classNames.classNames, classNameParts)
+
+ if (Array.isArray(classNameInfo)) {
+ return []
+ }
+
+ if (!isCssDoc(state, document)) {
+ let languageBoundaries = getLanguageBoundaries(state, document)
+ if (!languageBoundaries) return []
+ cssRange = languageBoundaries.css.find((range) =>
+ isWithinRange(diagnostic.range.start, range)
+ )
+ if (!cssRange) return []
+ cssText = document.getText(cssRange)
+ }
+
+ try {
+ await postcss([
+ postcss.plugin('', (_options = {}) => {
+ return (root: Root) => {
+ root.walkRules((rule) => {
+ if (changes.length) return false
+
+ rule.walkAtRules('apply', (atRule) => {
+ let atRuleRange = postcssSourceToRange(atRule.source)
+ if (cssRange) {
+ atRuleRange = absoluteRange(atRuleRange, cssRange)
+ }
+
+ if (!isWithinRange(diagnostic.range.start, atRuleRange))
+ return true
+
+ let ast = classNameToAst(
+ state,
+ classNameParts,
+ rule.selector,
+ diagnostic.className.classList.important
+ )
+
+ if (!ast) return false
+
+ rule.after(ast.nodes)
+ let insertedRule = rule.next()
+ if (!insertedRule) return false
+
+ if (totalClassNamesInClassList === 1) {
+ atRule.remove()
+ } else {
+ changes.push({
+ range: diagnostic.className.classList.range,
+ newText: removeRangesFromString(
+ diagnostic.className.classList.classList,
+ diagnostic.className.relativeRange
+ ),
+ })
+ }
+
+ let ruleRange = postcssSourceToRange(rule.source)
+ if (cssRange) {
+ ruleRange = absoluteRange(ruleRange, cssRange)
+ }
+
+ let outputIndent: string
+ let documentIndent = detectIndent(cssText)
+
+ changes.push({
+ range: ruleRange,
+ newText:
+ rule.toString() +
+ (insertedRule.raws.before || '\n\n') +
+ insertedRule
+ .toString()
+ .replace(/\n\s*\n/g, '\n')
+ .replace(/(@apply [^;\n]+)$/gm, '$1;')
+ .replace(/([^\s^]){$/gm, '$1 {')
+ .replace(/^\s+/gm, (m: string) => {
+ if (typeof outputIndent === 'undefined') outputIndent = m
+ return m.replace(
+ new RegExp(outputIndent, 'g'),
+ documentIndent.indent
+ )
+ })
+ .replace(/^(\s+)(.*?[^{}]\n)([^\s}])/gm, '$1$2$1$3'),
+ })
+
+ return false
+ })
+ })
+ }
+ }),
+ ]).process(cssText, { from: undefined })
+ } catch (_) {
+ return []
+ }
+
+ if (!changes.length) {
+ return []
+ }
+
+ return [
+ {
+ title: 'Extract to new rule',
+ kind: CodeActionKind.QuickFix,
+ diagnostics: [diagnostic],
+ edit: {
+ changes: {
+ [params.textDocument.uri]: changes,
+ },
+ },
+ },
+ ]
+}
+
+function postcssSourceToRange(source: NodeSource): Range {
+ return {
+ start: {
+ line: source.start.line - 1,
+ character: source.start.column - 1,
+ },
+ end: {
+ line: source.end.line - 1,
+ character: source.end.column,
+ },
+ }
+}
+
+function classNameToAst(
+ state: State,
+ classNameParts: string[],
+ selector: string,
+ important: boolean = false
+) {
+ const baseClassName = classNameParts[classNameParts.length - 1]
+ const validatedBaseClassName = validateApply(state, [baseClassName])
+ if (
+ validatedBaseClassName === null ||
+ validatedBaseClassName.isApplyable === false
+ ) {
+ return null
+ }
+ const meta = getClassNameMeta(state, classNameParts)
+ if (Array.isArray(meta)) return null
+ let context = meta.context
+ let pseudo = meta.pseudo
+ const globalContexts = state.classNames.context
+ let screens = dlv(
+ state.config,
+ 'theme.screens',
+ dlv(state.config, 'screens', {})
+ )
+ if (!isObject(screens)) screens = {}
+ screens = Object.keys(screens)
+ const path = []
+
+ for (let i = 0; i < classNameParts.length - 1; i++) {
+ let part = classNameParts[i]
+ let common = globalContexts[part]
+ if (!common) return null
+ if (screens.includes(part)) {
+ path.push(`@screen ${part}`)
+ context = context.filter((con) => !common.includes(con))
+ }
+ }
+
+ path.push(...context)
+
+ let obj = {}
+ for (let i = 1; i <= path.length; i++) {
+ dset(obj, path.slice(0, i), {})
+ }
+
+ selector = appendPseudosToSelector(selector, pseudo)
+ if (selector === null) return null
+
+ let rule = {
+ [selector]: {
+ [`@apply ${baseClassName}${important ? ' !important' : ''}`]: '',
+ },
+ }
+ if (path.length) {
+ dset(obj, path, rule)
+ } else {
+ obj = rule
+ }
+
+ return cssObjToAst(obj, state.modules.postcss)
+}
+
+function appendPseudosToSelector(
+ selector: string,
+ pseudos: string[]
+): string | null {
+ if (pseudos.length === 0) return selector
+
+ let canTransform = true
+
+ let transformedSelector = selectorParser((selectors) => {
+ flatten(selectors.split((_) => true)).forEach((sel) => {
+ // @ts-ignore
+ for (let i = sel.nodes.length - 1; i >= 0; i--) {
+ // @ts-ignore
+ if (sel.nodes[i].type !== 'pseudo') {
+ break
+ // @ts-ignore
+ } else if (pseudos.includes(sel.nodes[i].value)) {
+ canTransform = false
+ break
+ }
+ }
+ if (canTransform) {
+ pseudos.forEach((p) => {
+ // @ts-ignore
+ sel.append(selectorParser.pseudo({ value: p }))
+ })
+ }
+ })
+ }).processSync(selector)
+
+ if (!canTransform) return null
+
+ return transformedSelector
+}
diff --git a/src/lsp/providers/codeActions/provideSuggestionCodeActions.ts b/src/lsp/providers/codeActions/provideSuggestionCodeActions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9da5fcb58f4863d4f6edc2643319263f95c22aa6
--- /dev/null
+++ b/src/lsp/providers/codeActions/provideSuggestionCodeActions.ts
@@ -0,0 +1,38 @@
+import { State } from '../../util/state'
+import {
+ CodeActionParams,
+ CodeAction,
+ CodeActionKind,
+} from 'vscode-languageserver'
+import {
+ InvalidConfigPathDiagnostic,
+ InvalidTailwindDirectiveDiagnostic,
+ InvalidScreenDiagnostic,
+ InvalidVariantDiagnostic,
+} from '../diagnostics/types'
+
+export function provideSuggestionCodeActions(
+ _state: State,
+ params: CodeActionParams,
+ diagnostic:
+ | InvalidConfigPathDiagnostic
+ | InvalidTailwindDirectiveDiagnostic
+ | InvalidScreenDiagnostic
+ | InvalidVariantDiagnostic
+): CodeAction[] {
+ return diagnostic.suggestions.map((suggestion) => ({
+ title: `Replace with '${suggestion}'`,
+ kind: CodeActionKind.QuickFix,
+ diagnostics: [diagnostic],
+ edit: {
+ changes: {
+ [params.textDocument.uri]: [
+ {
+ range: diagnostic.range,
+ newText: suggestion,
+ },
+ ],
+ },
+ },
+ }))
+}
diff --git a/src/lsp/providers/completionProvider.ts b/src/lsp/providers/completionProvider.ts
index 6dbb1e7a8de391c327c00dee9b74fb0b1fe08c87..ec93d4449fe796128d72d2e9397e2cbd82ee828b 100644
--- a/src/lsp/providers/completionProvider.ts
+++ b/src/lsp/providers/completionProvider.ts
@@ -618,12 +618,11 @@ async function provideEmmetCompletions(
state: State,
{ position, textDocument }: CompletionParams
): Promise<CompletionList> {
+ let doc = state.editor.documents.get(textDocument.uri)
CompletionItem,
CompletionItemKind,
- CompletionItem,
+} from '../util/lexers'
if (settings.emmetCompletions !== true) return null
-
- let doc = state.editor.documents.get(textDocument.uri)
const syntax = isHtmlContext(state, doc, position)
? 'html'
diff --git a/src/lsp/providers/diagnostics/diagnosticsProvider.ts b/src/lsp/providers/diagnostics/diagnosticsProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a22f8460e8c1eef13c3db52052b37d051a58797a
--- /dev/null
+++ b/src/lsp/providers/diagnostics/diagnosticsProvider.ts
@@ -0,0 +1,74 @@
+import { 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'
+import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics'
+import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics'
+import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics'
+import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics'
+
+export async function getDiagnostics(
+ state: State,
+ document: TextDocument,
+ only: DiagnosticKind[] = [
+ DiagnosticKind.CssConflict,
+ DiagnosticKind.InvalidApply,
+ DiagnosticKind.InvalidScreen,
+ DiagnosticKind.InvalidVariant,
+ DiagnosticKind.InvalidConfigPath,
+ DiagnosticKind.InvalidTailwindDirective,
+ ]
+): Promise<AugmentedDiagnostic[]> {
+ const settings = await getDocumentSettings(state, document)
+
+ return settings.validate
+ ? [
+ ...(only.includes(DiagnosticKind.CssConflict)
+ ? getCssConflictDiagnostics(state, document, settings)
+ : []),
+ ...(only.includes(DiagnosticKind.InvalidApply)
+ ? getInvalidApplyDiagnostics(state, document, settings)
+ : []),
+ ...(only.includes(DiagnosticKind.InvalidScreen)
+ ? getInvalidScreenDiagnostics(state, document, settings)
+ : []),
+ ...(only.includes(DiagnosticKind.InvalidVariant)
+ ? getInvalidVariantDiagnostics(state, document, settings)
+ : []),
+ ...(only.includes(DiagnosticKind.InvalidConfigPath)
+ ? getInvalidConfigPathDiagnostics(state, document, settings)
+ : []),
+ ...(only.includes(DiagnosticKind.InvalidTailwindDirective)
+ ? getInvalidTailwindDirectiveDiagnostics(state, document, settings)
+ : []),
+ ]
+ : []
+}
+
+export async function provideDiagnostics(state: State, document: TextDocument) {
+ state.editor.connection.sendDiagnostics({
+ uri: document.uri,
+ diagnostics: await getDiagnostics(state, document),
+ })
+}
+
+export function clearDiagnostics(state: State, document: TextDocument): void {
+ state.editor.connection.sendDiagnostics({
+ uri: document.uri,
+ diagnostics: [],
+ })
+}
+
+export function clearAllDiagnostics(state: State): void {
+ state.editor.documents.all().forEach((document) => {
+ clearDiagnostics(state, document)
+ })
+}
+
+export function updateAllDiagnostics(state: State): void {
+ state.editor.documents.all().forEach((document) => {
+ provideDiagnostics(state, document)
+ })
+}
diff --git a/src/lsp/providers/diagnostics/getCssConflictDiagnostics.ts b/src/lsp/providers/diagnostics/getCssConflictDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..73da4869da22efd201be30ab434b9f61a0a5b3b6
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getCssConflictDiagnostics.ts
@@ -0,0 +1,85 @@
+import { joinWithAnd } from '../../util/joinWithAnd'
+import { State, Settings } from '../../util/state'
+import { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
+import { CssConflictDiagnostic, DiagnosticKind } from './types'
+import {
+ findClassListsInDocument,
+ getClassNamesInClassList,
+} from '../../util/find'
+import { getClassNameDecls } from '../../util/getClassNameDecls'
+import { getClassNameMeta } from '../../util/getClassNameMeta'
+import { equal } from '../../../util/array'
+
+export function getCssConflictDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): CssConflictDiagnostic[] {
+ let severity = settings.lint.cssConflict
+ if (severity === 'ignore') return []
+
+ let diagnostics: CssConflictDiagnostic[] = []
+ const classLists = findClassListsInDocument(state, document)
+
+ classLists.forEach((classList) => {
+ const classNames = getClassNamesInClassList(classList)
+
+ classNames.forEach((className, index) => {
+ let decls = getClassNameDecls(state, className.className)
+ if (!decls) return
+
+ let properties = Object.keys(decls)
+ let meta = getClassNameMeta(state, className.className)
+
+ let otherClassNames = classNames.filter((_className, i) => i !== index)
+
+ let conflictingClassNames = otherClassNames.filter((otherClassName) => {
+ let otherDecls = getClassNameDecls(state, otherClassName.className)
+ if (!otherDecls) return false
+
+ let otherMeta = getClassNameMeta(state, otherClassName.className)
+
+ return (
+ equal(properties, Object.keys(otherDecls)) &&
+ !Array.isArray(meta) &&
+ !Array.isArray(otherMeta) &&
+ equal(meta.context, otherMeta.context) &&
+ equal(meta.pseudo, otherMeta.pseudo)
+ )
+ })
+
+ if (conflictingClassNames.length === 0) return
+
+ diagnostics.push({
+ code: DiagnosticKind.CssConflict,
+ className,
+ otherClassNames: conflictingClassNames,
+ range: className.range,
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : 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 diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e829b5735e74900f08d5d7161c5d561eebd7b89f
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts
@@ -0,0 +1,37 @@
+import { findClassNamesInRange } from '../../util/find'
+import { InvalidApplyDiagnostic, DiagnosticKind } from './types'
+import { Settings, State } from '../../util/state'
+import { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
+import { validateApply } from '../../util/validateApply'
+
+export function getInvalidApplyDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidApplyDiagnostic[] {
+ let severity = settings.lint.invalidApply
+ if (severity === 'ignore') return []
+
+ const classNames = findClassNamesInRange(document, undefined, 'css')
+
+ let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => {
+ let result = validateApply(state, className.className)
+
+ if (result === null || result.isApplyable === true) {
+ return null
+ }
+
+ return {
+ code: DiagnosticKind.InvalidApply,
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ range: className.range,
+ message: result.reason,
+ className,
+ }
+ })
+
+ return diagnostics.filter(Boolean)
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ccf52fcb1aa3b88bd1b73aa9963e0e7729b29cca
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts
@@ -0,0 +1,230 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidConfigPathDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import { stringToPath } from '../../util/stringToPath'
+import isObject from '../../../util/isObject'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+import { combinations } from '../../util/combinations'
+const dlv = require('dlv')
+
+function pathToString(path: string | string[]): string {
+ if (typeof path === 'string') return path
+ return path.reduce((acc, cur, i) => {
+ if (i === 0) return cur
+ if (cur.includes('.')) return `${acc}[${cur}]`
+ return `${acc}.${cur}`
+ }, '')
+}
+
+function validateConfigPath(
+ state: State,
+ path: string | string[],
+ base: string[] = []
+):
+ | { isValid: true; value: any }
+ | { isValid: false; reason: string; suggestions: string[] } {
+ let keys = Array.isArray(path) ? path : stringToPath(path)
+ let value = dlv(state.config, [...base, ...keys])
+ let suggestions: string[] = []
+
+ function findAlternativePath(): string[] {
+ let points = combinations('123456789'.substr(0, keys.length - 1)).map((x) =>
+ x.split('').map((x) => parseInt(x, 10))
+ )
+
+ let possibilities: string[][] = points
+ .map((p) => {
+ let result = []
+ let i = 0
+ p.forEach((x) => {
+ result.push(keys.slice(i, x).join('.'))
+ i = x
+ })
+ result.push(keys.slice(i).join('.'))
+ return result
+ })
+ .slice(1) // skip original path
+
+ return possibilities.find(
+ (possibility) => validateConfigPath(state, possibility, base).isValid
+ )
+ }
+
+ if (typeof value === 'undefined') {
+ let reason = `'${pathToString(path)}' does not exist in your theme config.`
+ let parentPath = [...base, ...keys.slice(0, keys.length - 1)]
+ let parentValue = dlv(state.config, parentPath)
+
+ if (isObject(parentValue)) {
+ let closestValidKey = closest(
+ keys[keys.length - 1],
+ Object.keys(parentValue).filter(
+ (key) => validateConfigPath(state, [...parentPath, key]).isValid
+ )
+ )
+ if (closestValidKey) {
+ suggestions.push(
+ pathToString([...keys.slice(0, keys.length - 1), closestValidKey])
+ )
+ reason += ` Did you mean '${suggestions[0]}'?`
+ }
+ } else {
+ let altPath = findAlternativePath()
+ if (altPath) {
+ return {
+ isValid: false,
+ reason: `${reason} Did you mean '${pathToString(altPath)}'?`,
+ suggestions: [pathToString(altPath)],
+ }
+ }
+ }
+
+ return {
+ isValid: false,
+ reason,
+ suggestions,
+ }
+ }
+
+ if (
+ !(
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ value instanceof String ||
+ value instanceof Number ||
+ Array.isArray(value)
+ )
+ ) {
+ let reason = `'${pathToString(
+ path
+ )}' was found but does not resolve to a string.`
+
+ if (isObject(value)) {
+ let validKeys = Object.keys(value).filter(
+ (key) => validateConfigPath(state, [...keys, key], base).isValid
+ )
+ if (validKeys.length) {
+ suggestions.push(
+ ...validKeys.map((validKey) => pathToString([...keys, validKey]))
+ )
+ reason += ` Did you mean something like '${suggestions[0]}'?`
+ }
+ }
+ return {
+ isValid: false,
+ reason,
+ suggestions,
+ }
+ }
+
+ // The value resolves successfully, but we need to check that there
+ // wasn't any funny business. If you have a theme object:
+ // { msg: 'hello' } and do theme('msg.0')
+ // this will resolve to 'h', which is probably not intentional, so we
+ // check that all of the keys are object or array keys (i.e. not string
+ // indexes)
+ let isValid = true
+ for (let i = keys.length - 1; i >= 0; i--) {
+ let key = keys[i]
+ let parentValue = dlv(state.config, [...base, ...keys.slice(0, i)])
+ if (/^[0-9]+$/.test(key)) {
+ if (!isObject(parentValue) && !Array.isArray(parentValue)) {
+ isValid = false
+ break
+ }
+ } else if (!isObject(parentValue)) {
+ isValid = false
+ break
+ }
+ }
+ if (!isValid) {
+ let reason = `'${pathToString(path)}' does not exist in your theme config.`
+
+ let altPath = findAlternativePath()
+ if (altPath) {
+ return {
+ isValid: false,
+ reason: `${reason} Did you mean '${pathToString(altPath)}'?`,
+ suggestions: [pathToString(altPath)],
+ }
+ }
+
+ return {
+ isValid: false,
+ reason,
+ suggestions: [],
+ }
+ }
+
+ return {
+ isValid: true,
+ value,
+ }
+}
+
+export function getInvalidConfigPathDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidConfigPathDiagnostic[] {
+ let severity = settings.lint.invalidConfigPath
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidConfigPathDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(
+ /(?<prefix>\s|^)(?<helper>config|theme)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/g,
+ text
+ )
+
+ matches.forEach((match) => {
+ let base = match.groups.helper === 'theme' ? ['theme'] : []
+ let result = validateConfigPath(state, match.groups.key, base)
+
+ if (result.isValid === true) {
+ return null
+ }
+
+ let startIndex =
+ match.index +
+ match.groups.prefix.length +
+ match.groups.helper.length +
+ 1 + // open paren
+ match.groups.quote.length
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidConfigPath,
+ range: absoluteRange(
+ {
+ start: indexToPosition(text, startIndex),
+ end: indexToPosition(text, startIndex + match.groups.key.length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message: result.reason,
+ suggestions: result.suggestions,
+ })
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0e4252438ea6bb7dd47071189c3881aae219225
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts
@@ -0,0 +1,75 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidScreenDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+const dlv = require('dlv')
+
+export function getInvalidScreenDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidScreenDiagnostic[] {
+ let severity = settings.lint.invalidScreen
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidScreenDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(/(?:\s|^)@screen\s+(?<screen>[^\s{]+)/g, text)
+
+ let screens = Object.keys(
+ dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
+ )
+
+ matches.forEach((match) => {
+ if (screens.includes(match.groups.screen)) {
+ return null
+ }
+
+ let message = `The screen '${match.groups.screen}' does not exist in your theme config.`
+ let suggestions: string[] = []
+ let suggestion = closest(match.groups.screen, screens)
+
+ if (suggestion) {
+ suggestions.push(suggestion)
+ message += ` Did you mean '${suggestion}'?`
+ }
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidScreen,
+ range: absoluteRange(
+ {
+ start: indexToPosition(
+ text,
+ match.index + match[0].length - match.groups.screen.length
+ ),
+ end: indexToPosition(text, match.index + match[0].length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message,
+ suggestions,
+ })
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9b88bdb06e0e5c3754c6c5eecbf23af7c342a21d
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts
@@ -0,0 +1,83 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidTailwindDirectiveDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import semver from 'semver'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+
+export function getInvalidTailwindDirectiveDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidTailwindDirectiveDiagnostic[] {
+ let severity = settings.lint.invalidTailwindDirective
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidTailwindDirectiveDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(/(?:\s|^)@tailwind\s+(?<value>[^;]+)/g, text)
+
+ let valid = [
+ 'utilities',
+ 'components',
+ 'screens',
+ semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight',
+ ]
+
+ matches.forEach((match) => {
+ if (valid.includes(match.groups.value)) {
+ return null
+ }
+
+ let message = `'${match.groups.value}' is not a valid group.`
+ let suggestions: string[] = []
+
+ if (match.groups.value === 'preflight') {
+ suggestions.push('base')
+ message += ` Did you mean 'base'?`
+ } else {
+ let suggestion = closest(match.groups.value, valid)
+ if (suggestion) {
+ suggestions.push(suggestion)
+ message += ` Did you mean '${suggestion}'?`
+ }
+ }
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidTailwindDirective,
+ range: absoluteRange(
+ {
+ start: indexToPosition(
+ text,
+ match.index + match[0].length - match.groups.value.length
+ ),
+ end: indexToPosition(text, match.index + match[0].length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message,
+ suggestions,
+ })
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..006740103c2776d4d68d6452dd5f808b8100a5d2
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts
@@ -0,0 +1,77 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidVariantDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+
+export function getInvalidVariantDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidVariantDiagnostic[] {
+ let severity = settings.lint.invalidVariant
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidVariantDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(/(?:\s|^)@variants\s+(?<variants>[^{]+)/g, text)
+
+ matches.forEach((match) => {
+ let variants = match.groups.variants.split(/(\s*,\s*)/)
+ let listStartIndex =
+ match.index + match[0].length - match.groups.variants.length
+
+ for (let i = 0; i < variants.length; i += 2) {
+ let variant = variants[i].trim()
+ if (state.variants.includes(variant)) {
+ continue
+ }
+
+ let message = `The variant '${variant}' does not exist.`
+ let suggestions: string[] = []
+ let suggestion = closest(variant, state.variants)
+
+ if (suggestion) {
+ suggestions.push(suggestion)
+ message += ` Did you mean '${suggestion}'?`
+ }
+
+ let variantStartIndex =
+ listStartIndex + variants.slice(0, i).join('').length
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidVariant,
+ range: absoluteRange(
+ {
+ start: indexToPosition(text, variantStartIndex),
+ end: indexToPosition(text, variantStartIndex + variant.length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message,
+ suggestions,
+ })
+ }
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/types.ts b/src/lsp/providers/diagnostics/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1cfd0e2b4d4b26b543776be2c4810661b799f6c2
--- /dev/null
+++ b/src/lsp/providers/diagnostics/types.ts
@@ -0,0 +1,86 @@
+import { Diagnostic } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from '../../util/state'
+
+export enum DiagnosticKind {
+ CssConflict = 'cssConflict',
+ InvalidApply = 'invalidApply',
+ InvalidScreen = 'invalidScreen',
+ InvalidVariant = 'invalidVariant',
+ InvalidConfigPath = 'invalidConfigPath',
+ InvalidTailwindDirective = 'invalidTailwindDirective',
+}
+
+export type CssConflictDiagnostic = Diagnostic & {
+ code: DiagnosticKind.CssConflict
+ className: DocumentClassName
+ otherClassNames: DocumentClassName[]
+}
+
+export function isCssConflictDiagnostic(
+ diagnostic: AugmentedDiagnostic
+): diagnostic is CssConflictDiagnostic {
+ return diagnostic.code === DiagnosticKind.CssConflict
+}
+
+export type InvalidApplyDiagnostic = Diagnostic & {
+ code: DiagnosticKind.InvalidApply
+ className: DocumentClassName
+}
+
+export function isInvalidApplyDiagnostic(
+ diagnostic: AugmentedDiagnostic
+): diagnostic is InvalidApplyDiagnostic {
+ return diagnostic.code === DiagnosticKind.InvalidApply
+}
+
+export type InvalidScreenDiagnostic = Diagnostic & {
+ code: DiagnosticKind.InvalidScreen
+ suggestions: string[]
+}
+
+export function isInvalidScreenDiagnostic(
+ diagnostic: AugmentedDiagnostic
+): diagnostic is InvalidScreenDiagnostic {
+ return diagnostic.code === DiagnosticKind.InvalidScreen
+}
+
+export type InvalidVariantDiagnostic = Diagnostic & {
+ code: DiagnosticKind.InvalidVariant
+ suggestions: string[]
+}
+
+export function isInvalidVariantDiagnostic(
+ diagnostic: AugmentedDiagnostic
+): diagnostic is InvalidVariantDiagnostic {
+ return diagnostic.code === DiagnosticKind.InvalidVariant
+}
+
+export type InvalidConfigPathDiagnostic = Diagnostic & {
+ code: DiagnosticKind.InvalidConfigPath
+ suggestions: string[]
+}
+
+export function isInvalidConfigPathDiagnostic(
+ diagnostic: AugmentedDiagnostic
+): diagnostic is InvalidConfigPathDiagnostic {
+ return diagnostic.code === DiagnosticKind.InvalidConfigPath
+}
+
+export type InvalidTailwindDirectiveDiagnostic = Diagnostic & {
+ code: DiagnosticKind.InvalidTailwindDirective
+ suggestions: string[]
+}
+
+export function isInvalidTailwindDirectiveDiagnostic(
+ diagnostic: AugmentedDiagnostic
+): diagnostic is InvalidTailwindDirectiveDiagnostic {
+ return diagnostic.code === DiagnosticKind.InvalidTailwindDirective
+}
+
+export type AugmentedDiagnostic =
+ | CssConflictDiagnostic
+ | InvalidApplyDiagnostic
+ | InvalidScreenDiagnostic
+ | InvalidVariantDiagnostic
+ | InvalidConfigPathDiagnostic
+ | InvalidTailwindDirectiveDiagnostic
diff --git a/src/lsp/providers/hoverProvider.ts b/src/lsp/providers/hoverProvider.ts
index a9010a3142cb4e9700f63e3bd06426a144b9afa0..2084ec42933cbf2cfc02ead3b476698727a2d89e 100644
--- a/src/lsp/providers/hoverProvider.ts
+++ b/src/lsp/providers/hoverProvider.ts
@@ -1,10 +1,11 @@
import { State } from '../util/state'
import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
-import { getClassNameParts } from '../util/getClassNameAtPosition'
import { stringifyCss, stringifyConfigValue } from '../util/stringify'
const dlv = require('dlv')
import { isCssContext } from '../util/css'
import { findClassNameAtPosition } from '../util/find'
+import { validateApply } from '../util/validateApply'
+import { getClassNameParts } from '../util/getClassNameAtPosition'
export function provideHover(
state: State,
@@ -80,6 +81,13 @@ if (className === null) return null
const parts = getClassNameParts(state, className.className)
if (!parts) return null
+
+ if (isCssContext(state, doc, position)) {
+ let validated = validateApply(state, parts)
+ if (validated === null || validated.isApplyable === false) {
+ return null
+ }
+ }
return {
contents: {
diff --git a/src/lsp/server.ts b/src/lsp/server.ts
index b28b1bad9d226222f5d9e24472c9d8d698df6187..cc85ae4662a1e6cbc4a8af47df121dd7acdb8f31 100644
--- a/src/lsp/server.ts
+++ b/src/lsp/server.ts
@@ -17,6 +17,9 @@ Hover,
TextDocumentPositionParams,
DidChangeConfigurationNotification,
/* --------------------------------------------------------------------------------------------
+ capabilities.workspace && !!capabilities.workspace.configuration,
+ CodeAction,
+/* --------------------------------------------------------------------------------------------
ProposedFeatures,
import getTailwindState from '../class-names/index'
import { State, Settings, EditorState } from './util/state'
@@ -27,25 +30,45 @@ } from './providers/completionProvider'
import { provideHover } from './providers/hoverProvider'
import { URI } from 'vscode-uri'
import { getDocumentSettings } from './util/getDocumentSettings'
+import {
+ provideDiagnostics,
+ updateAllDiagnostics,
+ clearAllDiagnostics,
+} from './providers/diagnostics/diagnosticsProvider'
+ Hover,
-let state: State = { enabled: false }
+import { provideCodeActions } from './providers/codeActions/codeActionProvider'
+
let connection = createConnection(ProposedFeatures.all)
+let state: State = { enabled: false, emitter: createEmitter(connection) }
let documents = new TextDocuments()
let workspaceFolder: string | null
const defaultSettings: Settings = {
emmetCompletions: false,
includeLanguages: {},
+ 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.onDidOpen((event) => {
- getDocumentSettings(state, event.document.uri)
+ getDocumentSettings(state, event.document)
})
documents.onDidClose((event) => {
documentSettings.delete(event.document.uri)
})
+documents.onDidChangeContent((change) => {
+ provideDiagnostics(state, change.document)
+})
documents.listen(connection)
connection.onInitialize(
@@ -65,6 +88,10 @@ : {},
capabilities: {
configuration:
capabilities.workspace && !!capabilities.workspace.configuration,
+ diagnosticRelatedInformation:
+ capabilities.textDocument &&
+ capabilities.textDocument.publishDiagnostics &&
+ capabilities.textDocument.publishDiagnostics.relatedInformation,
},
}
@@ -74,16 +101,27 @@ {
// @ts-ignore
onChange: (newState: State): void => {
if (newState && !newState.error) {
+ state = {
+ ...newState,
+ DidChangeConfigurationNotification,
import {
+ emitter: state.emitter,
+ DidChangeConfigurationNotification,
TextDocuments,
+ }
connection.sendNotification('tailwindcss/configUpdated', [
state.configPath,
state.config,
state.plugins,
])
+ updateAllDiagnostics(state)
} else {
+ state = {
+ enabled: false,
+ DidChangeConfigurationNotification,
createConnection,
-
+ editor: editorState,
+ }
if (newState && newState.error) {
const payload: {
message: string
@@ -98,6 +136,7 @@ payload.line = parseInt(match.groups.line, 10)
}
connection.sendNotification('tailwindcss/configError', [payload])
}
+ clearAllDiagnostics(state)
// TODO
// connection.sendNotification('tailwindcss/configUpdated', [null])
}
@@ -106,12 +145,22 @@ }
)
if (tailwindState) {
+/* --------------------------------------------------------------------------------------------
ProposedFeatures,
- createConnection,
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+/* --------------------------------------------------------------------------------------------
ProposedFeatures,
- TextDocuments,
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+/* --------------------------------------------------------------------------------------------
ProposedFeatures,
+ * ------------------------------------------------------------------------------------------ */
+/* --------------------------------------------------------------------------------------------
ProposedFeatures,
+
+ ...tailwindState,
+ }
+ } else {
+ state = { enabled: false, emitter: state.emitter, editor: editorState }
}
return {
@@ -139,6 +188,7 @@ typeof state.separator === 'undefined' ? ':' : state.separator,
],
},
hoverProvider: true,
+ codeActionProvider: true,
},
}
}
@@ -171,9 +221,7 @@ )
}
/* --------------------------------------------------------------------------------------------
- async (params: InitializeParams): Promise<InitializeResult> => {
- .all()
- .forEach((doc) => getDocumentSettings(state, doc.uri))
+ state = { enabled: false, editor: editorState }
})
connection.onCompletion(
@@ -194,6 +242,13 @@ connection.onHover(
(params: TextDocumentPositionParams): Hover => {
if (!state.enabled) return null
return provideHover(state, params)
+ }
+)
+
+connection.onCodeAction(
+ (params: CodeActionParams): Promise<CodeAction[]> => {
+ if (!state.enabled) return null
+ return provideCodeActions(state, params)
}
)
diff --git a/src/lsp/util/absoluteRange.ts b/src/lsp/util/absoluteRange.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9250e4fb9ec9b13ff3124072808f2013ef5009a0
--- /dev/null
+++ b/src/lsp/util/absoluteRange.ts
@@ -0,0 +1,18 @@
+import { Range } from 'vscode-languageserver'
+
+export function absoluteRange(range: Range, reference?: Range) {
+ return {
+ start: {
+ line: (reference?.start.line || 0) + range.start.line,
+ character:
+ (range.end.line === 0 ? reference?.start.character || 0 : 0) +
+ range.start.character,
+ },
+ end: {
+ line: (reference?.start.line || 0) + range.end.line,
+ character:
+ (range.end.line === 0 ? reference?.start.character || 0 : 0) +
+ range.end.character,
+ },
+ }
+}
diff --git a/src/lsp/util/closest.ts b/src/lsp/util/closest.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ebdfacc270c2c5467d501b4ff4642aef04f6277e
--- /dev/null
+++ b/src/lsp/util/closest.ts
@@ -0,0 +1,5 @@
+import sift from 'sift-string'
+
+export function closest(input: string, options: string[]): string | undefined {
+ return options.concat([]).sort((a, b) => sift(input, a) - sift(input, b))[0]
+}
diff --git a/src/lsp/util/combinations.ts b/src/lsp/util/combinations.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2c9868b207b01b60cc0789b800058285257e187a
--- /dev/null
+++ b/src/lsp/util/combinations.ts
@@ -0,0 +1,13 @@
+export function combinations(str: string): string[] {
+ let fn = function (active: string, rest: string, a: string[]) {
+ if (!active && !rest) return
+ if (!rest) {
+ a.push(active)
+ } else {
+ fn(active + rest[0], rest.slice(1), a)
+ fn(active, rest.slice(1), a)
+ }
+ return a
+ }
+ return fn('', str, [])
+}
diff --git a/src/lsp/util/cssObjToAst.ts b/src/lsp/util/cssObjToAst.ts
new file mode 100644
index 0000000000000000000000000000000000000000..42826f7526c514588519a63a8d2e6723a9ebfdd2
--- /dev/null
+++ b/src/lsp/util/cssObjToAst.ts
@@ -0,0 +1,127 @@
+/*
+This is a modified version of the postcss-js 'parse' function which accepts the
+postcss module as an argument. License below:
+
+The MIT License (MIT)
+
+Copyright 2015 Andrey Sitnik <andrey@sitnik.ru>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+var IMPORTANT = /\s*!important\s*$/i
+
+var unitless = {
+ 'box-flex': true,
+ 'box-flex-group': true,
+ 'column-count': true,
+ flex: true,
+ 'flex-grow': true,
+ 'flex-positive': true,
+ 'flex-shrink': true,
+ 'flex-negative': true,
+ 'font-weight': true,
+ 'line-clamp': true,
+ 'line-height': true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ 'tab-size': true,
+ widows: true,
+ 'z-index': true,
+ zoom: true,
+ 'fill-opacity': true,
+ 'stroke-dashoffset': true,
+ 'stroke-opacity': true,
+ 'stroke-width': true,
+}
+
+function dashify(str) {
+ return str
+ .replace(/([A-Z])/g, '-$1')
+ .replace(/^ms-/, '-ms-')
+ .toLowerCase()
+}
+
+function decl(parent, name, value, postcss) {
+ if (value === false || value === null) return
+
+ name = dashify(name)
+ if (typeof value === 'number') {
+ if (value === 0 || unitless[name]) {
+ value = value.toString()
+ } else {
+ value = value.toString() + 'px'
+ }
+ }
+
+ if (name === 'css-float') name = 'float'
+
+ if (IMPORTANT.test(value)) {
+ value = value.replace(IMPORTANT, '')
+ parent.push(postcss.decl({ prop: name, value: value, important: true }))
+ } else {
+ parent.push(postcss.decl({ prop: name, value: value }))
+ }
+}
+
+function atRule(parent, parts, value, postcss) {
+ var node = postcss.atRule({ name: parts[1], params: parts[3] || '' })
+ if (typeof value === 'object') {
+ node.nodes = []
+ parse(value, node, postcss)
+ }
+ parent.push(node)
+}
+
+function parse(obj, parent, postcss) {
+ var name, value, node, i
+ for (name in obj) {
+ if (obj.hasOwnProperty(name)) {
+ value = obj[name]
+ if (value === null || typeof value === 'undefined') {
+ continue
+ } else if (name[0] === '@') {
+ var parts = name.match(/@([^\s]+)(\s+([\w\W]*)\s*)?/)
+ if (Array.isArray(value)) {
+ for (i = 0; i < value.length; i++) {
+ atRule(parent, parts, value[i], postcss)
+ }
+ } else {
+ atRule(parent, parts, value, postcss)
+ }
+ } else if (Array.isArray(value)) {
+ for (i = 0; i < value.length; i++) {
+ decl(parent, name, value[i], postcss)
+ }
+ } else if (typeof value === 'object') {
+ node = postcss.rule({ selector: name })
+ parse(value, node, postcss)
+ parent.push(node)
+ } else {
+ decl(parent, name, value, postcss)
+ }
+ }
+ }
+}
+
+export function cssObjToAst(obj, postcss) {
+ var root = postcss.root()
+ parse(obj, root, postcss)
+ return root
+}
diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts
index 5eaef42ef1b5c41b8eeef529d00fb4109cd481bd..b642534d012dc3816516f0284de71b1ecbf31954 100644
--- a/src/lsp/util/find.ts
+++ b/src/lsp/util/find.ts
@@ -5,10 +5,12 @@ import { isCssContext, isCssDoc } from './css'
import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
import { isWithinRange } from './isWithinRange'
import { isJsContext, isJsDoc } from './js'
+import { flatten } from '../../util/array'
import {
getClassAttributeLexer,
getComputedClassAttributeLexer,
} from './lexers'
+import { getLanguageBoundaries } from './getLanguageBoundaries'
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
let match: RegExpMatchArray
@@ -28,49 +30,104 @@ return matches[matches.length - 1]
}
import { DocumentClassName, DocumentClassList, State } from './state'
+ return null
+ classList,
+ const matches = findAll(re, str)
import { isWithinRange } from './isWithinRange'
+import { DocumentClassName, DocumentClassList, State } from './state'
doc: TextDocument,
import { DocumentClassName, DocumentClassList, State } from './state'
+import { DocumentClassName, DocumentClassList, State } from './state'
import {
+import { DocumentClassName, DocumentClassList, State } from './state'
mode?: 'html' | 'css'
+import { DocumentClassName, DocumentClassList, State } from './state'
): DocumentClassName[] {
+import { DocumentClassName, DocumentClassList, State } from './state'
const classLists = findClassListsInRange(doc, range, mode)
+import { DocumentClassName, DocumentClassList, State } from './state'
return [].concat.apply(
+import { DocumentClassName, DocumentClassList, State } from './state'
[],
+import { DocumentClassName, DocumentClassList, State } from './state'
classLists.map(({ classList, range }) => {
+import { DocumentClassName, DocumentClassList, State } from './state'
const parts = classList.split(/(\s+)/)
+import { DocumentClassName, DocumentClassList, State } from './state'
const names: DocumentClassName[] = []
+import { DocumentClassName, DocumentClassList, State } from './state'
let index = 0
+import { DocumentClassName, DocumentClassList, State } from './state'
for (let i = 0; i < parts.length; i++) {
+import { DocumentClassName, DocumentClassList, State } from './state'
if (i % 2 === 0) {
+import { DocumentClassName, DocumentClassList, State } from './state'
const start = indexToPosition(classList, index)
+import { DocumentClassName, DocumentClassList, State } from './state'
const end = indexToPosition(classList, index + parts[i].length)
+ },
+import { DocumentClassName, DocumentClassList, State } from './state'
names.push({
+import { DocumentClassName, DocumentClassList, State } from './state'
className: parts[i],
+import { DocumentClassName, DocumentClassList, State } from './state'
range: {
+ },
+import { DocumentClassName, DocumentClassList, State } from './state'
start: {
+import { DocumentClassName, DocumentClassList, State } from './state'
line: range.start.line + start.line,
+import { DocumentClassName, DocumentClassList, State } from './state'
character:
+import { DocumentClassName, DocumentClassList, State } from './state'
(end.line === 0 ? range.start.character : 0) +
+import { DocumentClassName, DocumentClassList, State } from './state'
start.character,
+import { DocumentClassName, DocumentClassList, State } from './state'
},
+import { DocumentClassName, DocumentClassList, State } from './state'
end: {
+import { DocumentClassName, DocumentClassList, State } from './state'
line: range.start.line + end.line,
+import { DocumentClassName, DocumentClassList, State } from './state'
import { isCssContext, isCssDoc } from './css'
-import { isJsContext, isJsDoc } from './js'
+import {
+import { DocumentClassName, DocumentClassList, State } from './state'
(end.line === 0 ? range.start.character : 0) + end.character,
+import { DocumentClassName, DocumentClassList, State } from './state'
},
+ },
+ })
+ }
+import { DocumentClassName, DocumentClassList, State } from './state'
},
+ }
+import { DocumentClassName, DocumentClassList, State } from './state'
})
+}
+
+export function findClassNamesInRange(
+ doc: TextDocument,
+ range?: Range,
+ mode?: 'html' | 'css'
+): DocumentClassName[] {
+ const classLists = findClassListsInRange(doc, range, mode)
+import { DocumentClassName, DocumentClassList, State } from './state'
}
+}
+
+import { DocumentClassName, DocumentClassList, State } from './state'
index += parts[i].length
+import { TextDocument, Range, Position } from 'vscode-languageserver'
import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
-import {
+import { isJsContext, isJsDoc } from './js'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
- getClassAttributeLexer,
+import {
-import { isWithinRange } from './isWithinRange'
+import lineColumn from 'line-column'
+ const classLists = findClassListsInDocument(state, doc)
+ return matches[matches.length - 1]
import { isWithinRange } from './isWithinRange'
-import { TextDocument, Range, Position } from 'vscode-languageserver'
}
export function findClassListsInCssRange(
@@ -78,8 +135,13 @@ doc: TextDocument,
range?: Range
): DocumentClassList[] {
const text = doc.getText(range)
+ const matches = findAll(
+import { DocumentClassName, DocumentClassList, State } from './state'
import { isWithinRange } from './isWithinRange'
+import { DocumentClassName, DocumentClassList, State } from './state'
import { isWithinRange } from './isWithinRange'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
+ )
const globalStart: Position = range ? range.start : { line: 0, character: 0 }
return matches.map((match) => {
@@ -90,6 +152,7 @@ match.index + match[1].length + match.groups.classList.length
)
return {
classList: match.groups.classList,
+ important: Boolean(match.groups.important),
range: {
start: {
line: globalStart.line + start.line,
@@ -108,7 +171,7 @@ }
export function findClassListsInHtmlRange(
doc: TextDocument,
- range: Range
+ range?: Range
): DocumentClassList[] {
const text = doc.getText(range)
const matches = findAll(/(?:\b|:)class(?:Name)?=['"`{]/g, text)
@@ -187,18 +250,20 @@ return {
classList: value.substr(beforeOffset, value.length + afterOffset),
range: {
start: {
-import { isCssContext, isCssDoc } from './css'
+import { DocumentClassName, DocumentClassList, State } from './state'
import { isWithinRange } from './isWithinRange'
+import lineColumn from 'line-column'
character:
+export function findClassNamesInRange(
import { isCssContext, isCssDoc } from './css'
-import {
start.character,
},
end: {
-import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
import { DocumentClassName, DocumentClassList, State } from './state'
+ const text = doc.getText(range)
character:
- (end.line === 0 ? range.start.character : 0) + end.character,
+ (end.line === 0 ? range?.start.character || 0 : 0) +
+ end.character,
},
},
}
@@ -212,9 +277,9 @@ }
export function findClassListsInRange(
doc: TextDocument,
- range: Range,
+ range?: Range,
- let matches: RegExpMatchArray[] = []
import { DocumentClassName, DocumentClassList, State } from './state'
+ getClassAttributeLexer,
): DocumentClassList[] {
if (mode === 'css') {
return findClassListsInCssRange(doc, range)
@@ -230,90 +295,27 @@ if (isCssDoc(state, doc)) {
return findClassListsInCssRange(doc)
}
- if (isVueDoc(doc)) {
- while ((match = re.exec(str)) !== null) {
import { DocumentClassName, DocumentClassList, State } from './state'
- let blocks = findAll(
- /<(?<type>template|style|script)\b[^>]*>.*?(<\/\k<type>>|$)/gis,
- text
- )
- let htmlRanges: Range[] = []
-import { TextDocument, Range, Position } from 'vscode-languageserver'
const globalStart: Position = range ? range.start : { line: 0, character: 0 }
- for (let i = 0; i < blocks.length; i++) {
- let range = {
- start: indexToPosition(text, blocks[i].index),
- end: indexToPosition(text, blocks[i].index + blocks[i][0].length),
- }
- matches.push({ ...match })
import { DocumentClassName, DocumentClassList, State } from './state'
- cssRanges.push(range)
- } else {
- htmlRanges.push(range)
- }
-import {
import { isWithinRange } from './isWithinRange'
- return [].concat.apply(
- [],
- [
- ...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)),
- ...cssRanges.map((range) => findClassListsInCssRange(doc, range)),
- ]
- )
-import { TextDocument, Range, Position } from 'vscode-languageserver'
import {
- }
import { DocumentClassName, DocumentClassList, State } from './state'
-import { TextDocument, Range, Position } from 'vscode-languageserver'
import { isWithinRange } from './isWithinRange'
-import { DocumentClassName, DocumentClassList, State } from './state'
- let styleBlocks = findAll(/<style(?:\s[^>]*>|>).*?(<\/style>|$)/gis, text)
- let htmlRanges: Range[] = []
- let cssRanges: Range[] = []
- let currentIndex = 0
+ getClassAttributeLexer,
-import { TextDocument, Range, Position } from 'vscode-languageserver'
import { DocumentClassName, DocumentClassList, State } from './state'
- for (let i = 0; i < styleBlocks.length; i++) {
- htmlRanges.push({
- }
import { isJsContext, isJsDoc } from './js'
- end: indexToPosition(text, styleBlocks[i].index),
- })
- cssRanges.push({
- return matches
- end: indexToPosition(
- return matches
import { DocumentClassName, DocumentClassList, State } from './state'
- styleBlocks[i].index + styleBlocks[i][0].length
- ),
- })
- currentIndex = styleBlocks[i].index + styleBlocks[i][0].length
- }
- htmlRanges.push({
- return matches
import { isJsContext, isJsDoc } from './js'
import { TextDocument, Range, Position } from 'vscode-languageserver'
- lexer.reset(subtext)
- })
-import { TextDocument, Range, Position } from 'vscode-languageserver'
import { DocumentClassName, DocumentClassList, State } from './state'
-import { TextDocument, Range, Position } from 'vscode-languageserver'
import { isJsContext, isJsDoc } from './js'
-import { isWithinRange } from './isWithinRange'
- [],
- [
- ...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)),
- ...cssRanges.map((range) => findClassListsInCssRange(doc, range)),
- ]
- )
- }
-import { TextDocument, Range, Position } from 'vscode-languageserver'
import { DocumentClassName, DocumentClassList, State } from './state'
- return []
}
import { DocumentClassName, DocumentClassList, State } from './state'
+ )
const { line, col } = lineColumn(str + '\n', index)
return { line: line - 1, character: col - 1 }
}
diff --git a/src/lsp/util/getClassNameAtPosition.ts b/src/lsp/util/getClassNameAtPosition.ts
index 95de79a0392a9cfcf0de45d184a266748a77a1a3..7418b2f157723967380453978a082e42fdb9ce49 100644
--- a/src/lsp/util/getClassNameAtPosition.ts
+++ b/src/lsp/util/getClassNameAtPosition.ts
@@ -1,50 +1,8 @@
-import { TextDocument, Range, Position } from 'vscode-languageserver'
-import { State, DocumentClassName } from './state'
-const dlv = require('dlv')
-
-export function getClassNameAtPosition(
- document: TextDocument,
position: Position
-): DocumentClassName {
- const range1: Range = {
- start: { line: Math.max(position.line - 5, 0), character: 0 },
- end: position,
- }
- const text1: string = document.getText(range1)
-
- if (!/\bclass(Name)?=['"][^'"]*$/.test(text1)) return null
-
- const range2: Range = {
- start: { line: Math.max(position.line - 5, 0), character: 0 },
- end: { line: position.line + 1, character: position.character },
- }
-import { TextDocument, Range, Position } from 'vscode-languageserver'
position: Position
-
- let str: string = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0]
- let matches: RegExpMatchArray = str.match(/\bclass(Name)?=["']([^"']+)$/)
-
- if (!matches) return null
-
- let className: string = matches[2].split(' ').pop()
- if (!className) return null
-
- let range: Range = {
- start: {
- line: position.line,
- character:
-import { State, DocumentClassName } from './state'
position: Position
-import { State, DocumentClassName } from './state'
): DocumentClassName {
- end: {
- line: position.line,
const dlv = require('dlv')
- },
- }
-
- return { className, range }
-}
export function getClassNameParts(state: State, className: string): string[] {
let separator = state.separator
@@ -86,17 +44,3 @@ }
return false
})
}
-
-function combinations(str: string): string[] {
- let fn = function (active: string, rest: string, a: string[]) {
- if (!active && !rest) return
- if (!rest) {
- a.push(active)
- } else {
- fn(active + rest[0], rest.slice(1), a)
- fn(active, rest.slice(1), a)
- }
- return a
- }
- return fn('', str, [])
-}
diff --git a/src/lsp/util/getClassNameDecls.ts b/src/lsp/util/getClassNameDecls.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a04ce94f186e0999b7546021378aeef7a6db2498
--- /dev/null
+++ b/src/lsp/util/getClassNameDecls.ts
@@ -0,0 +1,20 @@
+import { State } from './state'
+import { getClassNameParts } from './getClassNameAtPosition'
+import removeMeta from './removeMeta'
+const dlv = require('dlv')
+
+export function getClassNameDecls(
+ state: State,
+ className: string
+): Record<string, string> | Record<string, string>[] | null {
+ const parts = getClassNameParts(state, className)
+ if (!parts) return null
+
+ const info = dlv(state.classNames.classNames, parts)
+
+ if (Array.isArray(info)) {
+ return info.map(removeMeta)
+ }
+
+ return removeMeta(info)
+}
diff --git a/src/lsp/util/getClassNameMeta.ts b/src/lsp/util/getClassNameMeta.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1099a1ab85825f114ad20874f36c5991dfb2866b
--- /dev/null
+++ b/src/lsp/util/getClassNameMeta.ts
@@ -0,0 +1,30 @@
+import { State, ClassNameMeta } from './state'
+import { getClassNameParts } from './getClassNameAtPosition'
+const dlv = require('dlv')
+
+export function getClassNameMeta(
+ state: State,
+ classNameOrParts: string | string[]
+): ClassNameMeta | ClassNameMeta[] {
+ const parts = Array.isArray(classNameOrParts)
+ ? classNameOrParts
+ : getClassNameParts(state, classNameOrParts)
+ if (!parts) return null
+ const info = dlv(state.classNames.classNames, parts)
+
+ if (Array.isArray(info)) {
+ return info.map((i) => ({
+ source: i.__source,
+ pseudo: i.__pseudo,
+ scope: i.__scope,
+ context: i.__context,
+ }))
+ }
+
+ return {
+ source: info.__source,
+ pseudo: info.__pseudo,
+ scope: info.__scope,
+ context: info.__context,
+ }
+}
diff --git a/src/lsp/util/getDocumentSettings.ts b/src/lsp/util/getDocumentSettings.ts
index 3ee25bd837e498fad81958abf76ff085f54325fa..2f127de4cdbe2bdc3df6778c26e283ad1771cd01 100644
--- a/src/lsp/util/getDocumentSettings.ts
+++ b/src/lsp/util/getDocumentSettings.ts
@@ -1,21 +1,22 @@
import { State, Settings } from './state'
+import { TextDocument } from 'vscode-languageserver'
export async function getDocumentSettings(
state: State,
- resource: string
+ document: TextDocument
): Promise<Settings> {
if (!state.editor.capabilities.configuration) {
return Promise.resolve(state.editor.globalSettings)
}
+
import { State, Settings } from './state'
if (!result) {
-import { State, Settings } from './state'
+
-import { State, Settings } from './state'
+
export async function getDocumentSettings(
- section: 'tailwindCSS',
})
- state.editor.documentSettings.set(resource, result)
+ state.editor.documentSettings.set(document.uri, result)
}
return result
}
diff --git a/src/lsp/util/getLanguageBoundaries.ts b/src/lsp/util/getLanguageBoundaries.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c309aaeca3846b7f2f0fc98af7cf80a95e6cd817
--- /dev/null
+++ b/src/lsp/util/getLanguageBoundaries.ts
@@ -0,0 +1,89 @@
+import { TextDocument, Range } from 'vscode-languageserver'
+import { isVueDoc, isHtmlDoc, isSvelteDoc } from './html'
+import { State } from './state'
+import { findAll, indexToPosition } from './find'
+import { isJsDoc } from './js'
+
+export interface LanguageBoundaries {
+ html: Range[]
+ css: Range[]
+}
+
+export function getLanguageBoundaries(
+ state: State,
+ doc: TextDocument
+): LanguageBoundaries | null {
+ if (isVueDoc(doc)) {
+ let text = doc.getText()
+ let blocks = findAll(
+ /(?<open><(?<type>template|style|script)\b[^>]*>).*?(?<close><\/\k<type>>|$)/gis,
+ text
+ )
+ let htmlRanges: Range[] = []
+ let cssRanges: Range[] = []
+ for (let i = 0; i < blocks.length; i++) {
+ let range = {
+ start: indexToPosition(
+ text,
+ blocks[i].index + blocks[i].groups.open.length
+ ),
+ end: indexToPosition(
+ text,
+ blocks[i].index + blocks[i][0].length - blocks[i].groups.close.length
+ ),
+ }
+ if (blocks[i].groups.type === 'style') {
+ cssRanges.push(range)
+ } else {
+ htmlRanges.push(range)
+ }
+ }
+
+ return {
+ html: htmlRanges,
+ css: cssRanges,
+ }
+ }
+
+ if (isHtmlDoc(state, doc) || isJsDoc(state, doc) || isSvelteDoc(doc)) {
+ let text = doc.getText()
+ let styleBlocks = findAll(
+ /(?<open><style(?:\s[^>]*>|>)).*?(?<close><\/style>|$)/gis,
+ text
+ )
+ let htmlRanges: Range[] = []
+ let cssRanges: Range[] = []
+ let currentIndex = 0
+
+ for (let i = 0; i < styleBlocks.length; i++) {
+ htmlRanges.push({
+ start: indexToPosition(text, currentIndex),
+ end: indexToPosition(text, styleBlocks[i].index),
+ })
+ cssRanges.push({
+ start: indexToPosition(
+ text,
+ styleBlocks[i].index + styleBlocks[i].groups.open.length
+ ),
+ end: indexToPosition(
+ text,
+ styleBlocks[i].index +
+ styleBlocks[i][0].length -
+ styleBlocks[i].groups.close.length
+ ),
+ })
+ currentIndex = styleBlocks[i].index + styleBlocks[i][0].length
+ }
+ htmlRanges.push({
+ start: indexToPosition(text, currentIndex),
+ end: indexToPosition(text, text.length),
+ })
+
+ return {
+ html: htmlRanges,
+ css: cssRanges,
+ }
+ }
+
+ return null
+}
diff --git a/src/lsp/util/joinWithAnd.ts b/src/lsp/util/joinWithAnd.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2b2efb716afd1394a91dd44fc6f16c05d053b438
--- /dev/null
+++ b/src/lsp/util/joinWithAnd.ts
@@ -0,0 +1,11 @@
+export function joinWithAnd(strings: string[]): string {
+ return strings.reduce((acc, cur, i) => {
+ if (i === 0) {
+ return cur
+ }
+ if (strings.length > 1 && i === strings.length - 1) {
+ return `${acc} and ${cur}`
+ }
+ return `${acc}, ${cur}`
+ }, '')
+}
diff --git a/src/lsp/util/logFull.ts b/src/lsp/util/logFull.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c05fc1b824a56b3fe33f1f3e4e91b24d8d6074a7
--- /dev/null
+++ b/src/lsp/util/logFull.ts
@@ -0,0 +1,5 @@
+import * as util from 'util'
+
+export function logFull(object: any): void {
+ console.log(util.inspect(object, { showHidden: false, depth: null }))
+}
diff --git a/src/lsp/util/rangesEqual.ts b/src/lsp/util/rangesEqual.ts
new file mode 100644
index 0000000000000000000000000000000000000000..220cebd50e242933d666ee7581d68e4030dd321b
--- /dev/null
+++ b/src/lsp/util/rangesEqual.ts
@@ -0,0 +1,10 @@
+import { Range } from 'vscode-languageserver'
+
+export function rangesEqual(a: Range, b: Range): boolean {
+ return (
+ a.start.line === b.start.line &&
+ a.start.character === b.start.character &&
+ a.end.line === b.end.line &&
+ a.end.character === b.end.character
+ )
+}
diff --git a/src/lsp/util/removeRangesFromString.ts b/src/lsp/util/removeRangesFromString.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f97d62b95b2aecae61c0e4195fba475af541a17e
--- /dev/null
+++ b/src/lsp/util/removeRangesFromString.ts
@@ -0,0 +1,39 @@
+import { Range } from 'vscode-languageserver'
+import lineColumn from 'line-column'
+import { ensureArray } from '../../util/array'
+
+export function removeRangesFromString(
+ str: string,
+ rangeOrRanges: Range | Range[]
+): string {
+ let ranges = ensureArray(rangeOrRanges)
+ let finder = lineColumn(str + '\n', { origin: 0 })
+ let indexRanges: { start: number; end: number }[] = []
+
+ ranges.forEach((range) => {
+ let start = finder.toIndex(range.start.line, range.start.character)
+ let end = finder.toIndex(range.end.line, range.end.character)
+ for (let i = start - 1; i >= 0; i--) {
+ if (/\s/.test(str.charAt(i))) {
+ start = i
+ } else {
+ break
+ }
+ }
+ indexRanges.push({ start, end })
+ })
+
+ indexRanges.sort((a, b) => a.start - b.start)
+
+ let result = ''
+ let i = 0
+
+ indexRanges.forEach((indexRange) => {
+ result += str.substring(i, indexRange.start)
+ i = indexRange.end
+ })
+
+ result += str.substring(i)
+
+ return result.trim()
+}
diff --git a/src/lsp/util/state.ts b/src/lsp/util/state.ts
index daaa0501c05f20e0e88dc85c4357533d26e4a74d..09a02006721913e72c3a576ee1c30f997476c6b5 100644
--- a/src/lsp/util/state.ts
+++ b/src/lsp/util/state.ts
@@ -1,4 +1,5 @@
import { TextDocuments, Connection, Range } from 'vscode-languageserver'
+import { NotificationEmitter } from '../../lib/emitter'
export type ClassNamesTree = {
[key: string]: ClassNamesTree
@@ -21,19 +22,36 @@ globalSettings: Settings
userLanguages: Record<string, string>
capabilities: {
configuration: boolean
+ diagnosticRelatedInformation: boolean
}
}
+type DiagnosticSeveritySetting = 'ignore' | 'warning' | 'error'
+
export type Settings = {
emmetCompletions: boolean
includeLanguages: Record<string, string>
+ validate: boolean
+ lint: {
+ cssConflict: DiagnosticSeveritySetting
+ invalidApply: DiagnosticSeveritySetting
+ invalidScreen: DiagnosticSeveritySetting
+ invalidVariant: DiagnosticSeveritySetting
+ invalidConfigPath: DiagnosticSeveritySetting
+ invalidTailwindDirective: DiagnosticSeveritySetting
+ }
}
export type State = null | {
enabled: boolean
+ emitter: NotificationEmitter
version?: string
configPath?: string
config?: any
+ modules?: {
+ tailwindcss: any
+ postcss: any
+ }
separator?: string
plugins?: any[]
variants?: string[]
@@ -47,8 +65,20 @@ export type DocumentClassList = {
classList: string
range: Range
}
+}
+}
export type DocumentClassName = {
className: string
range: Range
}
+export type ClassNamesContext = {
+ classList: DocumentClassList
+}
+
+export type ClassNameMeta = {
+ source: 'base' | 'components' | 'utilities'
+ pseudo: string[]
+ scope: string[]
+ context: string[]
+}
diff --git a/src/lsp/util/stringToPath.ts b/src/lsp/util/stringToPath.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b06e1532e1903abb99361f1df64c01ad8bf408cf
--- /dev/null
+++ b/src/lsp/util/stringToPath.ts
@@ -0,0 +1,15 @@
+// https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L6735-L6744
+let rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g
+let reEscapeChar = /\\(\\)?/g
+
+export function stringToPath(string: string): string[] {
+ let result: string[] = []
+ if (string.charCodeAt(0) === 46 /* . */) {
+ result.push('')
+ }
+ // @ts-ignore
+ string.replace(rePropName, (match, number, quote, subString) => {
+ result.push(quote ? subString.replace(reEscapeChar, '$1') : number || match)
+ })
+ return result
+}
diff --git a/src/lsp/util/validateApply.ts b/src/lsp/util/validateApply.ts
new file mode 100644
index 0000000000000000000000000000000000000000..52f2b2c432fc5d85f448a1e9560683dc75a5bfed
--- /dev/null
+++ b/src/lsp/util/validateApply.ts
@@ -0,0 +1,44 @@
+import { State } from './state'
+import { getClassNameMeta } from './getClassNameMeta'
+
+export function validateApply(
+ state: State,
+ classNameOrParts: string | string[]
+): { isApplyable: true } | { isApplyable: false; reason: string } | null {
+ const meta = getClassNameMeta(state, classNameOrParts)
+ if (!meta) return null
+
+ const className = Array.isArray(classNameOrParts)
+ ? classNameOrParts.join(state.separator)
+ : classNameOrParts
+
+ let reason: string
+
+ if (Array.isArray(meta)) {
+ reason = `'@apply' cannot be used with '${className}' because it is included in multiple rulesets.`
+ } else if (meta.source !== 'utilities') {
+ reason = `'@apply' cannot be used with '${className}' because it is not a utility.`
+ } else if (meta.context && meta.context.length > 0) {
+ if (meta.context.length === 1) {
+ reason = `'@apply' cannot be used with '${className}' because it is nested inside of an at-rule ('${meta.context[0]}').`
+ } else {
+ reason = `'@apply' cannot be used with '${className}' because it is nested inside of at-rules (${meta.context
+ .map((c) => `'${c}'`)
+ .join(', ')}).`
+ }
+ } else if (meta.pseudo && meta.pseudo.length > 0) {
+ if (meta.pseudo.length === 1) {
+ reason = `'@apply' cannot be used with '${className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')`
+ } else {
+ reason = `'@apply' cannot be used with '${className}' because its definition includes pseudo-selectors (${meta.pseudo
+ .map((p) => `'${p}'`)
+ .join(', ')}).`
+ }
+ }
+
+ if (reason) {
+ return { isApplyable: false, reason }
+ }
+
+ return { isApplyable: true }
+}
diff --git a/src/util/array.ts b/src/util/array.ts
index b40dd245fe57a3f1e1ddeb24fc75343041b530db..869eb9f6cd346528cd554b115bfb374952032e48 100644
--- a/src/util/array.ts
+++ b/src/util/array.ts
@@ -2,6 +2,16 @@ 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]
}