tailwind-typescript-plugin 1.4.1-beta.2 → 1.4.1-beta.20

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