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.
- package/CHANGELOG.md +73 -0
- package/README.md +206 -34
- package/lib/core/interfaces.d.ts +2 -2
- package/lib/core/interfaces.d.ts.map +1 -1
- package/lib/core/types.d.ts +19 -1
- package/lib/core/types.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.d.ts +49 -2
- package/lib/extractors/BaseExtractor.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.js +184 -5
- package/lib/extractors/BaseExtractor.js.map +1 -1
- package/lib/extractors/CvaExtractor.js +1 -1
- package/lib/extractors/CvaExtractor.js.map +1 -1
- package/lib/extractors/ExpressionExtractor.d.ts.map +1 -1
- package/lib/extractors/ExpressionExtractor.js +35 -5
- package/lib/extractors/ExpressionExtractor.js.map +1 -1
- package/lib/extractors/JsxAttributeExtractor.js +1 -1
- package/lib/extractors/JsxAttributeExtractor.js.map +1 -1
- package/lib/extractors/TailwindVariantsExtractor.d.ts.map +1 -1
- package/lib/extractors/TailwindVariantsExtractor.js +1 -5
- package/lib/extractors/TailwindVariantsExtractor.js.map +1 -1
- package/lib/extractors/VariableReferenceExtractor.d.ts.map +1 -1
- package/lib/extractors/VariableReferenceExtractor.js +21 -0
- package/lib/extractors/VariableReferenceExtractor.js.map +1 -1
- package/lib/infrastructure/TailwindConflictDetector.d.ts +28 -18
- package/lib/infrastructure/TailwindConflictDetector.d.ts.map +1 -1
- package/lib/infrastructure/TailwindConflictDetector.js +216 -486
- package/lib/infrastructure/TailwindConflictDetector.js.map +1 -1
- package/lib/infrastructure/TailwindValidator.d.ts +15 -0
- package/lib/infrastructure/TailwindValidator.d.ts.map +1 -1
- package/lib/infrastructure/TailwindValidator.js +84 -5
- package/lib/infrastructure/TailwindValidator.js.map +1 -1
- package/lib/infrastructure/TailwindValidator.spec.js +194 -0
- package/lib/infrastructure/TailwindValidator.spec.js.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.d.ts +26 -0
- package/lib/plugin/TailwindTypescriptPlugin.d.ts.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.js +185 -1
- package/lib/plugin/TailwindTypescriptPlugin.js.map +1 -1
- package/lib/services/ClassNameExtractionService.d.ts +2 -2
- package/lib/services/ClassNameExtractionService.d.ts.map +1 -1
- package/lib/services/ClassNameExtractionService.js.map +1 -1
- package/lib/services/CompletionService.d.ts +113 -0
- package/lib/services/CompletionService.d.ts.map +1 -0
- package/lib/services/CompletionService.js +488 -0
- package/lib/services/CompletionService.js.map +1 -0
- package/lib/services/CompletionService.spec.d.ts +2 -0
- package/lib/services/CompletionService.spec.d.ts.map +1 -0
- package/lib/services/CompletionService.spec.js +1039 -0
- package/lib/services/CompletionService.spec.js.map +1 -0
- package/lib/services/ConflictClassDetection.spec.js +24 -19
- package/lib/services/ConflictClassDetection.spec.js.map +1 -1
- package/lib/services/DuplicateClassDetection.spec.js +41 -60
- package/lib/services/DuplicateClassDetection.spec.js.map +1 -1
- package/lib/services/PluginConfigService.d.ts +2 -1
- package/lib/services/PluginConfigService.d.ts.map +1 -1
- package/lib/services/PluginConfigService.js +19 -12
- package/lib/services/PluginConfigService.js.map +1 -1
- package/lib/services/ValidationService.d.ts +4 -2
- package/lib/services/ValidationService.d.ts.map +1 -1
- package/lib/services/ValidationService.js +10 -7
- package/lib/services/ValidationService.js.map +1 -1
- package/package.json +4 -2
- package/lib/extractors/StringLiteralExtractor.d.ts +0 -12
- package/lib/extractors/StringLiteralExtractor.d.ts.map +0 -1
- package/lib/extractors/StringLiteralExtractor.js +0 -21
- package/lib/extractors/StringLiteralExtractor.js.map +0 -1
- package/lib/services/ClassNameExtractionService.original.d.ts +0 -20
- package/lib/services/ClassNameExtractionService.original.d.ts.map +0 -1
- package/lib/services/ClassNameExtractionService.original.js +0 -48
- 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
|