tailwind-typescript-plugin 1.4.1-beta.1 → 1.4.1-beta.11

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +206 -34
  3. package/lib/core/interfaces.d.ts +2 -2
  4. package/lib/core/interfaces.d.ts.map +1 -1
  5. package/lib/core/types.d.ts +19 -1
  6. package/lib/core/types.d.ts.map +1 -1
  7. package/lib/extractors/BaseExtractor.d.ts +49 -2
  8. package/lib/extractors/BaseExtractor.d.ts.map +1 -1
  9. package/lib/extractors/BaseExtractor.js +184 -5
  10. package/lib/extractors/BaseExtractor.js.map +1 -1
  11. package/lib/extractors/CvaExtractor.js +1 -1
  12. package/lib/extractors/CvaExtractor.js.map +1 -1
  13. package/lib/extractors/ExpressionExtractor.d.ts.map +1 -1
  14. package/lib/extractors/ExpressionExtractor.js +35 -5
  15. package/lib/extractors/ExpressionExtractor.js.map +1 -1
  16. package/lib/extractors/JsxAttributeExtractor.js +1 -1
  17. package/lib/extractors/JsxAttributeExtractor.js.map +1 -1
  18. package/lib/extractors/TailwindVariantsExtractor.d.ts.map +1 -1
  19. package/lib/extractors/TailwindVariantsExtractor.js +1 -5
  20. package/lib/extractors/TailwindVariantsExtractor.js.map +1 -1
  21. package/lib/extractors/VariableReferenceExtractor.d.ts.map +1 -1
  22. package/lib/extractors/VariableReferenceExtractor.js +21 -0
  23. package/lib/extractors/VariableReferenceExtractor.js.map +1 -1
  24. package/lib/infrastructure/TailwindConflictDetector.d.ts +28 -18
  25. package/lib/infrastructure/TailwindConflictDetector.d.ts.map +1 -1
  26. package/lib/infrastructure/TailwindConflictDetector.js +216 -486
  27. package/lib/infrastructure/TailwindConflictDetector.js.map +1 -1
  28. package/lib/infrastructure/TailwindValidator.d.ts +15 -0
  29. package/lib/infrastructure/TailwindValidator.d.ts.map +1 -1
  30. package/lib/infrastructure/TailwindValidator.js +84 -5
  31. package/lib/infrastructure/TailwindValidator.js.map +1 -1
  32. package/lib/infrastructure/TailwindValidator.spec.js +194 -0
  33. package/lib/infrastructure/TailwindValidator.spec.js.map +1 -1
  34. package/lib/plugin/TailwindTypescriptPlugin.d.ts +26 -0
  35. package/lib/plugin/TailwindTypescriptPlugin.d.ts.map +1 -1
  36. package/lib/plugin/TailwindTypescriptPlugin.js +185 -1
  37. package/lib/plugin/TailwindTypescriptPlugin.js.map +1 -1
  38. package/lib/services/ClassNameExtractionService.d.ts +2 -2
  39. package/lib/services/ClassNameExtractionService.d.ts.map +1 -1
  40. package/lib/services/ClassNameExtractionService.js.map +1 -1
  41. package/lib/services/CompletionService.d.ts +113 -0
  42. package/lib/services/CompletionService.d.ts.map +1 -0
  43. package/lib/services/CompletionService.js +488 -0
  44. package/lib/services/CompletionService.js.map +1 -0
  45. package/lib/services/CompletionService.spec.d.ts +2 -0
  46. package/lib/services/CompletionService.spec.d.ts.map +1 -0
  47. package/lib/services/CompletionService.spec.js +1039 -0
  48. package/lib/services/CompletionService.spec.js.map +1 -0
  49. package/lib/services/ConflictClassDetection.spec.js +24 -19
  50. package/lib/services/ConflictClassDetection.spec.js.map +1 -1
  51. package/lib/services/DuplicateClassDetection.spec.js +41 -60
  52. package/lib/services/DuplicateClassDetection.spec.js.map +1 -1
  53. package/lib/services/PluginConfigService.d.ts +2 -1
  54. package/lib/services/PluginConfigService.d.ts.map +1 -1
  55. package/lib/services/PluginConfigService.js +19 -12
  56. package/lib/services/PluginConfigService.js.map +1 -1
  57. package/lib/services/ValidationService.d.ts +4 -2
  58. package/lib/services/ValidationService.d.ts.map +1 -1
  59. package/lib/services/ValidationService.js +10 -7
  60. package/lib/services/ValidationService.js.map +1 -1
  61. package/package.json +4 -2
  62. package/lib/extractors/StringLiteralExtractor.d.ts +0 -12
  63. package/lib/extractors/StringLiteralExtractor.d.ts.map +0 -1
  64. package/lib/extractors/StringLiteralExtractor.js +0 -21
  65. package/lib/extractors/StringLiteralExtractor.js.map +0 -1
  66. package/lib/services/ClassNameExtractionService.original.d.ts +0 -20
  67. package/lib/services/ClassNameExtractionService.original.d.ts.map +0 -1
  68. package/lib/services/ClassNameExtractionService.original.js +0 -48
  69. package/lib/services/ClassNameExtractionService.original.js.map +0 -1
