Merge branch 'diagnostics'
After Width: | Height: | Size: 256 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 235 KiB |
After Width: | Height: | Size: 267 KiB |
|
@ -8,4 +8,4 @@ contributing.md
|
|||
node_modules/**
|
||||
src/**
|
||||
tests/**
|
||||
img/**
|
||||
.github/**
|
||||
|
|
17
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
|
||||
|
|
98
README.md
|
@ -1,60 +1,36 @@
|
|||
# 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)**
|
||||
## Installation
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html.gif" alt="HTML autocompletion" width="750">
|
||||
**[Install via the Visual Studio Code Marketplace →](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**
|
||||
|
||||
## 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/`)
|
||||
In order for the extension to activate you must have [`tailwindcss` installed](https://tailwindcss.com/docs/installation/#1-install-tailwind-via-npm) and a [Tailwind config file](https://tailwindcss.com/docs/installation/#3-create-your-tailwind-config-file-optional) named `tailwind.config.js` or `tailwind.js` in your workspace.
|
||||
|
||||
## Features
|
||||
|
||||
Tailwind CSS IntelliSense uses your projects Tailwind installation and configuration to provide suggestions as you type.
|
||||
### Autocomplete
|
||||
|
||||
It also includes features that improve the overall Tailwind experience, including improved syntax highlighting, and CSS previews.
|
||||
Intelligent suggestions for class names, as well as [CSS functions and directives](https://tailwindcss.com/docs/functions-and-directives/).
|
||||
|
||||
### HTML (including Vue, JSX, PHP etc.)
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/diagnostics/.github/autocomplete.png" alt="" />
|
||||
|
||||
- [Class name suggestions, including support for Emmet syntax](#class-name-suggestions-including-support-for-emmet-syntax)
|
||||
- Suggestions include color previews where applicable, for example for text and background colors
|
||||
- They also include a preview of the actual CSS for that class name
|
||||
- [CSS preview when hovering over class names](#css-preview-when-hovering-over-class-names)
|
||||
### Linting
|
||||
|
||||
### CSS
|
||||
Highlights errors and potential bugs in both your CSS and your markup.
|
||||
|
||||
- [Suggestions when using `@apply` and config helpers](#suggestions-when-using-apply-and-config)
|
||||
- Suggestions when using the `@screen` directive
|
||||
- Suggestions when using the `@variants` directive
|
||||
- [Improves syntax highlighting when using `@apply` and config helpers](#improves-syntax-highlighting-when-using-apply-and-config-helpers)
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/diagnostics/.github/linting.png" alt="" />
|
||||
|
||||
## Examples
|
||||
### Hover Preview
|
||||
|
||||
#### Class name suggestions, including support for Emmet syntax
|
||||
See the complete CSS for a Tailwind class name by hovering over it.
|
||||
|
||||
<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/diagnostics/.github/hover.png" alt="" />
|
||||
|
||||
#### CSS preview when hovering over class names
|
||||
### CSS Syntax Highlighting
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/html-hover.gif" alt="HTML hover preview" width="750">
|
||||
|
||||
#### Suggestions when using `@apply` and config helpers
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/css.gif" alt="CSS autocompletion" width="750">
|
||||
|
||||
#### Improves syntax highlighting when using `@apply` and config helpers
|
||||
|
||||
Before:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/css-highlighting-before.png" alt="CSS syntax highlighting before" width="400">
|
||||
|
||||
After:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/img/css-highlighting-after.png" alt="CSS syntax highlighting after" width="400">
|
||||
Provides syntax definitions so that Tailwind features are highlighted correctly.
|
||||
|
||||
## Settings
|
||||
|
||||
|
@ -70,7 +46,7 @@ This setting allows you to add additional language support. The key of each entr
|
|||
}
|
||||
```
|
||||
|
||||
### `tailwindcss.emmetCompletions`
|
||||
### `tailwindCSS.emmetCompletions`
|
||||
|
||||
Enable completions when using [Emmet](https://emmet.io/)-style syntax, for example `div.bg-red-500.uppercase`. **Default: `false`**
|
||||
|
||||
|
@ -79,3 +55,43 @@ Enable completions when using [Emmet](https://emmet.io/)-style syntax, for examp
|
|||
"tailwindCSS.emmetCompletions": true
|
||||
}
|
||||
```
|
||||
|
||||
### `tailwindCSS.validate`
|
||||
|
||||
Enable linting. Rules can be configured individually using the `tailwindcss.lint` settings:
|
||||
|
||||
- `ignore`: disable lint rule entirely
|
||||
- `warning`: rule violations will be considered "warnings," typically represented by a yellow underline
|
||||
- `error`: rule violations will be considered "errors," typically represented by a red underline
|
||||
|
||||
#### `tailwindCSS.lint.invalidScreen`
|
||||
|
||||
Unknown screen name used with the [`@screen` directive](https://tailwindcss.com/docs/functions-and-directives/#screen). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidVariant`
|
||||
|
||||
Unknown variant name used with the [`@variants` directive](https://tailwindcss.com/docs/functions-and-directives/#variants). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidTailwindDirective`
|
||||
|
||||
Unknown value used with the [`@tailwind` directive](https://tailwindcss.com/docs/functions-and-directives/#tailwind). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidApply`
|
||||
|
||||
Unsupported use of the [`@apply` directive](https://tailwindcss.com/docs/functions-and-directives/#apply). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.invalidConfigPath`
|
||||
|
||||
Unknown or invalid path used with the [`theme` helper](https://tailwindcss.com/docs/functions-and-directives/#theme). **Default: `error`**
|
||||
|
||||
#### `tailwindCSS.lint.cssConflict`
|
||||
|
||||
Class names on the same HTML element which apply the same CSS property or properties. **Default: `warning`**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you’re having issues getting the IntelliSense features to activate, there are a few things you can check:
|
||||
|
||||
- Ensure that you have a Tailwind config file in your workspace and that this is named `tailwind.config.js` or `tailwind.js`. Check out the Tailwind documentation for details on [creating a config file](https://tailwindcss.com/docs/installation/#3-create-your-tailwind-config-file-optional).
|
||||
- Ensure that the `tailwindcss` module is installed in your workspace, via `npm`, `yarn`, or `pnpm`. Tailwind CSS IntelliSense does not currently support Yarn Plug'n'Play.
|
||||
- If you installed `tailwindcss` or created your config file while your project was already open in Visual Studio Code you may need to reload the editor. You can either restart VS Code entirely, or use the `Developer: Reload Window` command which can be found in the command palette.
|
||||
|
|
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 32 KiB |
BIN
img/css.gif
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 592 KiB |
BIN
img/html.gif
Before Width: | Height: | Size: 2.8 MiB |
|
@ -2026,6 +2026,12 @@
|
|||
"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",
|
||||
|
@ -6084,6 +6090,12 @@
|
|||
"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",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
|
|
85
package.json
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"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",
|
||||
"homepage": "https://github.com/bradlc/vscode-tailwindcss",
|
||||
"homepage": "https://github.com/tailwindcss/intellisense",
|
||||
"bugs": {
|
||||
"url": "https://github.com/bradlc/vscode-tailwindcss/issues",
|
||||
"url": "https://github.com/tailwindcss/intellisense/issues",
|
||||
"email": "hello@bradley.dev"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bradlc/vscode-tailwindcss.git"
|
||||
"url": "https://github.com/tailwindcss/intellisense.git"
|
||||
},
|
||||
"publisher": "bradlc",
|
||||
"keywords": [
|
||||
|
@ -28,10 +28,11 @@
|
|||
"vscode": "^1.33.0"
|
||||
},
|
||||
"categories": [
|
||||
"Linters",
|
||||
"Other"
|
||||
],
|
||||
"galleryBanner": {
|
||||
"color": "#f1f5f8"
|
||||
"color": "#f9fafb"
|
||||
},
|
||||
"icon": "media/icon.png",
|
||||
"activationEvents": [
|
||||
|
@ -69,6 +70,78 @@
|
|||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +166,7 @@
|
|||
"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",
|
||||
|
@ -111,6 +185,7 @@
|
|||
"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",
|
||||
|
|
|
@ -141,6 +141,10 @@ export default async function getClassNames(
|
|||
postcss,
|
||||
browserslist,
|
||||
}),
|
||||
modules: {
|
||||
tailwindcss,
|
||||
postcss,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
OutputChannel,
|
||||
WorkspaceFolder,
|
||||
Uri,
|
||||
ConfigurationScope,
|
||||
} from 'vscode'
|
||||
import {
|
||||
LanguageClient,
|
||||
|
@ -22,6 +23,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'
|
||||
|
@ -150,6 +152,9 @@ export function activate(context: ExtensionContext) {
|
|||
client.onReady().then(() => {
|
||||
let emitter = createEmitter(client)
|
||||
registerConfigErrorHandler(emitter)
|
||||
onMessage(client, 'getConfiguration', async (scope) => {
|
||||
return Workspace.getConfiguration('tailwindCSS', scope)
|
||||
})
|
||||
})
|
||||
|
||||
client.start()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import mitt from 'mitt'
|
||||
import { LanguageClient } from 'vscode-languageclient'
|
||||
import crypto from 'crypto'
|
||||
import { Connection } from 'vscode-languageserver'
|
||||
|
||||
export interface NotificationEmitter {
|
||||
on: (name: string, handler: (args: any) => void) => void
|
||||
|
@ -8,7 +9,9 @@ export interface NotificationEmitter {
|
|||
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,7 +29,7 @@ export function createEmitter(client: LanguageClient): NotificationEmitter {
|
|||
emitter.off(name, handler)
|
||||
}
|
||||
|
||||
const emit = (name: string, params: any) => {
|
||||
const emit = (name: string, params: Record<string, any> = {}) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
const id = crypto.randomBytes(16).toString('hex')
|
||||
on(`${name}Response`, (result) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
}
|
|
@ -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
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
|
@ -618,11 +618,11 @@ async function provideEmmetCompletions(
|
|||
state: State,
|
||||
{ position, textDocument }: CompletionParams
|
||||
): Promise<CompletionList> {
|
||||
let settings = await getDocumentSettings(state, textDocument.uri)
|
||||
if (settings.emmetCompletions !== true) return null
|
||||
|
||||
let doc = state.editor.documents.get(textDocument.uri)
|
||||
|
||||
let settings = await getDocumentSettings(state, doc)
|
||||
if (settings.emmetCompletions !== true) return null
|
||||
|
||||
const syntax = isHtmlContext(state, doc, position)
|
||||
? 'html'
|
||||
: isJsContext(state, doc, position)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
|
@ -81,6 +82,13 @@ function provideClassNameHover(
|
|||
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: {
|
||||
language: 'css',
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
Hover,
|
||||
TextDocumentPositionParams,
|
||||
DidChangeConfigurationNotification,
|
||||
CodeActionParams,
|
||||
CodeAction,
|
||||
} from 'vscode-languageserver'
|
||||
import getTailwindState from '../class-names/index'
|
||||
import { State, Settings, EditorState } from './util/state'
|
||||
|
@ -26,25 +28,44 @@ import {
|
|||
import { provideHover } from './providers/hoverProvider'
|
||||
import { URI } from 'vscode-uri'
|
||||
import { getDocumentSettings } from './util/getDocumentSettings'
|
||||
import {
|
||||
provideDiagnostics,
|
||||
updateAllDiagnostics,
|
||||
clearAllDiagnostics,
|
||||
} from './providers/diagnostics/diagnosticsProvider'
|
||||
import { createEmitter } from '../lib/emitter'
|
||||
import { provideCodeActions } from './providers/codeActions/codeActionProvider'
|
||||
|
||||
let state: State = { enabled: false }
|
||||
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(
|
||||
|
@ -64,6 +85,10 @@ connection.onInitialize(
|
|||
capabilities: {
|
||||
configuration:
|
||||
capabilities.workspace && !!capabilities.workspace.configuration,
|
||||
diagnosticRelatedInformation:
|
||||
capabilities.textDocument &&
|
||||
capabilities.textDocument.publishDiagnostics &&
|
||||
capabilities.textDocument.publishDiagnostics.relatedInformation,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -73,14 +98,24 @@ connection.onInitialize(
|
|||
// @ts-ignore
|
||||
onChange: (newState: State): void => {
|
||||
if (newState && !newState.error) {
|
||||
state = { ...newState, enabled: true, editor: editorState }
|
||||
state = {
|
||||
...newState,
|
||||
enabled: true,
|
||||
emitter: state.emitter,
|
||||
editor: editorState,
|
||||
}
|
||||
connection.sendNotification('tailwindcss/configUpdated', [
|
||||
state.configPath,
|
||||
state.config,
|
||||
state.plugins,
|
||||
])
|
||||
updateAllDiagnostics(state)
|
||||
} else {
|
||||
state = { enabled: false, editor: editorState }
|
||||
state = {
|
||||
enabled: false,
|
||||
emitter: state.emitter,
|
||||
editor: editorState,
|
||||
}
|
||||
if (newState && newState.error) {
|
||||
const payload: {
|
||||
message: string
|
||||
|
@ -95,6 +130,7 @@ connection.onInitialize(
|
|||
}
|
||||
connection.sendNotification('tailwindcss/configError', [payload])
|
||||
}
|
||||
clearAllDiagnostics(state)
|
||||
// TODO
|
||||
// connection.sendNotification('tailwindcss/configUpdated', [null])
|
||||
}
|
||||
|
@ -103,9 +139,14 @@ connection.onInitialize(
|
|||
)
|
||||
|
||||
if (tailwindState) {
|
||||
state = { enabled: true, editor: editorState, ...tailwindState }
|
||||
state = {
|
||||
enabled: true,
|
||||
emitter: state.emitter,
|
||||
editor: editorState,
|
||||
...tailwindState,
|
||||
}
|
||||
} else {
|
||||
state = { enabled: false, editor: editorState }
|
||||
state = { enabled: false, emitter: state.emitter, editor: editorState }
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -133,6 +174,7 @@ connection.onInitialize(
|
|||
],
|
||||
},
|
||||
hoverProvider: true,
|
||||
codeActionProvider: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -164,9 +206,7 @@ connection.onDidChangeConfiguration((change) => {
|
|||
)
|
||||
}
|
||||
|
||||
state.editor.documents
|
||||
.all()
|
||||
.forEach((doc) => getDocumentSettings(state, doc.uri))
|
||||
updateAllDiagnostics(state)
|
||||
})
|
||||
|
||||
connection.onCompletion(
|
||||
|
@ -190,4 +230,11 @@ connection.onHover(
|
|||
}
|
||||
)
|
||||
|
||||
connection.onCodeAction(
|
||||
(params: CodeActionParams): Promise<CodeAction[]> => {
|
||||
if (!state.enabled) return null
|
||||
return provideCodeActions(state, params)
|
||||
}
|
||||
)
|
||||
|
||||
connection.listen()
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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, [])
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
@ -27,15 +29,11 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray {
|
|||
return matches[matches.length - 1]
|
||||
}
|
||||
|
||||
export function findClassNamesInRange(
|
||||
doc: TextDocument,
|
||||
range?: Range,
|
||||
mode?: 'html' | 'css'
|
||||
): DocumentClassName[] {
|
||||
const classLists = findClassListsInRange(doc, range, mode)
|
||||
return [].concat.apply(
|
||||
[],
|
||||
classLists.map(({ classList, range }) => {
|
||||
export function getClassNamesInClassList({
|
||||
classList,
|
||||
range,
|
||||
important,
|
||||
}: DocumentClassList): DocumentClassName[] {
|
||||
const parts = classList.split(/(\s+)/)
|
||||
const names: DocumentClassName[] = []
|
||||
let index = 0
|
||||
|
@ -45,12 +43,20 @@ export function findClassNamesInRange(
|
|||
const end = indexToPosition(classList, index + parts[i].length)
|
||||
names.push({
|
||||
className: parts[i],
|
||||
classList: {
|
||||
classList,
|
||||
range,
|
||||
important,
|
||||
},
|
||||
relativeRange: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
range: {
|
||||
start: {
|
||||
line: range.start.line + start.line,
|
||||
character:
|
||||
(end.line === 0 ? range.start.character : 0) +
|
||||
start.character,
|
||||
(end.line === 0 ? range.start.character : 0) + start.character,
|
||||
},
|
||||
end: {
|
||||
line: range.start.line + end.line,
|
||||
|
@ -63,8 +69,23 @@ export function findClassNamesInRange(
|
|||
index += parts[i].length
|
||||
}
|
||||
return names
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function findClassNamesInRange(
|
||||
doc: TextDocument,
|
||||
range?: Range,
|
||||
mode?: 'html' | 'css'
|
||||
): DocumentClassName[] {
|
||||
const classLists = findClassListsInRange(doc, range, mode)
|
||||
return flatten(classLists.map(getClassNamesInClassList))
|
||||
}
|
||||
|
||||
export function findClassNamesInDocument(
|
||||
state: State,
|
||||
doc: TextDocument
|
||||
): DocumentClassName[] {
|
||||
const classLists = findClassListsInDocument(state, doc)
|
||||
return flatten(classLists.map(getClassNamesInClassList))
|
||||
}
|
||||
|
||||
export function findClassListsInCssRange(
|
||||
|
@ -72,7 +93,10 @@ export function findClassListsInCssRange(
|
|||
range?: Range
|
||||
): DocumentClassList[] {
|
||||
const text = doc.getText(range)
|
||||
const matches = findAll(/(@apply\s+)(?<classList>[^;}]+)[;}]/g, text)
|
||||
const matches = findAll(
|
||||
/(@apply\s+)(?<classList>[^;}]+?)(?<important>\s*!important)?\s*[;}]/g,
|
||||
text
|
||||
)
|
||||
const globalStart: Position = range ? range.start : { line: 0, character: 0 }
|
||||
|
||||
return matches.map((match) => {
|
||||
|
@ -83,6 +107,7 @@ export function findClassListsInCssRange(
|
|||
)
|
||||
return {
|
||||
classList: match.groups.classList,
|
||||
important: Boolean(match.groups.important),
|
||||
range: {
|
||||
start: {
|
||||
line: globalStart.line + start.line,
|
||||
|
@ -101,7 +126,7 @@ export function findClassListsInCssRange(
|
|||
|
||||
export function findClassListsInHtmlRange(
|
||||
doc: TextDocument,
|
||||
range: Range
|
||||
range?: Range
|
||||
): DocumentClassList[] {
|
||||
const text = doc.getText(range)
|
||||
const matches = findAll(/(?:\b|:)class(?:Name)?=['"`{]/g, text)
|
||||
|
@ -180,15 +205,16 @@ export function findClassListsInHtmlRange(
|
|||
classList: value.substr(beforeOffset, value.length + afterOffset),
|
||||
range: {
|
||||
start: {
|
||||
line: range.start.line + start.line,
|
||||
line: (range?.start.line || 0) + start.line,
|
||||
character:
|
||||
(end.line === 0 ? range.start.character : 0) +
|
||||
(end.line === 0 ? range?.start.character || 0 : 0) +
|
||||
start.character,
|
||||
},
|
||||
end: {
|
||||
line: range.start.line + end.line,
|
||||
line: (range?.start.line || 0) + end.line,
|
||||
character:
|
||||
(end.line === 0 ? range.start.character : 0) + end.character,
|
||||
(end.line === 0 ? range?.start.character || 0 : 0) +
|
||||
end.character,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -202,8 +228,8 @@ export function findClassListsInHtmlRange(
|
|||
|
||||
export function findClassListsInRange(
|
||||
doc: TextDocument,
|
||||
range: Range,
|
||||
mode: 'html' | 'css'
|
||||
range?: Range,
|
||||
mode?: 'html' | 'css'
|
||||
): DocumentClassList[] {
|
||||
if (mode === 'css') {
|
||||
return findClassListsInCssRange(doc, range)
|
||||
|
@ -219,73 +245,16 @@ export function findClassListsInDocument(
|
|||
return findClassListsInCssRange(doc)
|
||||
}
|
||||
|
||||
if (isVueDoc(doc)) {
|
||||
let text = doc.getText()
|
||||
let blocks = findAll(
|
||||
/<(?<type>template|style|script)\b[^>]*>.*?(<\/\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),
|
||||
end: indexToPosition(text, blocks[i].index + blocks[i][0].length),
|
||||
}
|
||||
if (blocks[i].groups.type === 'style') {
|
||||
cssRanges.push(range)
|
||||
} else {
|
||||
htmlRanges.push(range)
|
||||
}
|
||||
}
|
||||
return [].concat.apply(
|
||||
[],
|
||||
[
|
||||
...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)),
|
||||
...cssRanges.map((range) => findClassListsInCssRange(doc, range)),
|
||||
]
|
||||
)
|
||||
}
|
||||
let boundaries = getLanguageBoundaries(state, doc)
|
||||
if (!boundaries) return []
|
||||
|
||||
if (isHtmlDoc(state, doc) || isJsDoc(state, doc) || isSvelteDoc(doc)) {
|
||||
let text = doc.getText()
|
||||
let styleBlocks = findAll(/<style(?:\s[^>]*>|>).*?(<\/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),
|
||||
end: indexToPosition(
|
||||
text,
|
||||
styleBlocks[i].index + styleBlocks[i][0].length
|
||||
),
|
||||
})
|
||||
currentIndex = styleBlocks[i].index + styleBlocks[i][0].length
|
||||
}
|
||||
htmlRanges.push({
|
||||
start: indexToPosition(text, currentIndex),
|
||||
end: indexToPosition(text, text.length),
|
||||
})
|
||||
|
||||
return [].concat.apply(
|
||||
[],
|
||||
[
|
||||
...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)),
|
||||
...cssRanges.map((range) => findClassListsInCssRange(doc, range)),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
return []
|
||||
return flatten([
|
||||
...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)),
|
||||
...boundaries.css.map((range) => findClassListsInCssRange(doc, range)),
|
||||
])
|
||||
}
|
||||
|
||||
function indexToPosition(str: string, index: number): Position {
|
||||
export function indexToPosition(str: string, index: number): Position {
|
||||
const { line, col } = lineColumn(str + '\n', index)
|
||||
return { line: line - 1, character: col - 1 }
|
||||
}
|
||||
|
|
|
@ -1,48 +1,7 @@
|
|||
import { TextDocument, Range, Position } from 'vscode-languageserver'
|
||||
import { State, DocumentClassName } from './state'
|
||||
import { State } from './state'
|
||||
import { combinations } from './combinations'
|
||||
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 },
|
||||
}
|
||||
const text2: string = document.getText(range2)
|
||||
|
||||
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:
|
||||
position.character + str.length - text1.length - className.length,
|
||||
},
|
||||
end: {
|
||||
line: position.line,
|
||||
character: position.character + str.length - text1.length,
|
||||
},
|
||||
}
|
||||
|
||||
return { className, range }
|
||||
}
|
||||
|
||||
export function getClassNameParts(state: State, className: string): string[] {
|
||||
let separator = state.separator
|
||||
className = className.replace(/^\./, '')
|
||||
|
@ -83,17 +42,3 @@ export function getClassNameParts(state: State, className: string): string[] {
|
|||
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, [])
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
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)
|
||||
}
|
||||
let result = state.editor.documentSettings.get(resource)
|
||||
let result = state.editor.documentSettings.get(document.uri)
|
||||
if (!result) {
|
||||
result = await state.editor.connection.workspace.getConfiguration({
|
||||
scopeUri: resource,
|
||||
section: 'tailwindCSS',
|
||||
result = await state.emitter.emit('getConfiguration', {
|
||||
languageId: document.languageId,
|
||||
})
|
||||
state.editor.documentSettings.set(resource, result)
|
||||
state.editor.documentSettings.set(document.uri, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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}`
|
||||
}, '')
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import * as util from 'util'
|
||||
|
||||
export function logFull(object: any): void {
|
||||
console.log(util.inspect(object, { showHidden: false, depth: null }))
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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 @@ export type EditorState = {
|
|||
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[]
|
||||
|
@ -46,9 +64,19 @@ export type State = null | {
|
|||
export type DocumentClassList = {
|
||||
classList: string
|
||||
range: Range
|
||||
important?: boolean
|
||||
}
|
||||
|
||||
export type DocumentClassName = {
|
||||
className: string
|
||||
range: Range
|
||||
relativeRange: Range
|
||||
classList: DocumentClassList
|
||||
}
|
||||
|
||||
export type ClassNameMeta = {
|
||||
source: 'base' | 'components' | 'utilities'
|
||||
pseudo: string[]
|
||||
scope: string[]
|
||||
context: string[]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
|
|