Add initial color decorators

master
Brad Cornes 2020-08-12 18:45:36 +01:00
parent 81446acdb3
commit f262bbbe92
8 changed files with 300 additions and 1 deletions

View File

@ -71,6 +71,21 @@
"default": {}, "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\"}`" "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": { "tailwindCSS.validate": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,

View File

@ -24,6 +24,7 @@ import isObject from './util/isObject'
import { dedupe, equal } from './util/array' import { dedupe, equal } from './util/array'
import { createEmitter } from './lib/emitter' import { createEmitter } from './lib/emitter'
import { onMessage } from './lsp/notifications' import { onMessage } from './lsp/notifications'
import { registerColorDecorator } from './lib/registerColorDecorator'
const CLIENT_ID = 'tailwindcss-intellisense' const CLIENT_ID = 'tailwindcss-intellisense'
const CLIENT_NAME = 'Tailwind CSS IntelliSense' const CLIENT_NAME = 'Tailwind CSS IntelliSense'
@ -152,6 +153,7 @@ export function activate(context: ExtensionContext) {
client.onReady().then(() => { client.onReady().then(() => {
let emitter = createEmitter(client) let emitter = createEmitter(client)
registerConfigErrorHandler(emitter) registerConfigErrorHandler(emitter)
registerColorDecorator(client, context, emitter)
onMessage(client, 'getConfiguration', async (scope) => { onMessage(client, 'getConfiguration', async (scope) => {
return Workspace.getConfiguration('tailwindCSS', scope) return Workspace.getConfiguration('tailwindCSS', scope)
}) })

View File

@ -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)
}
})
}

View File

@ -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 }
}
)
}

View File

@ -35,6 +35,7 @@ import {
} from './providers/diagnostics/diagnosticsProvider' } from './providers/diagnostics/diagnosticsProvider'
import { createEmitter } from '../lib/emitter' import { createEmitter } from '../lib/emitter'
import { provideCodeActions } from './providers/codeActions/codeActionProvider' import { provideCodeActions } from './providers/codeActions/codeActionProvider'
import { registerDocumentColorProvider } from './providers/documentColorProvider'
let connection = createConnection(ProposedFeatures.all) let connection = createConnection(ProposedFeatures.all)
let state: State = { enabled: false, emitter: createEmitter(connection) } let state: State = { enabled: false, emitter: createEmitter(connection) }
@ -195,6 +196,8 @@ connection.onInitialized &&
state.config, state.config,
state.plugins, state.plugins,
]) ])
registerDocumentColorProvider(state)
}) })
connection.onDidChangeConfiguration((change) => { connection.onDidChangeConfiguration((change) => {

View File

@ -1,5 +1,10 @@
import { TextDocument, Range, Position } from 'vscode-languageserver' 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 lineColumn from 'line-column'
import { isCssContext, isCssDoc } from './css' import { isCssContext, isCssDoc } from './css'
import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html' import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
@ -11,6 +16,7 @@ import {
getComputedClassAttributeLexer, getComputedClassAttributeLexer,
} from './lexers' } from './lexers'
import { getLanguageBoundaries } from './getLanguageBoundaries' import { getLanguageBoundaries } from './getLanguageBoundaries'
import { resolveRange } from './resolveRange'
export function findAll(re: RegExp, str: string): RegExpMatchArray[] { export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
let match: 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 { export function indexToPosition(str: string, index: number): Position {
const { line, col } = lineColumn(str + '\n', index) const { line, col } = lineColumn(str + '\n', index)
return { line: line - 1, character: col - 1 } return { line: line - 1, character: col - 1 }

View File

@ -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,
},
}
}

View File

@ -74,6 +74,15 @@ export type DocumentClassName = {
classList: DocumentClassList classList: DocumentClassList
} }
export type DocumentHelperFunction = {
full: string
helper: 'theme' | 'config'
value: string
quotes: '"' | "'"
range: Range
valueRange: Range
}
export type ClassNameMeta = { export type ClassNameMeta = {
source: 'base' | 'components' | 'utilities' source: 'base' | 'components' | 'utilities'
pseudo: string[] pseudo: string[]