tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
Merge branch 'color-decorators'
10 changed files, 346 additions(+), 27 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 3617ee8e60c972655beb158107c47bb0ba1723bd..f79d333c85f536da05d9bdf000525a25dbbe4168 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -1019,6 +1019,12 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
+ "@types/debounce": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.0.tgz",
+ "integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==",
+ "dev": true
+ },
"@types/graceful-fs": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz",
@@ -1958,6 +1964,12 @@ "date-fns": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.11.0.tgz",
"integrity": "sha512-8P1cDi8ebZyDxUyUprBXwidoEtiQAawYPGvpfb+Dg0G6JrQ+VozwOmm91xYC0vAv1+0VmLehEPb+isg4BGUFfA==",
+ "dev": true
+ },
+ "debounce": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
+ "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==",
"dev": true
},
"debug": {
diff --git a/package.json b/package.json
index 0cdb709c6539c495c4114edaaf2222479b237252..7815ce18bfdbad0cb2f858a749d55aecc269c885 100755
--- a/package.json
+++ b/package.json
@@ -71,6 +71,22 @@ },
"default": {},
"markdownDescription": "Enable features in languages that are not supported by default. Add a mapping here between the new language and an already supported language.\n E.g.: `{\"plaintext\": \"html\"}`"
},
+ "tailwindCSS.colorDecorators": {
+ "type": "string",
+ "enum": [
+ "inherit",
+ "on",
+ "off"
+ ],
+ "markdownEnumDescriptions": [
+ "Color decorators are rendered if `editor.colorDecorators` is enabled.",
+ "Color decorators are rendered.",
+ "Color decorators are not rendered."
+ ],
+ "default": "inherit",
+ "markdownDescription": "Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.",
+ "scope": "language-overridable"
+ },
"tailwindCSS.validate": {
"type": "boolean",
"default": true,
@@ -158,6 +174,8 @@ },
"devDependencies": {
"@ctrl/tinycolor": "^3.1.0",
{
+ "type": "boolean",
+{
"description": "Intelligent Tailwind CSS tooling for VS Code",
"@types/moo": "^0.5.3",
"@types/node": "^13.9.3",
@@ -167,6 +185,7 @@ "callsite": "^1.0.0",
"chokidar": "^3.3.1",
"concurrently": "^5.1.0",
"css.escape": "^1.5.1",
+ "debounce": "^1.2.0",
"detect-indent": "^6.0.0",
"dlv": "^1.1.3",
"dset": "^2.0.1",
diff --git a/src/extension.ts b/src/extension.ts
index 279ec969590ba1e30120474c179722fa450c238d..fe982e92a2882d37e61976a3f1d2733cee6e01e2 100755
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -24,6 +24,7 @@ import isObject from './util/isObject'
import { dedupe, equal } from './util/array'
import { createEmitter } from './lib/emitter'
import { onMessage } from './lsp/notifications'
+import { registerColorDecorator } from './lib/registerColorDecorator'
const CLIENT_ID = 'tailwindcss-intellisense'
const CLIENT_NAME = 'Tailwind CSS IntelliSense'
@@ -152,6 +153,7 @@
client.onReady().then(() => {
let emitter = createEmitter(client)
registerConfigErrorHandler(emitter)
+ registerColorDecorator(client, context, emitter)
onMessage(client, 'getConfiguration', async (scope) => {
return Workspace.getConfiguration('tailwindCSS', scope)
})
diff --git a/src/lib/registerColorDecorator.ts b/src/lib/registerColorDecorator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66d6767ee428934b351b9ccdcd884c0f236477db
--- /dev/null
+++ b/src/lib/registerColorDecorator.ts
@@ -0,0 +1,132 @@
+import { window, workspace, ExtensionContext, TextEditor } from 'vscode'
+import { NotificationEmitter } from './emitter'
+import { LanguageClient } from 'vscode-languageclient'
+import debounce from 'debounce'
+
+const colorDecorationType = window.createTextEditorDecorationType({
+ before: {
+ width: '0.8em',
+ height: '0.8em',
+ contentText: ' ',
+ border: '0.1em solid',
+ margin: '0.1em 0.2em 0',
+ },
+ dark: {
+ before: {
+ borderColor: '#eeeeee',
+ },
+ },
+ light: {
+ before: {
+ borderColor: '#000000',
+ },
+ },
+})
+
+export function registerColorDecorator(
+ client: LanguageClient,
+ context: ExtensionContext,
+ emitter: NotificationEmitter
+) {
+ let activeEditor = window.activeTextEditor
+
+ async function updateDecorations() {
+ return updateDecorationsInEditor(activeEditor)
+ }
+
+ async function updateDecorationsInEditor(editor: TextEditor) {
+ if (!editor) return
+ if (editor.document.uri.scheme !== 'file') return
+
+ let workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri)
+ if (
+ !workspaceFolder ||
+ workspaceFolder.uri.toString() !==
+ client.clientOptions.workspaceFolder.uri.toString()
+ ) {
+ return
+ }
+
+ let preference =
+ workspace.getConfiguration('tailwindCSS', editor.document)
+ .colorDecorators || 'inherit'
+
+ let enabled: boolean =
+ preference === 'inherit'
+ ? Boolean(workspace.getConfiguration('editor').colorDecorators)
+ : preference === 'on'
+
+ if (!enabled) {
+ editor.setDecorations(colorDecorationType, [])
+ return
+ }
+
+ let { colors } = await emitter.emit('getDocumentColors', {
+ document: editor.document.uri.toString(),
+ })
+
+ editor.setDecorations(
+ colorDecorationType,
+ colors
+ .filter(({ color }) => color !== 'rgba(0, 0, 0, 0.01)')
+ .map(({ range, color }) => ({
+ range,
+ renderOptions: { before: { backgroundColor: color } },
+ }))
+ )
+ }
+
+ const triggerUpdateDecorations = debounce(updateDecorations, 200)
+
+ if (activeEditor) {
+ triggerUpdateDecorations()
+ }
+
+ window.onDidChangeActiveTextEditor(
+ (editor) => {
+ activeEditor = editor
+ if (editor) {
+ triggerUpdateDecorations()
+ }
+ },
+ null,
+ context.subscriptions
+ )
+
+ workspace.onDidChangeTextDocument(
+ (event) => {
+ if (activeEditor && event.document === activeEditor.document) {
+ triggerUpdateDecorations()
+ }
+ },
+ null,
+ context.subscriptions
+ )
+
+ workspace.onDidOpenTextDocument(
+ (document) => {
+ if (activeEditor && document === activeEditor.document) {
+ triggerUpdateDecorations()
+ }
+ },
+ null,
+ context.subscriptions
+ )
+
+ workspace.onDidChangeConfiguration((e) => {
+ if (
+ e.affectsConfiguration('editor.colorDecorators') ||
+ e.affectsConfiguration('tailwindCSS.colorDecorators')
+ ) {
+ window.visibleTextEditors.forEach(updateDecorationsInEditor)
+ }
+ })
+
+ emitter.on('configUpdated', () => {
+ window.visibleTextEditors.forEach(updateDecorationsInEditor)
+ })
+
+ emitter.on('configError', () => {
+ window.visibleTextEditors.forEach(updateDecorationsInEditor)
+ })
+}
diff --git a/src/lsp/providers/documentColorProvider.ts b/src/lsp/providers/documentColorProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..688ee746ab6a01dca0217c5515e14e599bb1f235
--- /dev/null
+++ b/src/lsp/providers/documentColorProvider.ts
@@ -0,0 +1,49 @@
+import { onMessage } from '../notifications'
+import { State } from '../util/state'
+import {
+ findClassListsInDocument,
+ getClassNamesInClassList,
+ findHelperFunctionsInDocument,
+} from '../util/find'
+import { getClassNameParts } from '../util/getClassNameAtPosition'
+import { getColor, getColorFromValue } from '../util/color'
+import { stringToPath } from '../util/stringToPath'
+const dlv = require('dlv')
+
+export function registerDocumentColorProvider(state: State) {
+ onMessage(
+ state.editor.connection,
+ 'getDocumentColors',
+ async ({ document }) => {
+ let colors = []
+ if (!state.enabled) return { colors }
+ let doc = state.editor.documents.get(document)
+ if (!doc) return { colors }
+
+ let classLists = findClassListsInDocument(state, doc)
+ classLists.forEach((classList) => {
+ let classNames = getClassNamesInClassList(classList)
+ classNames.forEach((className) => {
+ let parts = getClassNameParts(state, className.className)
+ if (!parts) return
+ let color = getColor(state, parts)
+ if (!color) return
+ colors.push({ range: className.range, color: color.documentation })
+ })
+ })
+
+ let helperFns = findHelperFunctionsInDocument(state, doc)
+ helperFns.forEach((fn) => {
+ let keys = stringToPath(fn.value)
+ let base = fn.helper === 'theme' ? ['theme'] : []
+ let value = dlv(state.config, [...base, ...keys])
+ let color = getColorFromValue(value)
+ if (color) {
+ colors.push({ range: fn.valueRange, color })
+ }
+ })
+
+ return { colors }
+ }
+ )
+}
diff --git a/src/lsp/server.ts b/src/lsp/server.ts
index 4b149f82663e120586d51e1026c6f9f8eaf552f7..c4cb2d1dab02b93616d6f761d90e1ef7050dadd9 100644
--- a/src/lsp/server.ts
+++ b/src/lsp/server.ts
@@ -35,9 +35,10 @@ clearAllDiagnostics,
} from './providers/diagnostics/diagnosticsProvider'
import { createEmitter } from '../lib/emitter'
import { provideCodeActions } from './providers/codeActions/codeActionProvider'
+import { registerDocumentColorProvider } from './providers/documentColorProvider'
let connection = createConnection(ProposedFeatures.all)
-let state: State = { enabled: false, emitter: createEmitter(connection) }
+const state: State = { enabled: false, emitter: createEmitter(connection) }
let documents = new TextDocuments()
let workspaceFolder: string | null
@@ -73,7 +74,7 @@ connection.onInitialize(
async (params: InitializeParams): Promise<InitializeResult> => {
const capabilities = params.capabilities
-import {
+ CodeActionParams,
createConnection,
connection,
documents,
@@ -100,13 +101,9 @@ {
// @ts-ignore
onChange: (newState: State): void => {
if (newState && !newState.error) {
- state = {
- ...newState,
- enabled: true,
- emitter: state.emitter,
- editor: editorState,
+/* --------------------------------------------------------------------------------------------
ProposedFeatures,
- createConnection,
+ TextDocuments,
connection.sendNotification('tailwindcss/configUpdated', [
state.configPath,
state.config,
@@ -114,14 +111,9 @@ state.plugins,
])
updateAllDiagnostics(state)
} else {
- state = {
/* --------------------------------------------------------------------------------------------
-
ProposedFeatures,
-
ProposedFeatures,
-import {
- }
if (newState && newState.error) {
const payload: {
message: string
@@ -145,21 +137,11 @@ }
)
if (tailwindState) {
-/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
- createConnection,
- enabled: true,
- emitter: state.emitter,
- InitializeResult,
InitializeResult,
-/* --------------------------------------------------------------------------------------------
-/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
- * ------------------------------------------------------------------------------------------ */
- InitializeResult,
* Copyright (c) Microsoft Corporation. All rights reserved.
/* --------------------------------------------------------------------------------------------
-} from './providers/diagnostics/diagnosticsProvider'
}
return {
@@ -206,6 +187,8 @@ state.configPath,
state.config,
state.plugins,
])
+
+ registerDocumentColorProvider(state)
})
connection.onDidChangeConfiguration((change) => {
diff --git a/src/lsp/util/color.ts b/src/lsp/util/color.ts
index 95d54170555430e9334be51c46aee88876164365..31d56ec33a369f671737f625a3d61c8960ca7d4b 100644
--- a/src/lsp/util/color.ts
+++ b/src/lsp/util/color.ts
@@ -48,21 +48,50 @@ propsToCheck.map((prop) => ensureArray(item[prop]).map(createColor))
)
// check that all of the values are valid colors
- if (colors.some((color) => !color.isValid)) {
+ if (colors.some((color) => color !== 'transparent' && !color.isValid)) {
return null
}
+const COLOR_PROPS = [
import { TinyColor } from '@ctrl/tinycolor'
+ const colorStrings = dedupe(
+const COLOR_PROPS = [
-import { TinyColor } from '@ctrl/tinycolor'
const COLOR_PROPS = [
+const COLOR_PROPS = [
+ ? 'transparent'
+ : `${color.r}-${color.g}-${color.b}`
+ )
import { TinyColor } from '@ctrl/tinycolor'
+import removeMeta from './removeMeta'
'caret-color',
+const dlv = require('dlv')
return null
}
+ if (colorStrings[0] === 'transparent') {
+ return {
+ 'caret-color',
import { TinyColor } from '@ctrl/tinycolor'
+ }
+import removeMeta from './removeMeta'
'color',
+
+ const nonTransparentColors = colors.filter(
+ (color): color is TinyColor => color !== 'transparent'
+ )
+
+ const alphas = dedupe(nonTransparentColors.map((color) => color.a))
+
+ if (alphas.length === 1 || (alphas.length === 2 && alphas.includes(0))) {
+ return {
+ documentation: nonTransparentColors
+ .find((color) => color.a !== 0)
+ .toRgbString(),
+ }
+ }
+
+ return null
}
export function getColorFromValue(value: unknown): string {
@@ -77,10 +106,10 @@ }
return null
}
-import { ensureArray, dedupe, flatten } from '../../util/array'
'color',
+import removeMeta from './removeMeta'
if (str === 'transparent') {
- return new TinyColor({ r: 0, g: 0, b: 0, a: 0.01 })
+ return 'transparent'
}
// matches: rgba(<r>, <g>, <b>, var(--bg-opacity))
diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts
index db5609e3f06571bde42d9a555268d95f6318fc47..8ff4dedcaaa9d3ab1e20e53208b641fde0bf8a89 100644
--- a/src/lsp/util/find.ts
+++ b/src/lsp/util/find.ts
@@ -1,5 +1,11 @@
import { TextDocument, Range, Position } from 'vscode-languageserver'
+import {
import { DocumentClassName, DocumentClassList, State } from './state'
+ getClassAttributeLexer,
+ DocumentClassList,
+ State,
+ DocumentHelperFunction,
+} from './state'
import lineColumn from 'line-column'
import { isCssContext, isCssDoc } from './css'
import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
@@ -11,6 +17,7 @@ getClassAttributeLexer,
getComputedClassAttributeLexer,
} from './lexers'
import { getLanguageBoundaries } from './getLanguageBoundaries'
+import { resolveRange } from './resolveRange'
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
let match: RegExpMatchArray
@@ -252,6 +259,64 @@ return flatten([
...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)),
...boundaries.css.map((range) => findClassListsInCssRange(doc, range)),
])
+}
+
+export function findHelperFunctionsInDocument(
+ state: State,
+ doc: TextDocument
+): DocumentHelperFunction[] {
+ if (isCssDoc(state, doc)) {
+ return findHelperFunctionsInRange(doc)
+ }
+
+ let boundaries = getLanguageBoundaries(state, doc)
+ if (!boundaries) return []
+
+ return flatten(
+ boundaries.css.map((range) => findHelperFunctionsInRange(doc, range))
+ )
+}
+
+export function findHelperFunctionsInRange(
+ doc: TextDocument,
+ range?: Range
+): DocumentHelperFunction[] {
+ const text = doc.getText(range)
+ const matches = findAll(
+ /(?<before>^|\s)(?<helper>theme|config)\((?:(?<single>')([^']+)'|(?<double>")([^"]+)")\)/gm,
+ text
+ )
+
+ return matches.map((match) => {
+ let value = match[4] || match[6]
+ let startIndex = match.index + match.groups.before.length
+ return {
+ full: match[0].substr(match.groups.before.length),
+ value,
+ helper: match.groups.helper === 'theme' ? 'theme' : 'config',
+ quotes: match.groups.single ? "'" : '"',
+ range: resolveRange(
+ {
+ start: indexToPosition(text, startIndex),
+ end: indexToPosition(text, match.index + match[0].length),
+ },
+ range
+ ),
+ valueRange: resolveRange(
+ {
+ start: indexToPosition(
+ text,
+ startIndex + match.groups.helper.length + 1
+ ),
+ end: indexToPosition(
+ text,
+ startIndex + match.groups.helper.length + 1 + 1 + value.length + 1
+ ),
+ },
+ range
+ ),
+ }
+ })
}
export function indexToPosition(str: string, index: number): Position {
diff --git a/src/lsp/util/resolveRange.ts b/src/lsp/util/resolveRange.ts
new file mode 100644
index 0000000000000000000000000000000000000000..96fe343afd5328b288c76f2fdc26e1d08ea1b004
--- /dev/null
+++ b/src/lsp/util/resolveRange.ts
@@ -0,0 +1,18 @@
+import { Range } from 'vscode-languageserver'
+
+export function resolveRange(range: Range, relativeTo?: Range) {
+ return {
+ start: {
+ line: (relativeTo?.start.line || 0) + range.start.line,
+ character:
+ (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) +
+ range.start.character,
+ },
+ end: {
+ line: (relativeTo?.start.line || 0) + range.end.line,
+ character:
+ (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) +
+ range.end.character,
+ },
+ }
+}
diff --git a/src/lsp/util/state.ts b/src/lsp/util/state.ts
index 47b976d397b70e7ac0e0d26a42ee76cb54f40374..8369e920fc74458c375f3cf1f6debd7b1a0f5350 100644
--- a/src/lsp/util/state.ts
+++ b/src/lsp/util/state.ts
@@ -75,6 +75,15 @@ relativeRange: Range
classList: DocumentClassList
}
+export type DocumentHelperFunction = {
+ full: string
+ helper: 'theme' | 'config'
+ value: string
+ quotes: '"' | "'"
+ range: Range
+ valueRange: Range
+}
+
export type ClassNameMeta = {
source: 'base' | 'components' | 'utilities'
pseudo: string[]