Skip to content

Commit 9cc7bee

Browse files
committed
Merge branch 'color-decorators'
2 parents 2aa39c8 + d955334 commit 9cc7bee

10 files changed

+347
-28
lines changed

package-lock.json

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+18
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@
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": {
75+
"type": "string",
76+
"enum": [
77+
"inherit",
78+
"on",
79+
"off"
80+
],
81+
"markdownEnumDescriptions": [
82+
"Color decorators are rendered if `editor.colorDecorators` is enabled.",
83+
"Color decorators are rendered.",
84+
"Color decorators are not rendered."
85+
],
86+
"default": "inherit",
87+
"markdownDescription": "Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.",
88+
"scope": "language-overridable"
89+
},
7490
"tailwindCSS.validate": {
7591
"type": "boolean",
7692
"default": true,
@@ -157,6 +173,7 @@
157173
},
158174
"devDependencies": {
159175
"@ctrl/tinycolor": "^3.1.0",
176+
"@types/debounce": "^1.2.0",
160177
"@types/mocha": "^5.2.0",
161178
"@types/moo": "^0.5.3",
162179
"@types/node": "^13.9.3",
@@ -166,6 +183,7 @@
166183
"chokidar": "^3.3.1",
167184
"concurrently": "^5.1.0",
168185
"css.escape": "^1.5.1",
186+
"debounce": "^1.2.0",
169187
"detect-indent": "^6.0.0",
170188
"dlv": "^1.1.3",
171189
"dset": "^2.0.1",

src/extension.ts

+2
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

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

src/lsp/server.ts

+9-20
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ 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)
40-
let state: State = { enabled: false, emitter: createEmitter(connection) }
41+
const state: State = { enabled: false, emitter: createEmitter(connection) }
4142
let documents = new TextDocuments()
4243
let workspaceFolder: string | null
4344

@@ -73,7 +74,7 @@ connection.onInitialize(
7374
async (params: InitializeParams): Promise<InitializeResult> => {
7475
const capabilities = params.capabilities
7576

76-
const editorState: EditorState = {
77+
state.editor = {
7778
connection,
7879
documents,
7980
documentSettings,
@@ -99,24 +100,15 @@ connection.onInitialize(
99100
// @ts-ignore
100101
onChange: (newState: State): void => {
101102
if (newState && !newState.error) {
102-
state = {
103-
...newState,
104-
enabled: true,
105-
emitter: state.emitter,
106-
editor: editorState,
107-
}
103+
Object.assign(state, newState, { enabled: true })
108104
connection.sendNotification('tailwindcss/configUpdated', [
109105
state.configPath,
110106
state.config,
111107
state.plugins,
112108
])
113109
updateAllDiagnostics(state)
114110
} else {
115-
state = {
116-
enabled: false,
117-
emitter: state.emitter,
118-
editor: editorState,
119-
}
111+
state.enabled = false
120112
if (newState && newState.error) {
121113
const payload: {
122114
message: string
@@ -140,14 +132,9 @@ connection.onInitialize(
140132
)
141133

142134
if (tailwindState) {
143-
state = {
144-
enabled: true,
145-
emitter: state.emitter,
146-
editor: editorState,
147-
...tailwindState,
148-
}
135+
Object.assign(state, tailwindState, { enabled: true })
149136
} else {
150-
state = { enabled: false, emitter: state.emitter, editor: editorState }
137+
state.enabled = false
151138
}
152139

153140
return {
@@ -195,6 +182,8 @@ connection.onInitialized &&
195182
state.config,
196183
state.plugins,
197184
])
185+
186+
registerDocumentColorProvider(state)
198187
})
199188

200189
connection.onDidChangeConfiguration((change) => {

src/lsp/util/color.ts

+33-7
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,43 @@ export function getColor(
4848
)
4949

5050
// check that all of the values are valid colors
51-
if (colors.some((color) => !color.isValid)) {
51+
if (colors.some((color) => color !== 'transparent' && !color.isValid)) {
5252
return null
5353
}
5454

55-
// check that all of the values are the same color
56-
const colorStrings = colors.map((color) => color.toRgbString())
57-
if (dedupe(colorStrings).length !== 1) {
55+
// check that all of the values are the same color, ignoring alpha
56+
const colorStrings = dedupe(
57+
colors.map((color) =>
58+
color === 'transparent'
59+
? 'transparent'
60+
: `${color.r}-${color.g}-${color.b}`
61+
)
62+
)
63+
if (colorStrings.length !== 1) {
5864
return null
5965
}
6066

61-
return { documentation: colorStrings[0] }
67+
if (colorStrings[0] === 'transparent') {
68+
return {
69+
documentation: 'rgba(0, 0, 0, 0.01)',
70+
}
71+
}
72+
73+
const nonTransparentColors = colors.filter(
74+
(color): color is TinyColor => color !== 'transparent'
75+
)
76+
77+
const alphas = dedupe(nonTransparentColors.map((color) => color.a))
78+
79+
if (alphas.length === 1 || (alphas.length === 2 && alphas.includes(0))) {
80+
return {
81+
documentation: nonTransparentColors
82+
.find((color) => color.a !== 0)
83+
.toRgbString(),
84+
}
85+
}
86+
87+
return null
6288
}
6389

6490
export function getColorFromValue(value: unknown): string {
@@ -73,9 +99,9 @@ export function getColorFromValue(value: unknown): string {
7399
return null
74100
}
75101

76-
function createColor(str: string): TinyColor {
102+
function createColor(str: string): TinyColor | 'transparent' {
77103
if (str === 'transparent') {
78-
return new TinyColor({ r: 0, g: 0, b: 0, a: 0.01 })
104+
return 'transparent'
79105
}
80106

81107
// matches: rgba(<r>, <g>, <b>, var(--bg-opacity))

0 commit comments

Comments
 (0)