tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
Add initial color decorators
8 changed files, 300 additions(+), 0 deletions(-)
diff --git a/package.json b/package.json
index 0cdb709c6539c495c4114edaaf2222479b237252..13c2d39a2339a7689dcc9d6977af087000115b06 100755
--- a/package.json
+++ b/package.json
@@ -71,6 +71,21 @@ },
"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.enabled": {
+ "type": "boolean",
+ "default": true,
+ "scope": "language-overridable"
+ },
+ "tailwindCSS.colorDecorators.classes": {
+ "type": "boolean",
+ "default": true,
+ "scope": "language-overridable"
+ },
+ "tailwindCSS.colorDecorators.cssHelpers": {
+ "type": "boolean",
+ "default": true,
+ "scope": "language-overridable"
+ },
"tailwindCSS.validate": {
"type": "boolean",
"default": true,
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..cb01a7bb366da75bd8453c6f90d5a79b312f4daa
--- /dev/null
+++ b/src/lib/registerColorDecorator.ts
@@ -0,0 +1,125 @@
+import { window, workspace, ExtensionContext, TextEditor } from 'vscode'
+import { NotificationEmitter } from './emitter'
+import { LanguageClient } from 'vscode-languageclient'
+
+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
+ let timeout: NodeJS.Timer | undefined = undefined
+
+ 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 settings = workspace.getConfiguration(
+ 'tailwindCSS.colorDecorators',
+ editor.document
+ )
+
+ if (settings.enabled !== true) {
+ editor.setDecorations(colorDecorationType, [])
+ return
+ }
+
+ let { colors } = await emitter.emit('getDocumentColors', {
+ document: editor.document.uri.toString(),
+ classes: settings.classes,
+ cssHelpers: settings.cssHelpers,
+ })
+
+ editor.setDecorations(
+ colorDecorationType,
+ colors
+ .filter(({ color }) => color !== 'rgba(0, 0, 0, 0.01)')
+ .map(({ range, color }) => ({
+ range,
+ renderOptions: { before: { backgroundColor: color } },
+ }))
+ )
+ }
+
+ function triggerUpdateDecorations() {
+ if (timeout) {
+ clearTimeout(timeout)
+ timeout = undefined
+ }
+ timeout = setTimeout(updateDecorations, 500)
+ }
+
+ 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('tailwindCSS.colorDecorators')) {
+ window.visibleTextEditors.forEach(updateDecorationsInEditor)
+ }
+ })
+}
diff --git a/src/lsp/providers/documentColorProvider.ts b/src/lsp/providers/documentColorProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5c9e9e34917301c3ba93b1cd70a8f4bf3ce6e9a9
--- /dev/null
+++ b/src/lsp/providers/documentColorProvider.ts
@@ -0,0 +1,63 @@
+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 { logFull } from '../util/logFull'
+import { stringToPath } from '../util/stringToPath'
+const dlv = require('dlv')
+
+export function registerDocumentColorProvider(state: State) {
+ onMessage(
+ state.editor.connection,
+ 'getDocumentColors',
+ async ({ document, classes, cssHelpers }) => {
+ let colors = []
+ let doc = state.editor.documents.get(document)
+ if (!doc) return { colors }
+
+ if (classes) {
+ 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 })
+ })
+ })
+ }
+
+ if (cssHelpers) {
+ 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: {
+ // start: {
+ // line: fn.valueRange.start.line,
+ // character: fn.valueRange.start.character + 1,
+ // },
+ // end: fn.valueRange.end,
+ // },
+ // color,
+ // })
+ colors.push({ range: fn.valueRange, color })
+ }
+ })
+ }
+
+ return { colors }
+ }
+ )
+}
diff --git a/src/lsp/server.ts b/src/lsp/server.ts
index 4b149f82663e120586d51e1026c6f9f8eaf552f7..bd7cc22733c16d072d3935776c5c4d24c857855b 100644
--- a/src/lsp/server.ts
+++ b/src/lsp/server.ts
@@ -35,6 +35,7 @@ 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) }
@@ -195,6 +196,8 @@ state.configPath,
state.config,
state.plugins,
])
+
+ registerDocumentColorProvider(state)
})
connection.onDidChangeConfiguration((change) => {
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 09a02006721913e72c3a576ee1c30f997476c6b5..77afdfad4fd9faecb028f0ef8b6079fc04c34d40 100644
--- a/src/lsp/util/state.ts
+++ b/src/lsp/util/state.ts
@@ -74,6 +74,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[]