css-variable-lsp 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.beads/issues.jsonl +3 -0
- package/.beads/metadata.json +2 -1
- package/LICENSE +623 -0
- package/package.json +2 -1
- package/server/out/colorProvider.test.js +59 -0
- package/server/out/colorProvider.test.js.map +1 -0
- package/server/out/colorService.js +346 -0
- package/server/out/colorService.js.map +1 -0
- package/server/out/cssVariableManager.js +58 -1
- package/server/out/cssVariableManager.js.map +1 -1
- package/server/out/server.js +85 -1
- package/server/out/server.js.map +1 -1
- package/server/package.json +2 -1
- package/server/src/colorFormatting.test.ts +66 -0
- package/server/src/colorProvider.test.ts +74 -0
- package/server/src/colorService.ts +363 -0
- package/server/src/cssVariableManager.ts +72 -2
- package/server/src/server.ts +99 -2
- package/server/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { Color } from 'vscode-languageserver/node';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a CSS color string into an LSP Color object.
|
|
5
|
+
* Supports hex, rgb, rgba, hsl, hsla, and named colors.
|
|
6
|
+
*/
|
|
7
|
+
export function parseColor(value: string): Color | null {
|
|
8
|
+
value = value.trim().toLowerCase();
|
|
9
|
+
|
|
10
|
+
// Hex
|
|
11
|
+
if (value.startsWith('#')) {
|
|
12
|
+
return parseHex(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// RGB / RGBA
|
|
16
|
+
if (value.startsWith('rgb')) {
|
|
17
|
+
return parseRgb(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// HSL / HSLA
|
|
21
|
+
if (value.startsWith('hsl')) {
|
|
22
|
+
return parseHsl(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Named colors
|
|
26
|
+
return parseNamedColor(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format an LSP Color object back into a CSS string.
|
|
31
|
+
* Defaults to Hex if alpha is 1, otherwise rgba.
|
|
32
|
+
*/
|
|
33
|
+
export function formatColor(color: Color): string {
|
|
34
|
+
const r = Math.round(color.red * 255);
|
|
35
|
+
const g = Math.round(color.green * 255);
|
|
36
|
+
const b = Math.round(color.blue * 255);
|
|
37
|
+
const a = color.alpha;
|
|
38
|
+
|
|
39
|
+
if (a >= 1) {
|
|
40
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
41
|
+
} else {
|
|
42
|
+
return `rgba(${r}, ${g}, ${b}, ${Number(a.toFixed(2))})`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format color as hex (with alpha if not fully opaque)
|
|
48
|
+
*/
|
|
49
|
+
export function formatColorAsHex(color: Color): string {
|
|
50
|
+
const r = Math.round(color.red * 255);
|
|
51
|
+
const g = Math.round(color.green * 255);
|
|
52
|
+
const b = Math.round(color.blue * 255);
|
|
53
|
+
const a = Math.round(color.alpha * 255);
|
|
54
|
+
|
|
55
|
+
if (a >= 255) {
|
|
56
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
57
|
+
} else {
|
|
58
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a)}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format color as rgb() or rgba()
|
|
64
|
+
*/
|
|
65
|
+
export function formatColorAsRgb(color: Color): string {
|
|
66
|
+
const r = Math.round(color.red * 255);
|
|
67
|
+
const g = Math.round(color.green * 255);
|
|
68
|
+
const b = Math.round(color.blue * 255);
|
|
69
|
+
const a = color.alpha;
|
|
70
|
+
|
|
71
|
+
if (a >= 1) {
|
|
72
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
73
|
+
} else {
|
|
74
|
+
return `rgba(${r}, ${g}, ${b}, ${Number(a.toFixed(2))})`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Format color as hsl() or hsla()
|
|
80
|
+
*/
|
|
81
|
+
export function formatColorAsHsl(color: Color): string {
|
|
82
|
+
const { h, s, l } = rgbToHsl(color.red, color.green, color.blue);
|
|
83
|
+
const a = color.alpha;
|
|
84
|
+
|
|
85
|
+
const hDeg = Math.round(h * 360);
|
|
86
|
+
const sPercent = Math.round(s * 100);
|
|
87
|
+
const lPercent = Math.round(l * 100);
|
|
88
|
+
|
|
89
|
+
if (a >= 1) {
|
|
90
|
+
return `hsl(${hDeg}, ${sPercent}%, ${lPercent}%)`;
|
|
91
|
+
} else {
|
|
92
|
+
return `hsla(${hDeg}, ${sPercent}%, ${lPercent}%, ${Number(a.toFixed(2))})`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert RGB to HSL
|
|
98
|
+
*/
|
|
99
|
+
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
|
|
100
|
+
const max = Math.max(r, g, b);
|
|
101
|
+
const min = Math.min(r, g, b);
|
|
102
|
+
const l = (max + min) / 2;
|
|
103
|
+
|
|
104
|
+
if (max === min) {
|
|
105
|
+
return { h: 0, s: 0, l };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const d = max - min;
|
|
109
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
110
|
+
|
|
111
|
+
let h: number;
|
|
112
|
+
if (max === r) {
|
|
113
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
114
|
+
} else if (max === g) {
|
|
115
|
+
h = ((b - r) / d + 2) / 6;
|
|
116
|
+
} else {
|
|
117
|
+
h = ((r - g) / d + 4) / 6;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { h, s, l };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toHex(n: number): string {
|
|
124
|
+
const hex = n.toString(16);
|
|
125
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseHex(hex: string): Color | null {
|
|
129
|
+
hex = hex.substring(1); // Remove #
|
|
130
|
+
if (hex.length === 3) {
|
|
131
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
132
|
+
}
|
|
133
|
+
if (hex.length !== 6 && hex.length !== 8) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
138
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
139
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
140
|
+
let a = 1;
|
|
141
|
+
|
|
142
|
+
if (hex.length === 8) {
|
|
143
|
+
a = parseInt(hex.substring(6, 8), 16) / 255;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { red: r, green: g, blue: b, alpha: a };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseRgb(value: string): Color | null {
|
|
150
|
+
const match = value.match(/rgba?\(([\d\s\.]+),?\s*([\d\s\.]+),?\s*([\d\s\.]+)(?:,?\s*\/?,?\s*([\d\s\.]+))?\)/);
|
|
151
|
+
if (!match) return null;
|
|
152
|
+
|
|
153
|
+
const r = parseFloat(match[1]) / 255;
|
|
154
|
+
const g = parseFloat(match[2]) / 255;
|
|
155
|
+
const b = parseFloat(match[3]) / 255;
|
|
156
|
+
let a = 1;
|
|
157
|
+
|
|
158
|
+
if (match[4]) {
|
|
159
|
+
a = parseFloat(match[4]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { red: r, green: g, blue: b, alpha: a };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseHsl(value: string): Color | null {
|
|
166
|
+
const match = value.match(/hsla?\(([\d\s\.]+)(?:deg)?,?\s*([\d\s\.]+)%?,?\s*([\d\s\.]+)%?(?:,?\s*\/?,?\s*([\d\s\.]+))?\)/);
|
|
167
|
+
if (!match) return null;
|
|
168
|
+
|
|
169
|
+
const h = parseFloat(match[1]) / 360;
|
|
170
|
+
const s = parseFloat(match[2]) / 100;
|
|
171
|
+
const l = parseFloat(match[3]) / 100;
|
|
172
|
+
let a = 1;
|
|
173
|
+
|
|
174
|
+
if (match[4]) {
|
|
175
|
+
a = parseFloat(match[4]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return hslToRgb(h, s, l, a);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function hslToRgb(h: number, s: number, l: number, a: number): Color {
|
|
182
|
+
let r, g, b;
|
|
183
|
+
|
|
184
|
+
if (s === 0) {
|
|
185
|
+
r = g = b = l; // achromatic
|
|
186
|
+
} else {
|
|
187
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
188
|
+
if (t < 0) t += 1;
|
|
189
|
+
if (t > 1) t -= 1;
|
|
190
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
191
|
+
if (t < 1 / 2) return q;
|
|
192
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
193
|
+
return p;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
197
|
+
const p = 2 * l - q;
|
|
198
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
199
|
+
g = hue2rgb(p, q, h);
|
|
200
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { red: r, green: g, blue: b, alpha: a };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseNamedColor(name: string): Color | null {
|
|
207
|
+
const colors: { [key: string]: string } = {
|
|
208
|
+
black: '#000000',
|
|
209
|
+
white: '#ffffff',
|
|
210
|
+
red: '#ff0000',
|
|
211
|
+
green: '#008000',
|
|
212
|
+
blue: '#0000ff',
|
|
213
|
+
yellow: '#ffff00',
|
|
214
|
+
cyan: '#00ffff',
|
|
215
|
+
magenta: '#ff00ff',
|
|
216
|
+
gray: '#808080',
|
|
217
|
+
grey: '#808080',
|
|
218
|
+
silver: '#c0c0c0',
|
|
219
|
+
maroon: '#800000',
|
|
220
|
+
olive: '#808000',
|
|
221
|
+
purple: '#800080',
|
|
222
|
+
teal: '#008080',
|
|
223
|
+
navy: '#000080',
|
|
224
|
+
orange: '#ffa500',
|
|
225
|
+
aliceblue: '#f0f8ff',
|
|
226
|
+
antiquewhite: '#faebd7',
|
|
227
|
+
aqua: '#00ffff',
|
|
228
|
+
aquamarine: '#7fffd4',
|
|
229
|
+
azure: '#f0ffff',
|
|
230
|
+
beige: '#f5f5dc',
|
|
231
|
+
bisque: '#ffe4c4',
|
|
232
|
+
blanchedalmond: '#ffebcd',
|
|
233
|
+
blueviolet: '#8a2be2',
|
|
234
|
+
brown: '#a52a2a',
|
|
235
|
+
burlywood: '#deb887',
|
|
236
|
+
cadetblue: '#5f9ea0',
|
|
237
|
+
chartreuse: '#7fff00',
|
|
238
|
+
chocolate: '#d2691e',
|
|
239
|
+
coral: '#ff7f50',
|
|
240
|
+
cornflowerblue: '#6495ed',
|
|
241
|
+
cornsilk: '#fff8dc',
|
|
242
|
+
crimson: '#dc143c',
|
|
243
|
+
darkblue: '#00008b',
|
|
244
|
+
darkcyan: '#008b8b',
|
|
245
|
+
darkgoldenrod: '#b8860b',
|
|
246
|
+
darkgray: '#a9a9a9',
|
|
247
|
+
darkgreen: '#006400',
|
|
248
|
+
darkgrey: '#a9a9a9',
|
|
249
|
+
darkkhaki: '#bdb76b',
|
|
250
|
+
darkmagenta: '#8b008b',
|
|
251
|
+
darkolivegreen: '#556b2f',
|
|
252
|
+
darkorange: '#ff8c00',
|
|
253
|
+
darkorchid: '#9932cc',
|
|
254
|
+
darkred: '#8b0000',
|
|
255
|
+
darksalmon: '#e9967a',
|
|
256
|
+
darkseagreen: '#8fbc8f',
|
|
257
|
+
darkslateblue: '#483d8b',
|
|
258
|
+
darkslategray: '#2f4f4f',
|
|
259
|
+
darkslategrey: '#2f4f4f',
|
|
260
|
+
darkturquoise: '#00ced1',
|
|
261
|
+
darkviolet: '#9400d3',
|
|
262
|
+
deeppink: '#ff1493',
|
|
263
|
+
deepskyblue: '#00bfff',
|
|
264
|
+
dimgray: '#696969',
|
|
265
|
+
dimgrey: '#696969',
|
|
266
|
+
dodgerblue: '#1e90ff',
|
|
267
|
+
firebrick: '#b22222',
|
|
268
|
+
floralwhite: '#fffaf0',
|
|
269
|
+
forestgreen: '#228b22',
|
|
270
|
+
fuchsia: '#ff00ff',
|
|
271
|
+
gainsboro: '#dcdcdc',
|
|
272
|
+
ghostwhite: '#f8f8ff',
|
|
273
|
+
gold: '#ffd700',
|
|
274
|
+
goldenrod: '#daa520',
|
|
275
|
+
greenyellow: '#adff2f',
|
|
276
|
+
honeydew: '#f0fff0',
|
|
277
|
+
hotpink: '#ff69b4',
|
|
278
|
+
indianred: '#cd5c5c',
|
|
279
|
+
indigo: '#4b0082',
|
|
280
|
+
ivory: '#fffff0',
|
|
281
|
+
khaki: '#f0e68c',
|
|
282
|
+
lavender: '#e6e6fa',
|
|
283
|
+
lavenderblush: '#fff0f5',
|
|
284
|
+
lawngreen: '#7cfc00',
|
|
285
|
+
lemonchiffon: '#fffacd',
|
|
286
|
+
lightblue: '#add8e6',
|
|
287
|
+
lightcoral: '#f08080',
|
|
288
|
+
lightcyan: '#e0ffff',
|
|
289
|
+
lightgoldenrodyellow: '#fafad2',
|
|
290
|
+
lightgray: '#d3d3d3',
|
|
291
|
+
lightgreen: '#90ee90',
|
|
292
|
+
lightgrey: '#d3d3d3',
|
|
293
|
+
lightpink: '#ffb6c1',
|
|
294
|
+
lightsalmon: '#ffa07a',
|
|
295
|
+
lightseagreen: '#20b2aa',
|
|
296
|
+
lightskyblue: '#87cefa',
|
|
297
|
+
lightslategray: '#778899',
|
|
298
|
+
lightslategrey: '#778899',
|
|
299
|
+
lightsteelblue: '#b0c4de',
|
|
300
|
+
lightyellow: '#ffffe0',
|
|
301
|
+
lime: '#00ff00',
|
|
302
|
+
limegreen: '#32cd32',
|
|
303
|
+
linen: '#faf0e6',
|
|
304
|
+
mediumaquamarine: '#66cdaa',
|
|
305
|
+
mediumblue: '#0000cd',
|
|
306
|
+
mediumorchid: '#ba55d3',
|
|
307
|
+
mediumpurple: '#9370db',
|
|
308
|
+
mediumseagreen: '#3cb371',
|
|
309
|
+
mediumslateblue: '#7b68ee',
|
|
310
|
+
mediumspringgreen: '#00fa9a',
|
|
311
|
+
mediumturquoise: '#48d1cc',
|
|
312
|
+
mediumvioletred: '#c71585',
|
|
313
|
+
midnightblue: '#191970',
|
|
314
|
+
mintcream: '#f5fffa',
|
|
315
|
+
mistyrose: '#ffe4e1',
|
|
316
|
+
moccasin: '#ffe4b5',
|
|
317
|
+
navajowhite: '#ffdead',
|
|
318
|
+
oldlace: '#fdf5e6',
|
|
319
|
+
olivedrab: '#6b8e23',
|
|
320
|
+
orangered: '#ff4500',
|
|
321
|
+
orchid: '#da70d6',
|
|
322
|
+
palegoldenrod: '#eee8aa',
|
|
323
|
+
palegreen: '#98fb98',
|
|
324
|
+
paleturquoise: '#afeeee',
|
|
325
|
+
palevioletred: '#db7093',
|
|
326
|
+
papayawhip: '#ffefd5',
|
|
327
|
+
peachpuff: '#ffdab9',
|
|
328
|
+
peru: '#cd853f',
|
|
329
|
+
pink: '#ffc0cb',
|
|
330
|
+
plum: '#dda0dd',
|
|
331
|
+
powderblue: '#b0e0e6',
|
|
332
|
+
rosybrown: '#bc8f8f',
|
|
333
|
+
royalblue: '#4169e1',
|
|
334
|
+
saddlebrown: '#8b4513',
|
|
335
|
+
salmon: '#fa8072',
|
|
336
|
+
sandybrown: '#f4a460',
|
|
337
|
+
seagreen: '#2e8b57',
|
|
338
|
+
seashell: '#fff5ee',
|
|
339
|
+
sienna: '#a0522d',
|
|
340
|
+
skyblue: '#87ceeb',
|
|
341
|
+
slateblue: '#6a5acd',
|
|
342
|
+
slategray: '#708090',
|
|
343
|
+
slategrey: '#708090',
|
|
344
|
+
snow: '#fffafa',
|
|
345
|
+
springgreen: '#00ff7f',
|
|
346
|
+
steelblue: '#4682b4',
|
|
347
|
+
tan: '#d2b48c',
|
|
348
|
+
thistle: '#d8bfd8',
|
|
349
|
+
tomato: '#ff6347',
|
|
350
|
+
transparent: '#00000000', // Special case
|
|
351
|
+
turquoise: '#40e0d0',
|
|
352
|
+
violet: '#ee82ee',
|
|
353
|
+
wheat: '#f5deb3',
|
|
354
|
+
whitesmoke: '#f5f5f5',
|
|
355
|
+
yellowgreen: '#9acd32',
|
|
356
|
+
rebeccapurple: '#663399'
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
if (colors[name]) {
|
|
360
|
+
return parseHex(colors[name]);
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
@@ -6,12 +6,16 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as csstree from 'css-tree';
|
|
7
7
|
import { DOMTree, DOMNodeInfo } from './domTree';
|
|
8
8
|
import { parse, HTMLElement as ParsedHTMLElement } from 'node-html-parser';
|
|
9
|
+
import { Color } from 'vscode-languageserver/node';
|
|
10
|
+
import { parseColor } from './colorService';
|
|
11
|
+
import { calculateSpecificity, compareSpecificity } from './specificity';
|
|
9
12
|
|
|
10
13
|
export interface CssVariable {
|
|
11
14
|
name: string;
|
|
12
15
|
value: string;
|
|
13
16
|
uri: string;
|
|
14
|
-
range: Range;
|
|
17
|
+
range: Range; // Range of the entire declaration (e.g., "--foo: red")
|
|
18
|
+
valueRange?: Range; // Range of just the value part (e.g., "red")
|
|
15
19
|
selector: string; // CSS selector where this variable is defined (e.g., ":root", "div", ".class")
|
|
16
20
|
important: boolean; // Whether this definition uses !important
|
|
17
21
|
sourcePosition: number; // Character position in file (for source order)
|
|
@@ -210,11 +214,29 @@ export class CssVariableManager {
|
|
|
210
214
|
const startPos = document.positionAt(offset + node.loc.start.offset);
|
|
211
215
|
const endPos = document.positionAt(offset + node.loc.end.offset);
|
|
212
216
|
|
|
217
|
+
// Capture valueRange from node.value location
|
|
218
|
+
let valueRange: Range | undefined;
|
|
219
|
+
if (node.value && node.value.loc) {
|
|
220
|
+
// Get the raw text from the value node
|
|
221
|
+
const valueStartOffset = offset + node.value.loc.start.offset;
|
|
222
|
+
const valueEndOffset = offset + node.value.loc.end.offset;
|
|
223
|
+
const rawValueText = text.substring(valueStartOffset, valueEndOffset);
|
|
224
|
+
|
|
225
|
+
// Trim leading/trailing whitespace to get the actual value position
|
|
226
|
+
const leadingWhitespace = rawValueText.length - rawValueText.trimStart().length;
|
|
227
|
+
const trailingWhitespace = rawValueText.length - rawValueText.trimEnd().length;
|
|
228
|
+
|
|
229
|
+
const valueStartPos = document.positionAt(valueStartOffset + leadingWhitespace);
|
|
230
|
+
const valueEndPos = document.positionAt(valueEndOffset - trailingWhitespace);
|
|
231
|
+
valueRange = Range.create(valueStartPos, valueEndPos);
|
|
232
|
+
}
|
|
233
|
+
|
|
213
234
|
const variable: CssVariable = {
|
|
214
235
|
name,
|
|
215
236
|
value,
|
|
216
237
|
uri,
|
|
217
238
|
range: Range.create(startPos, endPos),
|
|
239
|
+
valueRange,
|
|
218
240
|
selector,
|
|
219
241
|
important,
|
|
220
242
|
sourcePosition: offset + node.loc.start.offset
|
|
@@ -350,7 +372,6 @@ export class CssVariableManager {
|
|
|
350
372
|
return;
|
|
351
373
|
}
|
|
352
374
|
|
|
353
|
-
this.parseContent(content, uri, languageId);
|
|
354
375
|
this.parseContent(content, uri, languageId);
|
|
355
376
|
this.logger.log(`[css-lsp] Updated file ${uri} from disk.`);
|
|
356
377
|
} catch (error) {
|
|
@@ -437,4 +458,53 @@ export class CssVariableManager {
|
|
|
437
458
|
return this.domTrees.get(uri);
|
|
438
459
|
}
|
|
439
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Resolve a variable name to a Color if possible.
|
|
463
|
+
* Handles recursive variable references: var(--a) -> var(--b) -> #fff
|
|
464
|
+
* Uses CSS cascade rules: !important > specificity > source order
|
|
465
|
+
*/
|
|
466
|
+
public resolveVariableColor(name: string, context?: string, seen = new Set<string>()): Color | null {
|
|
467
|
+
if (seen.has(name)) {
|
|
468
|
+
return null; // Cycle detected
|
|
469
|
+
}
|
|
470
|
+
seen.add(name);
|
|
471
|
+
|
|
472
|
+
const variables = this.getVariables(name);
|
|
473
|
+
if (variables.length === 0) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Apply CSS cascade rules to find the winning definition
|
|
478
|
+
// Sort by cascade rules: !important > specificity > source order
|
|
479
|
+
const sortedVars = [...variables].sort((a, b) => {
|
|
480
|
+
// !important always wins (unless both are !important)
|
|
481
|
+
if (a.important !== b.important) {
|
|
482
|
+
return a.important ? -1 : 1;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// After !important, check specificity
|
|
486
|
+
const specA = calculateSpecificity(a.selector);
|
|
487
|
+
const specB = calculateSpecificity(b.selector);
|
|
488
|
+
const specCompare = compareSpecificity(specA, specB);
|
|
489
|
+
|
|
490
|
+
if (specCompare !== 0) {
|
|
491
|
+
return -specCompare; // Negative for descending order
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Equal specificity - later in source wins
|
|
495
|
+
return b.sourcePosition - a.sourcePosition;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Use the winning definition (first after sort)
|
|
499
|
+
const variable = sortedVars[0];
|
|
500
|
+
let value = variable.value;
|
|
501
|
+
|
|
502
|
+
// Check if it's a reference to another variable
|
|
503
|
+
const match = value.match(/^var\((--[\w-]+)\)$/);
|
|
504
|
+
if (match) {
|
|
505
|
+
return this.resolveVariableColor(match[1], context, seen);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return parseColor(value);
|
|
509
|
+
}
|
|
440
510
|
}
|
package/server/src/server.ts
CHANGED
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
WorkspaceSymbol,
|
|
20
20
|
WorkspaceEdit,
|
|
21
21
|
TextEdit,
|
|
22
|
-
FileChangeType
|
|
22
|
+
FileChangeType,
|
|
23
|
+
ColorInformation,
|
|
24
|
+
ColorPresentation
|
|
23
25
|
} from 'vscode-languageserver/node';
|
|
24
26
|
import * as fs from 'fs'
|
|
25
27
|
import {
|
|
@@ -29,6 +31,7 @@ import { CssVariable } from './cssVariableManager';
|
|
|
29
31
|
|
|
30
32
|
import { CssVariableManager } from './cssVariableManager';
|
|
31
33
|
import { calculateSpecificity, compareSpecificity, formatSpecificity, matchesContext } from './specificity';
|
|
34
|
+
import { parseColor, formatColor, formatColorAsHex, formatColorAsRgb, formatColorAsHsl } from './colorService';
|
|
32
35
|
|
|
33
36
|
// Write startup log immediately
|
|
34
37
|
try {
|
|
@@ -99,7 +102,8 @@ connection.onInitialize((params: InitializeParams) => {
|
|
|
99
102
|
referencesProvider: true,
|
|
100
103
|
renameProvider: true,
|
|
101
104
|
documentSymbolProvider: true,
|
|
102
|
-
workspaceSymbolProvider: true
|
|
105
|
+
workspaceSymbolProvider: true,
|
|
106
|
+
colorProvider: true
|
|
103
107
|
}
|
|
104
108
|
};
|
|
105
109
|
if (hasWorkspaceFolderCapability) {
|
|
@@ -574,6 +578,99 @@ connection.onWorkspaceSymbol((params) => {
|
|
|
574
578
|
));
|
|
575
579
|
});
|
|
576
580
|
|
|
581
|
+
// Color Provider: Document Colors
|
|
582
|
+
connection.onDocumentColor((params) => {
|
|
583
|
+
const document = documents.get(params.textDocument.uri);
|
|
584
|
+
if (!document) {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const colors: ColorInformation[] = [];
|
|
589
|
+
const text = document.getText();
|
|
590
|
+
|
|
591
|
+
// 1. Check variable definitions: --my-color: #f00;
|
|
592
|
+
const definitions = cssVariableManager.getDocumentDefinitions(document.uri);
|
|
593
|
+
for (const def of definitions) {
|
|
594
|
+
const color = parseColor(def.value);
|
|
595
|
+
if (color) {
|
|
596
|
+
// Use the stored valueRange if available (accurate from csstree parsing)
|
|
597
|
+
if (def.valueRange) {
|
|
598
|
+
colors.push({
|
|
599
|
+
range: def.valueRange,
|
|
600
|
+
color: color
|
|
601
|
+
});
|
|
602
|
+
} else {
|
|
603
|
+
// Fallback: find the value within the declaration text
|
|
604
|
+
// This handles cases where valueRange wasn't captured (shouldn't happen normally)
|
|
605
|
+
const defText = text.substring(document.offsetAt(def.range.start), document.offsetAt(def.range.end));
|
|
606
|
+
const colonIndex = defText.indexOf(':');
|
|
607
|
+
if (colonIndex !== -1) {
|
|
608
|
+
const afterColon = defText.substring(colonIndex + 1);
|
|
609
|
+
const valueIndex = afterColon.indexOf(def.value.trim());
|
|
610
|
+
|
|
611
|
+
if (valueIndex !== -1) {
|
|
612
|
+
const absoluteValueStart = document.offsetAt(def.range.start) + colonIndex + 1 + valueIndex;
|
|
613
|
+
const start = document.positionAt(absoluteValueStart);
|
|
614
|
+
const end = document.positionAt(absoluteValueStart + def.value.trim().length);
|
|
615
|
+
colors.push({
|
|
616
|
+
range: { start, end },
|
|
617
|
+
color: color
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 2. Check variable usages: var(--my-color)
|
|
626
|
+
// We want to show the color of the variable being used.
|
|
627
|
+
// But we can't easily edit it (color picker on usage).
|
|
628
|
+
// VS Code allows read-only color information.
|
|
629
|
+
const regex = /var\((--[\w-]+)\)/g;
|
|
630
|
+
let match;
|
|
631
|
+
while ((match = regex.exec(text)) !== null) {
|
|
632
|
+
const varName = match[1];
|
|
633
|
+
const color = cssVariableManager.resolveVariableColor(varName);
|
|
634
|
+
if (color) {
|
|
635
|
+
const start = document.positionAt(match.index);
|
|
636
|
+
const end = document.positionAt(match.index + match[0].length);
|
|
637
|
+
colors.push({
|
|
638
|
+
range: { start, end },
|
|
639
|
+
color: color
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return colors;
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Color Provider: Color Presentation
|
|
648
|
+
connection.onColorPresentation((params) => {
|
|
649
|
+
const color = params.color;
|
|
650
|
+
const range = params.range;
|
|
651
|
+
const document = documents.get(params.textDocument.uri);
|
|
652
|
+
if (!document) {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Offer multiple format options for the color picker
|
|
657
|
+
const presentations: ColorPresentation[] = [];
|
|
658
|
+
|
|
659
|
+
// 1. Hex format (most common)
|
|
660
|
+
const hexStr = formatColorAsHex(color);
|
|
661
|
+
presentations.push(ColorPresentation.create(hexStr, TextEdit.replace(range, hexStr)));
|
|
662
|
+
|
|
663
|
+
// 2. RGB format
|
|
664
|
+
const rgbStr = formatColorAsRgb(color);
|
|
665
|
+
presentations.push(ColorPresentation.create(rgbStr, TextEdit.replace(range, rgbStr)));
|
|
666
|
+
|
|
667
|
+
// 3. HSL format
|
|
668
|
+
const hslStr = formatColorAsHsl(color);
|
|
669
|
+
presentations.push(ColorPresentation.create(hslStr, TextEdit.replace(range, hslStr)));
|
|
670
|
+
|
|
671
|
+
return presentations;
|
|
672
|
+
});
|
|
673
|
+
|
|
577
674
|
// Make the text document manager listen on the connection
|
|
578
675
|
// for open, change and close text document events
|
|
579
676
|
documents.listen(connection);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/cascadeandinline.test.ts","./src/cssvariablemanager.ts","./src/domtree.test.ts","./src/domtree.ts","./src/filelifecycle.test.ts","./src/filetypesandupdates.test.ts","./src/htmlcomments.test.ts","./src/server.ts","./src/specificity.test.ts","./src/specificity.ts","./src/test.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/cascadeandinline.test.ts","./src/colorprovider.test.ts","./src/colorservice.ts","./src/cssvariablemanager.ts","./src/domtree.test.ts","./src/domtree.ts","./src/filelifecycle.test.ts","./src/filetypesandupdates.test.ts","./src/htmlcomments.test.ts","./src/server.ts","./src/specificity.test.ts","./src/specificity.ts","./src/test.ts"],"version":"5.9.3"}
|