Skip to content

Commit f262bbb

Browse files
committed
Add initial color decorators
1 parent 81446ac commit f262bbb

File tree

8 files changed

+300
-1
lines changed

8 files changed

+300
-1
lines changed

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@
7171
"default": {},
7272
"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\"}`"
7373
},
74+
"tailwindCSS.colorDecorators.enabled": {
75+
"type": "boolean",
76+
"default": true,
77+
"scope": "language-overridable"
78+
},
79+
"tailwindCSS.colorDecorators.classes": {
80+
"type": "boolean",
81+
"default": true,
82+
"scope": "language-overridable"
83+
},
84+
"tailwindCSS.colorDecorators.cssHelpers": {
85+
"type": "boolean",
86+
"default": true,
87+
"scope": "language-overridable"
88+
},
7489
"tailwindCSS.validate": {
7590
"type": "boolean",
7691
"default": true,

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import isObject from './util/isObject'
2424
import { dedupe, equal } from './util/array'
2525
import { createEmitter } from './lib/emitter'
2626
import { onMessage } from './lsp/notifications'
27+
import { registerColorDecorator } from './lib/registerColorDecorator'
2728

2829
const CLIENT_ID = 'tailwindcss-intellisense'
2930
const CLIENT_NAME = 'Tailwind CSS IntelliSense'
@@ -152,6 +153,7 @@ export function activate(context: ExtensionContext) {
152153
client.onReady().then(() => {
153154
let emitter = createEmitter(client)
154155
registerConfigErrorHandler(emitter)
156+
registerColorDecorator(client, context, emitter)
155157
onMessage(client, 'getConfiguration', async (scope) => {
156158
return Workspace.getConfiguration('tailwindCSS', scope)
157159
})

src/lib/registerColorDecorator.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { window, workspace, ExtensionContext, TextEditor } from 'vscode'
2+
import { NotificationEmitter } from './emitter'
3+
import { LanguageClient } from 'vscode-languageclient'
4+
5+
const colorDecorationType = window.createTextEditorDecorationType({
6+
before: {
7+
width: '0.8em',
8+
height: '0.8em',
9+
contentText: ' ',
10+
border: '0.1em solid',
11+
margin: '0.1em 0.2em 0',
12+
},
13+
dark: {
14+
before: {
15+
borderColor: '#eeeeee',
16+
},
17+
},
18+
light: {
19+
before: {
20+
borderColor: '#000000',
21+
},
22+
},
23+
})
24+
25+
export function registerColorDecorator(
26+
client: LanguageClient,
27+
context: ExtensionContext,
28+
emitter: NotificationEmitter
29+
) {
30+
let activeEditor = window.activeTextEditor
31+
let timeout: NodeJS.Timer | undefined = undefined
32+
33+
async function updateDecorations() {
34+
return updateDecorationsInEditor(activeEditor)
35+
}
36+
37+
async function updateDecorationsInEditor(editor: TextEditor) {
38+
if (!editor) return
39+
if (editor.document.uri.scheme !== 'file') return
40+
41+
let workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri)
42+
if (
43+
!workspaceFolder ||
44+
workspaceFolder.uri.toString() !==
45+
client.clientOptions.workspaceFolder.uri.toString()
46+
) {
47+
return
48+
}
49+
50+
let settings = workspace.getConfiguration(
51+
'tailwindCSS.colorDecorators',
52+
editor.document
53+
)
54+
55+
if (settings.enabled !== true) {
56+
editor.setDecorations(colorDecorationType, [])
57+
return
58+
}
59+
60+
let { colors } = await emitter.emit('getDocumentColors', {
61+
document: editor.document.uri.toString(),
62+
classes: settings.classes,
63+
cssHelpers: settings.cssHelpers,
64+
})
65+
66+
editor.setDecorations(
67+
colorDecorationType,
68+
colors
69+
.filter(({ color }) => color !== 'rgba(0, 0, 0, 0.01)')
70+
.map(({ range, color }) => ({
71+
range,
72+
renderOptions: { before: { backgroundColor: color } },
73+
}))
74+
)
75+
}
76+
77+
function triggerUpdateDecorations() {
78+
if (timeout) {
79+
clearTimeout(timeout)
80+
timeout = undefined
81+
}
82+
timeout = setTimeout(updateDecorations, 500)
83+
}
84+
85+
if (activeEditor) {
86+
triggerUpdateDecorations()
87+
}
88+
89+
window.onDidChangeActiveTextEditor(
90+
(editor) => {
91+
activeEditor = editor
92+
if (editor) {
93+
triggerUpdateDecorations()
94+
}
95+
},
96+
null,
97+
context.subscriptions
98+
)
99+
100+
workspace.onDidChangeTextDocument(
101+
(event) => {
102+
if (activeEditor && event.document === activeEditor.document) {
103+
triggerUpdateDecorations()
104+
}
105+
},
106+
null,
107+
context.subscriptions
108+
)
109+
110+
workspace.onDidOpenTextDocument(
111+
(document) => {
112+
if (activeEditor && document === activeEditor.document) {
113+
triggerUpdateDecorations()
114+
}
115+
},
116+
null,
117+
context.subscriptions
118+
)
119+
120+
workspace.onDidChangeConfiguration((e) => {
121+
if (e.affectsConfiguration('tailwindCSS.colorDecorators')) {
122+
window.visibleTextEditors.forEach(updateDecorationsInEditor)
123+
}
124+
})
125+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { onMessage } from '../notifications'
2+
import { State } from '../util/state'
3+
import {
4+
findClassListsInDocument,
5+
getClassNamesInClassList,
6+
findHelperFunctionsInDocument,
7+
} from '../util/find'
8+
import { getClassNameParts } from '../util/getClassNameAtPosition'
9+
import { getColor, getColorFromValue } from '../util/color'
10+
import { logFull } from '../util/logFull'
11+
import { stringToPath } from '../util/stringToPath'
12+
const dlv = require('dlv')
13+
14+
export function registerDocumentColorProvider(state: State) {
15+
onMessage(
16+
state.editor.connection,
17+
'getDocumentColors',
18+
async ({ document, classes, cssHelpers }) => {
19+
let colors = []
20+
let doc = state.editor.documents.get(document)
21+
if (!doc) return { colors }
22+
23+
if (classes) {
24+
let classLists = findClassListsInDocument(state, doc)
25+
classLists.forEach((classList) => {
26+
let classNames = getClassNamesInClassList(classList)
27+
classNames.forEach((className) => {
28+
let parts = getClassNameParts(state, className.className)
29+
if (!parts) return
30+
let color = getColor(state, parts)
31+
if (!color) return
32+
colors.push({ range: className.range, color: color.documentation })
33+
})
34+
})
35+
}
36+
37+
if (cssHelpers) {
38+
let helperFns = findHelperFunctionsInDocument(state, doc)
39+
helperFns.forEach((fn) => {
40+
let keys = stringToPath(fn.value)
41+
let base = fn.helper === 'theme' ? ['theme'] : []
42+
let value = dlv(state.config, [...base, ...keys])
43+
let color = getColorFromValue(value)
44+
if (color) {
45+
// colors.push({
46+
// range: {
47+
// start: {
48+
// line: fn.valueRange.start.line,
49+
// character: fn.valueRange.start.character + 1,
50+
// },
51+
// end: fn.valueRange.end,
52+
// },
53+
// color,
54+
// })
55+
colors.push({ range: fn.valueRange, color })
56+
}
57+
})
58+
}
59+
60+
return { colors }
61+
}
62+
)
63+
}

src/lsp/server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from './providers/diagnostics/diagnosticsProvider'
3636
import { createEmitter } from '../lib/emitter'
3737
import { provideCodeActions } from './providers/codeActions/codeActionProvider'
38+
import { registerDocumentColorProvider } from './providers/documentColorProvider'
3839

3940
let connection = createConnection(ProposedFeatures.all)
4041
let state: State = { enabled: false, emitter: createEmitter(connection) }
@@ -195,6 +196,8 @@ connection.onInitialized &&
195196
state.config,
196197
state.plugins,
197198
])
199+
200+
registerDocumentColorProvider(state)
198201
})
199202

