Add initial color decorators
parent
81446acdb3
commit
f262bbbe92
15
package.json
15
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,
|
||||
|
|
|
@ -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 @@ export function activate(context: ExtensionContext) {
|
|||
client.onReady().then(() => {
|
||||
let emitter = createEmitter(client)
|
||||
registerConfigErrorHandler(emitter)
|
||||
registerColorDecorator(client, context, emitter)
|
||||
onMessage(client, 'getConfiguration', async (scope) => {
|
||||
return Workspace.getConfiguration('tailwindCSS', scope)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
)
|
||||
}
|
|
@ -35,6 +35,7 @@ import {
|
|||
} 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 @@ connection.onInitialized &&
|
|||
state.config,
|
||||
state.plugins,
|
||||
])
|
||||
|
||||
registerDocumentColorProvider(state)
|
||||
})
|
||||
|
||||
connection.onDidChangeConfiguration((change) => {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { TextDocument, Range, Position } from 'vscode-languageserver'
|
||||
import { DocumentClassName, DocumentClassList, State } from './state'
|
||||
import {
|
||||
DocumentClassName,
|
||||
DocumentClassList,
|
||||
State,
|
||||
DocumentHelperFunction,
|
||||
} from './state'
|
||||
import lineColumn from 'line-column'
|
||||
import { isCssContext, isCssDoc } from './css'
|
||||
import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
|
||||
|
@ -11,6 +16,7 @@ import {
|
|||
getComputedClassAttributeLexer,
|
||||
} from './lexers'
|
||||
import { getLanguageBoundaries } from './getLanguageBoundaries'
|
||||
import { resolveRange } from './resolveRange'
|
||||
|
||||
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
|
||||
let match: RegExpMatchArray
|
||||
|
@ -254,6 +260,64 @@ export function findClassListsInDocument(
|
|||
])
|
||||
}
|
||||
|
||||
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 {
|
||||
const { line, col } = lineColumn(str + '\n', index)
|
||||
return { line: line - 1, character: col - 1 }
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -74,6 +74,15 @@ export type DocumentClassName = {
|
|||
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[]
|
||||
|
|
Loading…
Reference in New Issue