@@ -0,0 +1,1039 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const ts = __importStar(require("typescript/lib/tsserverlibrary"));
37
+ const TailwindValidator_1 = require("../infrastructure/TailwindValidator");
38
+ const Logger_1 = require("../utils/Logger");
39
+ const CompletionService_1 = require("./CompletionService");
40
+ const defaultConfig = {
41
+ utilityFunctions: ['clsx', 'cn', 'classnames', 'classNames', 'cx', 'twMerge'],
42
+ tailwindVariantsEnabled: true,
43
+ classVarianceAuthorityEnabled: true
44
+ };
45
+ describe('CompletionService', () => {
46
+ let validator;
47
+ let completionService;
48
+ beforeEach(() => {
49
+ validator = new TailwindValidator_1.TailwindValidator('/fake/path.css', new Logger_1.NoOpLogger());
50
+ // Mock the validator methods
51
+ jest
52
+ .spyOn(validator, 'getAllClasses')
53
+ .mockReturnValue([
54
+ 'flex',
55
+ 'flex-row',
56
+ 'flex-col',
57
+ 'items-center',
58
+ 'items-start',
59
+ 'items-end',
60
+ 'justify-center',
61
+ 'justify-between',
62
+ 'p-4',
63
+ 'px-4',
64
+ 'py-4',
65
+ 'bg-red-500',
66
+ 'bg-blue-500',
67
+ 'text-white',
68
+ 'text-black'
69
+ ]);
70
+ jest.spyOn(validator, 'isValidClass').mockImplementation((className) => {
71
+ return validator.getAllClasses().includes(className);
72
+ });
73
+ jest.spyOn(validator, 'getCssForClasses').mockImplementation((classNames) => {
74
+ return classNames.map(name => {
75
+ if (name === 'flex')
76
+ return '.flex { display: flex; }';
77
+ if (name === 'p-4')
78
+ return '.p-4 { padding: 1rem; }';
79
+ return null;
80
+ });
81
+ });
82
+ completionService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), defaultConfig);
83
+ });
84
+ describe('getCompletionsAtPosition', () => {
85
+ it('should return original completions when not in className context', () => {
86
+ const sourceCode = 'const x = 1;';
87
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
88
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 5, // Position in "const"
89
+ undefined);
90
+ expect(result).toBeUndefined();
91
+ });
92
+ it('should provide completions inside className attribute', () => {
93
+ const sourceCode = '<div className="">Hello</div>';
94
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
95
+ // Position inside the empty className string (after the opening quote)
96
+ const position = 16;
97
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
98
+ expect(result).toBeDefined();
99
+ expect(result.entries.length).toBeGreaterThan(0);
100
+ // Should include flex classes
101
+ const flexEntry = result.entries.find(e => e.name === 'flex');
102
+ expect(flexEntry).toBeDefined();
103
+ });
104
+ it('should filter completions based on prefix', () => {
105
+ const sourceCode = '<div className="fl">Hello</div>';
106
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
107
+ // Position after "fl"
108
+ const position = 18;
109
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
110
+ expect(result).toBeDefined();
111
+ // Should only include classes starting with "fl"
112
+ const allStartWithFl = result.entries.every(e => e.name.toLowerCase().startsWith('fl'));
113
+ expect(allStartWithFl).toBe(true);
114
+ // Should include flex, flex-row, flex-col
115
+ const names = result.entries.map(e => e.name);
116
+ expect(names).toContain('flex');
117
+ expect(names).toContain('flex-row');
118
+ expect(names).toContain('flex-col');
119
+ });
120
+ it('should provide completions after space in className', () => {
121
+ const sourceCode = '<div className="flex ">Hello</div>';
122
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
123
+ // Position after "flex " (after the space)
124
+ const position = 21;
125
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
126
+ expect(result).toBeDefined();
127
+ // Should provide all classes except "flex" which is already present
128
+ const names = result.entries.map(e => e.name);
129
+ expect(names).not.toContain('flex');
130
+ expect(names).toContain('items-center');
131
+ });
132
+ it('should exclude already existing classes from completions', () => {
133
+ // className="flex items-center p"
134
+ // Position: 16 21 35
135
+ const sourceCode = '<div className="flex items-center p">Hello</div>';
136
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
137
+ // Position after "p" (typing a new class) - position 35 is after the "p"
138
+ const position = 35;
139
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
140
+ expect(result).toBeDefined();
141
+ const names = result.entries.map(e => e.name);
142
+ // Should not include already present classes
143
+ expect(names).not.toContain('flex');
144
+ expect(names).not.toContain('items-center');
145
+ // Should include p-4, px-4, py-4 as they match the "p" prefix
146
+ expect(names).toContain('p-4');
147
+ expect(names).toContain('px-4');
148
+ expect(names).toContain('py-4');
149
+ });
150
+ it('should provide completions in utility function arguments', () => {
151
+ const sourceCode = 'const classes = cn("flex ", isActive && "");';
152
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
153
+ // Position inside the second empty string in cn()
154
+ const position = 41;
155
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
156
+ expect(result).toBeDefined();
157
+ expect(result.entries.length).toBeGreaterThan(0);
158
+ });
159
+ it('should provide completions in clsx function', () => {
160
+ const sourceCode = 'const classes = clsx("f");';
161
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
162
+ // Position after "f"
163
+ const position = 23;
164
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
165
+ expect(result).toBeDefined();
166
+ // Should include classes starting with "f"
167
+ const names = result.entries.map(e => e.name);
168
+ expect(names).toContain('flex');
169
+ });
170
+ it('should provide completions in object property className', () => {
171
+ const sourceCode = 'const obj = { className: "fl" };';
172
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
173
+ // Position after "fl"
174
+ const position = 28;
175
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
176
+ expect(result).toBeDefined();
177
+ const names = result.entries.map(e => e.name);
178
+ expect(names).toContain('flex');
179
+ });
180
+ it('should set correct replacement span for partial class names', () => {
181
+ const sourceCode = '<div className="flex ite">Hello</div>';
182
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
183
+ // Position after "ite"
184
+ const position = 24;
185
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
186
+ expect(result).toBeDefined();
187
+ // Each entry should have a replacement span
188
+ const itemsCenter = result.entries.find(e => e.name === 'items-center');
189
+ expect(itemsCenter).toBeDefined();
190
+ expect(itemsCenter.replacementSpan).toBeDefined();
191
+ expect(itemsCenter.replacementSpan.length).toBe(3); // "ite" length
192
+ });
193
+ });
194
+ describe('getCompletionEntryDetails', () => {
195
+ it('should return CSS documentation for valid Tailwind class', () => {
196
+ const sourceCode = '<div className="flex">Hello</div>';
197
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
198
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'flex');
199
+ expect(result).toBeDefined();
200
+ expect(result.name).toBe('flex');
201
+ expect(result.documentation).toBeDefined();
202
+ expect(result.documentation.length).toBeGreaterThan(0);
203
+ expect(result.documentation[0].text).toContain('display: flex');
204
+ });
205
+ it('should format documentation with markdown CSS code block', () => {
206
+ const sourceCode = '<div className="flex">Hello</div>';
207
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
208
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'flex');
209
+ expect(result).toBeDefined();
210
+ expect(result.documentation).toBeDefined();
211
+ // Documentation should be wrapped in markdown CSS code block for syntax highlighting
212
+ const docText = result.documentation[0].text;
213
+ expect(docText).toContain('```css');
214
+ expect(docText).toContain('```');
215
+ });
216
+ it('should provide short CSS summary in displayParts', () => {
217
+ const sourceCode = '<div className="flex">Hello</div>';
218
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
219
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'flex');
220
+ expect(result).toBeDefined();
221
+ expect(result.displayParts).toBeDefined();
222
+ expect(result.displayParts.length).toBeGreaterThan(0);
223
+ // displayParts should contain just the declaration (e.g., "display: flex;")
224
+ const displayText = result.displayParts[0].text;
225
+ expect(displayText).toContain('display: flex');
226
+ expect(displayText).not.toContain('.flex');
227
+ expect(displayText).not.toContain('{');
228
+ });
229
+ it('should return undefined for non-Tailwind class', () => {
230
+ const sourceCode = '<div className="my-custom-class">Hello</div>';
231
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
232
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'my-custom-class');
233
+ expect(result).toBeUndefined();
234
+ });
235
+ it('should format CSS with proper indentation', () => {
236
+ // Mock a class with multiple declarations
237
+ jest.spyOn(validator, 'getCssForClasses').mockImplementation((classNames) => {
238
+ return classNames.map(name => {
239
+ if (name === 'p-4')
240
+ return '.p-4 { padding-top: 1rem; padding-right: 1rem; padding-bottom: 1rem; padding-left: 1rem; }';
241
+ return null;
242
+ });
243
+ });
244
+ const sourceCode = '<div className="p-4">Hello</div>';
245
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
246
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'p-4');
247
+ expect(result).toBeDefined();
248
+ const docText = result.documentation[0].text;
249
+ // Should be formatted with newlines for multiple declarations
250
+ expect(docText).toContain('\n');
251
+ expect(docText).toContain('padding-top: 1rem;');
252
+ expect(docText).toContain('padding-right: 1rem;');
253
+ });
254
+ });
255
+ describe('color class detection', () => {
256
+ it('should mark color classes with "color" kindModifier', () => {
257
+ // Add color classes to the mock
258
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue([
259
+ 'flex',
260
+ 'bg-red-500',
261
+ 'bg-blue-200',
262
+ 'text-white',
263
+ 'text-black',
264
+ 'border-gray-300',
265
+ 'ring-indigo-500',
266
+ 'text-sm', // not a color
267
+ 'border-2' // not a color
268
+ ]);
269
+ const sourceCode = '<div className="">Hello</div>';
270
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
271
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
272
+ expect(result).toBeDefined();
273
+ // Color classes should have 'color' kindModifier
274
+ const bgRed = result.entries.find(e => e.name === 'bg-red-500');
275
+ expect(bgRed?.kindModifiers).toBe('color');
276
+ const textWhite = result.entries.find(e => e.name === 'text-white');
277
+ expect(textWhite?.kindModifiers).toBe('color');
278
+ const borderGray = result.entries.find(e => e.name === 'border-gray-300');
279
+ expect(borderGray?.kindModifiers).toBe('color');
280
+ // Non-color classes should have empty kindModifier
281
+ const flexEntry = result.entries.find(e => e.name === 'flex');
282
+ expect(flexEntry?.kindModifiers).toBe('');
283
+ const textSm = result.entries.find(e => e.name === 'text-sm');
284
+ expect(textSm?.kindModifiers).toBe('');
285
+ const border2 = result.entries.find(e => e.name === 'border-2');
286
+ expect(border2?.kindModifiers).toBe('');
287
+ });
288
+ it('should detect various color class patterns', () => {
289
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue([
290
+ 'from-purple-400', // gradient from
291
+ 'via-pink-500', // gradient via
292
+ 'to-red-500', // gradient to
293
+ 'fill-current', // SVG fill
294
+ 'stroke-blue-600', // SVG stroke
295
+ 'accent-violet-500', // accent color
296
+ 'shadow-sm', // not a color (shadow size)
297
+ 'outline-none', // not a color (outline style)
298
+ 'divide-y', // not a color (divide direction)
299
+ 'text-center' // not a color (text alignment)
300
+ ]);
301
+ const sourceCode = '<div className="">Hello</div>';
302
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
303
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
304
+ expect(result).toBeDefined();
305
+ // Color classes
306
+ expect(result.entries.find(e => e.name === 'from-purple-400')?.kindModifiers).toBe('color');
307
+ expect(result.entries.find(e => e.name === 'via-pink-500')?.kindModifiers).toBe('color');
308
+ expect(result.entries.find(e => e.name === 'to-red-500')?.kindModifiers).toBe('color');
309
+ expect(result.entries.find(e => e.name === 'fill-current')?.kindModifiers).toBe('color');
310
+ expect(result.entries.find(e => e.name === 'stroke-blue-600')?.kindModifiers).toBe('color');
311
+ expect(result.entries.find(e => e.name === 'accent-violet-500')?.kindModifiers).toBe('color');
312
+ // Non-color classes
313
+ expect(result.entries.find(e => e.name === 'shadow-sm')?.kindModifiers).toBe('');
314
+ expect(result.entries.find(e => e.name === 'outline-none')?.kindModifiers).toBe('');
315
+ expect(result.entries.find(e => e.name === 'divide-y')?.kindModifiers).toBe('');
316
+ expect(result.entries.find(e => e.name === 'text-center')?.kindModifiers).toBe('');
317
+ });
318
+ });
319
+ describe('clearCache', () => {
320
+ it('should clear the cached class list', () => {
321
+ const sourceCode = '<div className="">Hello</div>';
322
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
323
+ // First call to populate cache
324
+ completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
325
+ // Clear cache
326
+ completionService.clearCache();
327
+ // Mock different classes
328
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue(['new-class', 'another-class']);
329
+ // Get completions again
330
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
331
+ expect(result).toBeDefined();
332
+ const names = result.entries.map(e => e.name);
333
+ expect(names).toContain('new-class');
334
+ expect(names).toContain('another-class');
335
+ expect(names).not.toContain('flex');
336
+ });
337
+ });
338
+ describe('custom utility functions', () => {
339
+ it('should provide completions in custom utility function', () => {
340
+ const customConfig = {
341
+ utilityFunctions: ['myCustomClass', { name: 'styles', from: '@/utils' }],
342
+ tailwindVariantsEnabled: false,
343
+ classVarianceAuthorityEnabled: false
344
+ };
345
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
346
+ const sourceCode = 'const x = myCustomClass("fl");';
347
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
348
+ // Position after "fl"
349
+ const position = 27;
350
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
351
+ expect(result).toBeDefined();
352
+ const names = result.entries.map(e => e.name);
353
+ expect(names).toContain('flex');
354
+ });
355
+ it('should provide completions in tv() when tailwindVariants is enabled', () => {
356
+ const sourceCode = 'const button = tv({ base: "fl" });';
357
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
358
+ // Position at "l" inside "fl" string (position 28)
359
+ // const button = tv({ base: "fl" });
360
+ // ^^ position 27-28
361
+ const position = 28;
362
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
363
+ expect(result).toBeDefined();
364
+ const names = result.entries.map(e => e.name);
365
+ expect(names).toContain('flex');
366
+ });
367
+ it('should NOT provide completions in tv() when tailwindVariants is disabled', () => {
368
+ const customConfig = {
369
+ utilityFunctions: ['clsx'],
370
+ tailwindVariantsEnabled: false,
371
+ classVarianceAuthorityEnabled: false
372
+ };
373
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
374
+ const sourceCode = 'const button = tv({ base: "fl" });';
375
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
376
+ // Position after "fl" inside the string
377
+ const position = 29;
378
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
379
+ // Should not provide Tailwind completions since tv() is not recognized
380
+ expect(result).toBeUndefined();
381
+ });
382
+ it('should provide completions in cva() when classVarianceAuthority is enabled', () => {
383
+ const sourceCode = 'const button = cva("fl");';
384
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
385
+ // Position after "fl"
386
+ const position = 22;
387
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
388
+ expect(result).toBeDefined();
389
+ const names = result.entries.map(e => e.name);
390
+ expect(names).toContain('flex');
391
+ });
392
+ it('should NOT provide completions in cva() when classVarianceAuthority is disabled', () => {
393
+ const customConfig = {
394
+ utilityFunctions: ['clsx'],
395
+ tailwindVariantsEnabled: false,
396
+ classVarianceAuthorityEnabled: false
397
+ };
398
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
399
+ const sourceCode = 'const button = cva("fl");';
400
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
401
+ // Position after "fl"
402
+ const position = 22;
403
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
404
+ // Should not provide Tailwind completions since cva() is not recognized
405
+ expect(result).toBeUndefined();
406
+ });
407
+ it('should provide completions with UtilityFunctionConfig objects', () => {
408
+ const customConfig = {
409
+ utilityFunctions: [{ name: 'customStyles', from: '@/lib/styles' }],
410
+ tailwindVariantsEnabled: false,
411
+ classVarianceAuthorityEnabled: false
412
+ };
413
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
414
+ const sourceCode = 'const x = customStyles("fl");';
415
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
416
+ const position = 26;
417
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
418
+ expect(result).toBeDefined();
419
+ const names = result.entries.map(e => e.name);
420
+ expect(names).toContain('flex');
421
+ });
422
+ it('should provide completions with mixed string and object utility functions', () => {
423
+ const customConfig = {
424
+ utilityFunctions: ['simpleUtil', { name: 'configuredUtil', from: '@/utils' }],
425
+ tailwindVariantsEnabled: false,
426
+ classVarianceAuthorityEnabled: false
427
+ };
428
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
429
+ // Test simple string utility
430
+ const sourceCode1 = 'const x = simpleUtil("fl");';
431
+ const sourceFile1 = ts.createSourceFile('test.tsx', sourceCode1, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
432
+ const result1 = customService.getCompletionsAtPosition(ts, sourceFile1, 24, undefined);
433
+ expect(result1).toBeDefined();
434
+ expect(result1.entries.map(e => e.name)).toContain('flex');
435
+ // Test configured utility
436
+ const sourceCode2 = 'const x = configuredUtil("fl");';
437
+ const sourceFile2 = ts.createSourceFile('test.tsx', sourceCode2, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
438
+ const result2 = customService.getCompletionsAtPosition(ts, sourceFile2, 28, undefined);
439
+ expect(result2).toBeDefined();
440
+ expect(result2.entries.map(e => e.name)).toContain('flex');
441
+ });
442
+ it('should provide completions in tv() variants property', () => {
443
+ const sourceCode = `const button = tv({
444
+ base: "flex",
445
+ variants: {
446
+ color: {
447
+ primary: "bg-blue-500 te",
448
+ secondary: "bg-gray-500"
449
+ }
450
+ }
451
+ });`;
452
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
453
+ // Find position after "te" in primary variant
454
+ const position = sourceCode.indexOf('te",') + 2;
455
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
456
+ expect(result).toBeDefined();
457
+ const names = result.entries.map(e => e.name);
458
+ expect(names).toContain('text-white');
459
+ expect(names).toContain('text-black');
460
+ });
461
+ it('should provide completions in cva() variants property', () => {
462
+ const sourceCode = `const button = cva("base-class", {
463
+ variants: {
464
+ size: {
465
+ sm: "p-",
466
+ lg: "p-4"
467
+ }
468
+ }
469
+ });`;
470
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
471
+ // Find position after "p-" in sm variant
472
+ const position = sourceCode.indexOf('p-",') + 2;
473
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
474
+ expect(result).toBeDefined();
475
+ const names = result.entries.map(e => e.name);
476
+ // Should match classes starting with "p-" from the mock (p-4, px-4, py-4)
477
+ expect(names).toContain('p-4');
478
+ });
479
+ it('should provide completions in tv() compoundVariants', () => {
480
+ const sourceCode = `const button = tv({
481
+ base: "flex",
482
+ variants: {
483
+ color: { primary: "bg-blue-500" }
484
+ },
485
+ compoundVariants: [
486
+ { color: "primary", class: "fl" }
487
+ ]
488
+ });`;
489
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
490
+ // Find position after "fl" in compoundVariants class
491
+ const position = sourceCode.indexOf('"fl"') + 3;
492
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
493
+ expect(result).toBeDefined();
494
+ const names = result.entries.map(e => e.name);
495
+ expect(names).toContain('flex');
496
+ });
497
+ it('should enable both tv and cva when both variant options are true', () => {
498
+ const customConfig = {
499
+ utilityFunctions: [],
500
+ tailwindVariantsEnabled: true,
501
+ classVarianceAuthorityEnabled: true
502
+ };
503
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
504
+ // Test tv()
505
+ const tvSource = 'const x = tv({ base: "fl" });';
506
+ const tvFile = ts.createSourceFile('test.tsx', tvSource, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
507
+ const tvResult = customService.getCompletionsAtPosition(ts, tvFile, 24, undefined);
508
+ expect(tvResult).toBeDefined();
509
+ // Test cva()
510
+ const cvaSource = 'const x = cva("fl");';
511
+ const cvaFile = ts.createSourceFile('test.tsx', cvaSource, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
512
+ const cvaResult = customService.getCompletionsAtPosition(ts, cvaFile, 17, undefined);
513
+ expect(cvaResult).toBeDefined();
514
+ });
515
+ });
516
+ describe('JSX attribute variations', () => {
517
+ it('should provide completions in "class" attribute', () => {
518
+ const sourceCode = '<div class="fl">Hello</div>';
519
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
520
+ const position = 14;
521
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
522
+ expect(result).toBeDefined();
523
+ const names = result.entries.map(e => e.name);
524
+ expect(names).toContain('flex');
525
+ });
526
+ it('should provide completions in "classList" attribute', () => {
527
+ const sourceCode = '<div classList="fl">Hello</div>';
528
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
529
+ const position = 18;
530
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
531
+ expect(result).toBeDefined();
532
+ const names = result.entries.map(e => e.name);
533
+ expect(names).toContain('flex');
534
+ });
535
+ it('should NOT provide completions in non-className attributes', () => {
536
+ const sourceCode = '<div id="fl">Hello</div>';
537
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
538
+ const position = 11;
539
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
540
+ expect(result).toBeUndefined();
541
+ });
542
+ it('should NOT provide completions in data attributes', () => {
543
+ const sourceCode = '<div data-class="fl">Hello</div>';
544
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
545
+ const position = 19;
546
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
547
+ expect(result).toBeUndefined();
548
+ });
549
+ it('should provide completions in self-closing JSX element', () => {
550
+ const sourceCode = '<input className="fl" />';
551
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
552
+ const position = 20;
553
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
554
+ expect(result).toBeDefined();
555
+ const names = result.entries.map(e => e.name);
556
+ expect(names).toContain('flex');
557
+ });
558
+ it('should provide completions in nested JSX elements', () => {
559
+ const sourceCode = '<div><span className="fl"></span></div>';
560
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
561
+ const position = 24;
562
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
563
+ expect(result).toBeDefined();
564
+ const names = result.entries.map(e => e.name);
565
+ expect(names).toContain('flex');
566
+ });
567
+ });
568
+ describe('utility function contexts', () => {
569
+ it('should provide completions in nested utility function calls', () => {
570
+ const sourceCode = 'const x = cn(clsx("fl"));';
571
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
572
+ const position = 21;
573
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
574
+ expect(result).toBeDefined();
575
+ const names = result.entries.map(e => e.name);
576
+ expect(names).toContain('flex');
577
+ });
578
+ it('should provide completions in twMerge function', () => {
579
+ const sourceCode = 'const x = twMerge("fl");';
580
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
581
+ const position = 21;
582
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
583
+ expect(result).toBeDefined();
584
+ const names = result.entries.map(e => e.name);
585
+ expect(names).toContain('flex');
586
+ });
587
+ it('should provide completions in classnames function (lowercase)', () => {
588
+ const sourceCode = 'const x = classnames("fl");';
589
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
590
+ const position = 24;
591
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
592
+ expect(result).toBeDefined();
593
+ const names = result.entries.map(e => e.name);
594
+ expect(names).toContain('flex');
595
+ });
596
+ it('should provide completions in classNames function (camelCase)', () => {
597
+ const sourceCode = 'const x = classNames("fl");';
598
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
599
+ const position = 24;
600
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
601
+ expect(result).toBeDefined();
602
+ const names = result.entries.map(e => e.name);
603
+ expect(names).toContain('flex');
604
+ });
605
+ it('should provide completions in cx function', () => {
606
+ const sourceCode = 'const x = cx("fl");';
607
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
608
+ const position = 16;
609
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
610
+ expect(result).toBeDefined();
611
+ const names = result.entries.map(e => e.name);
612
+ expect(names).toContain('flex');
613
+ });
614
+ it('should provide completions in conditional utility function argument', () => {
615
+ const sourceCode = 'const x = cn(isActive ? "fl" : "hidden");';
616
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
617
+ // Position inside the first string "fl"
618
+ const position = 27;
619
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
620
+ expect(result).toBeDefined();
621
+ const names = result.entries.map(e => e.name);
622
+ expect(names).toContain('flex');
623
+ });
624
+ it('should provide completions in array argument of utility function', () => {
625
+ const sourceCode = 'const x = cn(["fl", "items-center"]);';
626
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
627
+ // Position inside "fl"
628
+ const position = 17;
629
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
630
+ expect(result).toBeDefined();
631
+ const names = result.entries.map(e => e.name);
632
+ expect(names).toContain('flex');
633
+ });
634
+ it('should provide completions in object value of utility function', () => {
635
+ const sourceCode = 'const x = cn({ "fl": isActive });';
636
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
637
+ // Position inside "fl" key
638
+ const position = 18;
639
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
640
+ expect(result).toBeDefined();
641
+ const names = result.entries.map(e => e.name);
642
+ expect(names).toContain('flex');
643
+ });
644
+ it('should NOT provide completions in unknown function', () => {
645
+ const sourceCode = 'const x = unknownFn("fl");';
646
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
647
+ const position = 23;
648
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
649
+ expect(result).toBeUndefined();
650
+ });
651
+ it('should provide completions with property access expression (method call)', () => {
652
+ // When cn is called as a method: utils.cn("fl")
653
+ const customConfig = {
654
+ utilityFunctions: ['cn'],
655
+ tailwindVariantsEnabled: false,
656
+ classVarianceAuthorityEnabled: false
657
+ };
658
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
659
+ const sourceCode = 'const x = utils.cn("fl");';
660
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
661
+ const position = 22;
662
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
663
+ expect(result).toBeDefined();
664
+ const names = result.entries.map(e => e.name);
665
+ expect(names).toContain('flex');
666
+ });
667
+ });
668
+ describe('edge cases and position handling', () => {
669
+ it('should handle empty className attribute', () => {
670
+ const sourceCode = '<div className="">Hello</div>';
671
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
672
+ // Position inside empty string
673
+ const position = 16;
674
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
675
+ expect(result).toBeDefined();
676
+ expect(result.entries.length).toBeGreaterThan(0);
677
+ });
678
+ it('should handle position at start of string content', () => {
679
+ const sourceCode = '<div className="flex">Hello</div>';
680
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
681
+ // Position at start of string content (right after opening quote)
682
+ const position = 16;
683
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
684
+ expect(result).toBeDefined();
685
+ });
686
+ it('should handle multiple spaces between classes', () => {
687
+ const sourceCode = '<div className="flex items-center">Hello</div>';
688
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
689
+ // Position after multiple spaces
690
+ const position = 23;
691
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
692
+ expect(result).toBeDefined();
693
+ });
694
+ it('should handle trailing space in className', () => {
695
+ const sourceCode = '<div className="flex ">Hello</div>';
696
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
697
+ // Position after trailing space
698
+ const position = 21;
699
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
700
+ expect(result).toBeDefined();
701
+ // flex should be excluded as it's already in the string
702
+ const names = result.entries.map(e => e.name);
703
+ expect(names).not.toContain('flex');
704
+ });
705
+ it('should return existingCompletions when no Tailwind classes match', () => {
706
+ // Mock empty class list
707
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue([]);
708
+ const existingCompletions = {
709
+ isGlobalCompletion: false,
710
+ isMemberCompletion: false,
711
+ isNewIdentifierLocation: false,
712
+ entries: [{ name: 'existing', kind: ts.ScriptElementKind.unknown, sortText: '0' }]
713
+ };
714
+ const sourceCode = '<div className="xyz">Hello</div>';
715
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
716
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 19, existingCompletions);
717
+ expect(result).toBe(existingCompletions);
718
+ });
719
+ it('should handle cursor position before string start', () => {
720
+ const sourceCode = '<div className="flex">Hello</div>';
721
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
722
+ // Position before the opening quote
723
+ const position = 14;
724
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
725
+ // Should not provide completions when cursor is outside string
726
+ expect(result).toBeUndefined();
727
+ });
728
+ it('should handle very long class names', () => {
729
+ jest
730
+ .spyOn(validator, 'getAllClasses')
731
+ .mockReturnValue(['very-long-tailwind-class-name-that-is-quite-long', 'flex']);
732
+ const sourceCode = '<div className="very">Hello</div>';
733
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
734
+ const position = 20;
735
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
736
+ expect(result).toBeDefined();
737
+ const names = result.entries.map(e => e.name);
738
+ expect(names).toContain('very-long-tailwind-class-name-that-is-quite-long');
739
+ });
740
+ it('should handle special characters in class prefix', () => {
741
+ jest
742
+ .spyOn(validator, 'getAllClasses')
743
+ .mockReturnValue(['w-1/2', 'w-1/3', 'w-1/4', '-mt-4', '-translate-x-1/2']);
744
+ const sourceCode = '<div className="w-1/">Hello</div>';
745
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
746
+ const position = 20;
747
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
748
+ expect(result).toBeDefined();
749
+ const names = result.entries.map(e => e.name);
750
+ expect(names).toContain('w-1/2');
751
+ expect(names).toContain('w-1/3');
752
+ expect(names).toContain('w-1/4');
753
+ });
754
+ it('should handle negative value classes', () => {
755
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue(['-mt-4', '-mb-4', '-ml-4', '-mr-4']);
756
+ const sourceCode = '<div className="-m">Hello</div>';
757
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
758
+ const position = 18;
759
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
760
+ expect(result).toBeDefined();
761
+ const names = result.entries.map(e => e.name);
762
+ expect(names).toContain('-mt-4');
763
+ expect(names).toContain('-mb-4');
764
+ });
765
+ it('should handle arbitrary value classes', () => {
766
+ jest
767
+ .spyOn(validator, 'getAllClasses')
768
+ .mockReturnValue(['w-[100px]', 'h-[50vh]', 'bg-[#ff0000]']);
769
+ const sourceCode = '<div className="w-[">Hello</div>';
770
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
771
+ const position = 19;
772
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
773
+ expect(result).toBeDefined();
774
+ const names = result.entries.map(e => e.name);
775
+ expect(names).toContain('w-[100px]');
776
+ });
777
+ });
778
+ describe('sort text ordering', () => {
779
+ it('should prioritize exact matches', () => {
780
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue(['flex', 'flex-row', 'flex-col']);
781
+ const sourceCode = '<div className="flex">Hello</div>';
782
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
783
+ const position = 20;
784
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
785
+ expect(result).toBeDefined();
786
+ const flexEntry = result.entries.find(e => e.name === 'flex');
787
+ const flexRowEntry = result.entries.find(e => e.name === 'flex-row');
788
+ // Exact match should have lower sortText (higher priority)
789
+ expect(flexEntry.sortText < flexRowEntry.sortText).toBe(true);
790
+ });
791
+ it('should prioritize prefix matches over non-matches', () => {
792
+ jest
793
+ .spyOn(validator, 'getAllClasses')
794
+ .mockReturnValue(['flex', 'flex-row', 'items-center', 'justify-center']);
795
+ const sourceCode = '<div className="fl">Hello</div>';
796
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
797
+ const position = 18;
798
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
799
+ expect(result).toBeDefined();
800
+ const flexEntry = result.entries.find(e => e.name === 'flex');
801
+ // Prefix match should start with "1"
802
+ expect(flexEntry.sortText.startsWith('1')).toBe(true);
803
+ });
804
+ it('should sort alphabetically within same priority', () => {
805
+ jest.spyOn(validator, 'getAllClasses').mockReturnValue(['flex-col', 'flex-row', 'flex']);
806
+ const sourceCode = '<div className="flex-">Hello</div>';
807
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
808
+ const position = 21;
809
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
810
+ expect(result).toBeDefined();
811
+ const names = result.entries.map(e => e.name);
812
+ // Both should be prefix matches with alphabetical ordering
813
+ const colIndex = names.indexOf('flex-col');
814
+ const rowIndex = names.indexOf('flex-row');
815
+ expect(colIndex).toBeLessThan(rowIndex);
816
+ });
817
+ });
818
+ describe('color class detection - additional patterns', () => {
819
+ beforeEach(() => {
820
+ jest
821
+ .spyOn(validator, 'getAllClasses')
822
+ .mockReturnValue([
823
+ 'bg-red-500',
824
+ 'bg-red-500/50',
825
+ 'bg-[#ff0000]',
826
+ 'bg-gradient-to-r',
827
+ 'text-transparent',
828
+ 'text-current',
829
+ 'text-inherit',
830
+ 'placeholder-gray-400',
831
+ 'caret-blue-500',
832
+ 'decoration-pink-500',
833
+ 'divide-slate-200',
834
+ 'ring-offset-white',
835
+ 'shadow-black/25',
836
+ 'bg-opacity-50',
837
+ 'text-opacity-75'
838
+ ]);
839
+ });
840
+ it('should detect color classes with opacity modifiers', () => {
841
+ const sourceCode = '<div className="">Hello</div>';
842
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
843
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
844
+ expect(result).toBeDefined();
845
+ expect(result.entries.find(e => e.name === 'bg-red-500/50')?.kindModifiers).toBe('color');
846
+ expect(result.entries.find(e => e.name === 'shadow-black/25')?.kindModifiers).toBe('color');
847
+ });
848
+ it('should detect arbitrary color values', () => {
849
+ const sourceCode = '<div className="">Hello</div>';
850
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
851
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
852
+ expect(result).toBeDefined();
853
+ expect(result.entries.find(e => e.name === 'bg-[#ff0000]')?.kindModifiers).toBe('color');
854
+ });
855
+ it('should detect special color values (transparent, current, inherit)', () => {
856
+ const sourceCode = '<div className="">Hello</div>';
857
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
858
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
859
+ expect(result).toBeDefined();
860
+ expect(result.entries.find(e => e.name === 'text-transparent')?.kindModifiers).toBe('color');
861
+ expect(result.entries.find(e => e.name === 'text-current')?.kindModifiers).toBe('color');
862
+ expect(result.entries.find(e => e.name === 'text-inherit')?.kindModifiers).toBe('color');
863
+ });
864
+ it('should detect placeholder, caret, and decoration colors', () => {
865
+ const sourceCode = '<div className="">Hello</div>';
866
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
867
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
868
+ expect(result).toBeDefined();
869
+ expect(result.entries.find(e => e.name === 'placeholder-gray-400')?.kindModifiers).toBe('color');
870
+ expect(result.entries.find(e => e.name === 'caret-blue-500')?.kindModifiers).toBe('color');
871
+ expect(result.entries.find(e => e.name === 'decoration-pink-500')?.kindModifiers).toBe('color');
872
+ });
873
+ it('should NOT mark gradient direction as color', () => {
874
+ const sourceCode = '<div className="">Hello</div>';
875
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
876
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, 16, undefined);
877
+ expect(result).toBeDefined();
878
+ // bg-gradient-to-r is not a color, it's a gradient direction
879
+ expect(result.entries.find(e => e.name === 'bg-gradient-to-r')?.kindModifiers).toBe('');
880
+ });
881
+ });
882
+ describe('getCompletionEntryDetails - additional cases', () => {
883
+ it('should set color kindModifier in completion details', () => {
884
+ jest
885
+ .spyOn(validator, 'getCssForClasses')
886
+ .mockReturnValue(['.bg-red-500 { background-color: rgb(239 68 68); }']);
887
+ const sourceCode = '<div className="bg-red-500">Hello</div>';
888
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
889
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'bg-red-500');
890
+ expect(result).toBeDefined();
891
+ expect(result.kindModifiers).toBe('color');
892
+ });
893
+ it('should NOT set color kindModifier for non-color class in details', () => {
894
+ const sourceCode = '<div className="flex">Hello</div>';
895
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
896
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'flex');
897
+ expect(result).toBeDefined();
898
+ expect(result.kindModifiers).toBe('');
899
+ });
900
+ it('should handle class with no CSS definition gracefully', () => {
901
+ jest.spyOn(validator, 'getCssForClasses').mockReturnValue([null]);
902
+ jest.spyOn(validator, 'isValidClass').mockReturnValue(true);
903
+ const sourceCode = '<div className="unknown-class">Hello</div>';
904
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
905
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'unknown-class');
906
+ expect(result).toBeDefined();
907
+ expect(result.name).toBe('unknown-class');
908
+ expect(result.documentation).toEqual([]);
909
+ expect(result.displayParts[0].text).toBe('unknown-class');
910
+ });
911
+ it('should format CSS with already formatted multiline input', () => {
912
+ const multilineCss = `.complex {\n display: flex;\n align-items: center;\n}`;
913
+ jest.spyOn(validator, 'getCssForClasses').mockReturnValue([multilineCss]);
914
+ jest.spyOn(validator, 'isValidClass').mockReturnValue(true);
915
+ const sourceCode = '<div className="complex">Hello</div>';
916
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
917
+ const result = completionService.getCompletionEntryDetails(ts, sourceFile, 16, 'complex');
918
+ expect(result).toBeDefined();
919
+ // Should preserve the already formatted CSS
920
+ expect(result.documentation[0].text).toContain(multilineCss);
921
+ });
922
+ });
923
+ describe('template literals', () => {
924
+ it('should provide completions in template literal utility function', () => {
925
+ const sourceCode = 'const x = cn(`fl`);';
926
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
927
+ // Position inside template literal
928
+ const position = 16;
929
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
930
+ expect(result).toBeDefined();
931
+ const names = result.entries.map(e => e.name);
932
+ expect(names).toContain('flex');
933
+ });
934
+ it('should provide completions in NoSubstitutionTemplateLiteral inside utility function', () => {
935
+ const sourceCode = 'const cls = clsx(`flex fl`);';
936
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
937
+ // Position after "fl" inside template literal
938
+ const position = 25;
939
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
940
+ expect(result).toBeDefined();
941
+ const names = result.entries.map(e => e.name);
942
+ expect(names).toContain('flex-row');
943
+ expect(names).toContain('flex-col');
944
+ });
945
+ });
946
+ describe('boundary conditions', () => {
947
+ it('should NOT provide completions inside arrow function body', () => {
948
+ const customConfig = {
949
+ utilityFunctions: [],
950
+ tailwindVariantsEnabled: false,
951
+ classVarianceAuthorityEnabled: false
952
+ };
953
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
954
+ const sourceCode = 'const fn = () => { const x = "fl"; };';
955
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
956
+ // Position inside string in arrow function (not a utility function context)
957
+ const position = 32;
958
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
959
+ expect(result).toBeUndefined();
960
+ });
961
+ it('should NOT provide completions inside regular function body', () => {
962
+ const customConfig = {
963
+ utilityFunctions: [],
964
+ tailwindVariantsEnabled: false,
965
+ classVarianceAuthorityEnabled: false
966
+ };
967
+ const customService = new CompletionService_1.CompletionService(validator, new Logger_1.NoOpLogger(), customConfig);
968
+ const sourceCode = 'function fn() { const x = "fl"; }';
969
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
970
+ // Position inside string in function (not a utility function context)
971
+ const position = 29;
972
+ const result = customService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
973
+ expect(result).toBeUndefined();
974
+ });
975
+ it('should provide completions when utility function is inside arrow function', () => {
976
+ const sourceCode = 'const fn = () => cn("fl");';
977
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
978
+ // Position inside cn() string argument
979
+ const position = 23;
980
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
981
+ expect(result).toBeDefined();
982
+ const names = result.entries.map(e => e.name);
983
+ expect(names).toContain('flex');
984
+ });
985
+ });
986
+ describe('case sensitivity', () => {
987
+ it('should handle case-insensitive prefix matching', () => {
988
+ const sourceCode = '<div className="FL">Hello</div>';
989
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
990
+ const position = 18;
991
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
992
+ expect(result).toBeDefined();
993
+ const names = result.entries.map(e => e.name);
994
+ // Should still match lowercase flex classes even with uppercase prefix
995
+ expect(names).toContain('flex');
996
+ expect(names).toContain('flex-row');
997
+ });
998
+ });
999
+ describe('replacement span', () => {
1000
+ it('should set correct replacement span at start of string', () => {
1001
+ const sourceCode = '<div className="fl">Hello</div>';
1002
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1003
+ const position = 18;
1004
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
1005
+ expect(result).toBeDefined();
1006
+ const flexEntry = result.entries.find(e => e.name === 'flex');
1007
+ expect(flexEntry.replacementSpan).toEqual({
1008
+ start: 16, // Start of "fl"
1009
+ length: 2 // Length of "fl"
1010
+ });
1011
+ });
1012
+ it('should set correct replacement span after space', () => {
1013
+ const sourceCode = '<div className="flex ite">Hello</div>';
1014
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1015
+ const position = 24;
1016
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
1017
+ expect(result).toBeDefined();
1018
+ const itemsEntry = result.entries.find(e => e.name === 'items-center');
1019
+ expect(itemsEntry.replacementSpan).toEqual({
1020
+ start: 21, // Start of "ite" (after "flex ")
1021
+ length: 3 // Length of "ite"
1022
+ });
1023
+ });
1024
+ it('should set zero-length replacement span when no prefix', () => {
1025
+ const sourceCode = '<div className="flex ">Hello</div>';
1026
+ const sourceFile = ts.createSourceFile('test.tsx', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1027
+ // Position after space (no prefix yet)
1028
+ const position = 21;
1029
+ const result = completionService.getCompletionsAtPosition(ts, sourceFile, position, undefined);
1030
+ expect(result).toBeDefined();
1031
+ const itemsEntry = result.entries.find(e => e.name === 'items-center');
1032
+ expect(itemsEntry.replacementSpan).toEqual({
1033
+ start: 21,
1034
+ length: 0 // No prefix to replace
1035
+ });
1036
+ });
1037
+ });
1038
+ });
1039
+ //# sourceMappingURL=CompletionService.spec.js.map