200203
connection.onDidChangeConfiguration((change) => {

src/lsp/util/find.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { TextDocument, Range, Position } from 'vscode-languageserver'
2-
import { DocumentClassName, DocumentClassList, State } from './state'
2+
import {
3+
DocumentClassName,
4+
DocumentClassList,
5+
State,
6+
DocumentHelperFunction,
7+
} from './state'
38
import lineColumn from 'line-column'
49
import { isCssContext, isCssDoc } from './css'
510
import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
@@ -11,6 +16,7 @@ import {
1116
getComputedClassAttributeLexer,
1217
} from './lexers'
1318
import { getLanguageBoundaries } from './getLanguageBoundaries'
19+
import { resolveRange } from './resolveRange'
1420

1521
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
1622
let match: RegExpMatchArray
@@ -254,6 +260,64 @@ export function findClassListsInDocument(
254260
])
255261
}
256262

263+
export function findHelperFunctionsInDocument(
264+
state: State,
265+
doc: TextDocument
266+
): DocumentHelperFunction[] {
267+
if (isCssDoc(state, doc)) {
268+
return findHelperFunctionsInRange(doc)
269+
}
270+
271+
let boundaries = getLanguageBoundaries(state, doc)
272+
if (!boundaries) return []
273+
274+
return flatten(
275+
boundaries.css.map((range) => findHelperFunctionsInRange(doc, range))
276+
)
277+
}
278+
279+
export function findHelperFunctionsInRange(
280+
doc: TextDocument,
281+
range?: Range
282+
): DocumentHelperFunction[] {
283+
const text = doc.getText(range)
284+
const matches = findAll(
285+
/(?<before>^|\s)(?<helper>theme|config)\((?:(?<single>')([^']+)'|(?<double>")([^"]+)")\)/gm,
286+
text
287+
)
288+
289+
return matches.map((match) => {
290+
let value = match[4] || match[6]
291+
let startIndex = match.index + match.groups.before.length
292+
return {
293+
full: match[0].substr(match.groups.before.length),
294+
value,
295+
helper: match.groups.helper === 'theme' ? 'theme' : 'config',
296+
quotes: match.groups.single ? "'" : '"',
297+
range: resolveRange(
298+
{
299+
start: indexToPosition(text, startIndex),
300+
end: indexToPosition(text, match.index + match[0].length),
301+
},
302+
range
303+
),
304+
valueRange: resolveRange(
305+
{
306+
start: indexToPosition(
307+
text,
308+
startIndex + match.groups.helper.length + 1
309+
),
310+
end: indexToPosition(
311+
text,
312+
startIndex + match.groups.helper.length + 1 + 1 + value.length + 1
313+
),
314+
},
315+
range
316+
),
317+
}
318+
})
319+
}
320+
257321
export function indexToPosition(str: string, index: number): Position {
258322
const { line, col } = lineColumn(str + '\n', index)
259323
return { line: line - 1, character: col - 1 }

src/lsp/util/resolveRange.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Range } from 'vscode-languageserver'
2+
3+
export function resolveRange(range: Range, relativeTo?: Range) {
4+
return {
5+
start: {
6+
line: (relativeTo?.start.line || 0) + range.start.line,
7+
character:
8+
(range.end.line === 0 ? relativeTo?.start.character || 0 : 0) +
9+
range.start.character,
10+
},
11+
end: {
12+
line: (relativeTo?.start.line || 0) + range.end.line,
13+
character:
14+
(range.end.line === 0 ? relativeTo?.start.character || 0 : 0) +
15+
range.end.character,
16+
},
17+
}
18+
}

src/lsp/util/state.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ export type DocumentClassName = {
7474
classList: DocumentClassList
7575
}
7676

77+
export type DocumentHelperFunction = {
78+
full: string
79+
helper: 'theme' | 'config'
80+
value: string
81+
quotes: '"' | "'"
82+
range: Range
83+
valueRange: Range
84+
}
85+
7786
export type ClassNameMeta = {
7887
source: 'base' | 'components' | 'utilities'
7988
pseudo: string[]

0 commit comments

Comments
 (0)