Skip to content

Commit 59d6640

Browse files
committed
feat: autocompletion for jsx, style and styled
1 parent 50acfb8 commit 59d6640

9 files changed

+738
-76
lines changed

src/language-service.ts

+26-12
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ const prepareText = (value: string): string =>
6868
.replace(/\d(?!$|\d)/g, '$& ')
6969
.replace(/\s+/g, ' ')
7070

71-
export class TwindTemplateLanguageService implements TemplateLanguageService {
71+
export class TwindLanguageService implements TemplateLanguageService {
7272
private readonly typescript: typeof ts
7373
private readonly configurationManager: ConfigurationManager
7474
private readonly logger: Logger
@@ -234,11 +234,11 @@ export class TwindTemplateLanguageService implements TemplateLanguageService {
234234
switch (info.id) {
235235
case 'UNKNOWN_DIRECTIVE': {
236236
return {
237-
messageText: `Unknown utility "${rule.name}"`,
237+
messageText: `Unknown utility "${info.rule}"`,
238238
start: rule.loc.start,
239239
length: rule.loc.end - rule.loc.start,
240240
file: context.node.getSourceFile(),
241-
category: this.typescript.DiagnosticCategory.Warning,
241+
category: this.typescript.DiagnosticCategory.Error,
242242
code: ErrorCodes.UNKNOWN_DIRECTIVE,
243243
}
244244
}
@@ -251,17 +251,36 @@ export class TwindTemplateLanguageService implements TemplateLanguageService {
251251
start: rule.loc.start,
252252
length: rule.loc.end - rule.loc.start,
253253
file: context.node.getSourceFile(),
254-
category: this.typescript.DiagnosticCategory.Warning,
254+
category: this.typescript.DiagnosticCategory.Error,
255255
code: ErrorCodes.UNKNOWN_THEME_VALUE,
256256
}
257257
}
258258
}
259259
}
260260
})
261+
// Check non-empty directive
262+
.concat(
263+
rule.name
264+
? undefined
265+
: {
266+
messageText: `Missing utility class`,
267+
start: rule.loc.start,
268+
length: rule.loc.end - rule.loc.start,
269+
file: context.node.getSourceFile(),
270+
category: this.typescript.DiagnosticCategory.Error,
271+
code: ErrorCodes.UNKNOWN_DIRECTIVE,
272+
},
273+
)
261274
// check if every rule.variants exist
262275
.concat(
263276
rule.variants
264-
.filter((variant) => !this._twind.completions.variants.has(variant.value))
277+
.filter(
278+
(variant) =>
279+
!(
280+
this._twind.completions.variants.has(variant.value) ||
281+
(variant.value[0] == '[' && variant.value[variant.value.length - 2] == ']')
282+
),
283+
)
265284
.map(
266285
(variant): ts.Diagnostic => ({
267286
messageText: `Unknown variant "${variant.value}"`,
@@ -390,13 +409,8 @@ export class TwindTemplateLanguageService implements TemplateLanguageService {
390409
keys: [(completion) => prepareText(completion.value + ' ' + completion.detail)],
391410
baseSort,
392411
}),
393-
...matchSorter(utilities, needle, {
394-
// threshold: matchSorter.rankings.ACRONYM,
395-
keys: [(completion) => prepareText(completion.value)],
396-
baseSort,
397-
}),
398-
...matchSorter(variants, needle, {
399-
// threshold: matchSorter.rankings.ACRONYM,
412+
...matchSorter([...utilities, ...variants], needle, {
413+
// threshold: matchSorter.rankings.MATCHES,
400414
keys: [(completion) => prepareText(completion.value)],
401415
baseSort,
402416
}),

src/load-twind-config.ts renamed to src/load.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ export const findConfig = (cwd = process.cwd()): string | undefined =>
3131
findUp.sync(TWIND_CONFIG_FILES, { cwd }) ||
3232
findUp.sync(TAILWIND_CONFIG_FILES, { cwd })
3333

34-
export const loadConfig = (configFile: string, cwd = process.cwd()): Configuration => {
34+
export const loadFile = <T>(file: string, cwd = process.cwd()): T => {
3535
const result = buildSync({
3636
bundle: true,
37-
entryPoints: [configFile],
37+
entryPoints: [file],
3838
format: 'cjs',
3939
platform: 'node',
4040
target: 'es2018', // `node${process.versions.node}`,
@@ -60,13 +60,19 @@ export const loadConfig = (configFile: string, cwd = process.cwd()): Configurati
6060
result.outputFiles[0].text,
6161
)(
6262
module.exports,
63-
Module.createRequire?.(configFile) || Module.createRequireFromPath(configFile),
63+
Module.createRequire?.(file) || Module.createRequireFromPath(file),
6464
module,
65-
configFile,
66-
Path.dirname(configFile),
65+
file,
66+
Path.dirname(file),
6767
)
6868

69-
const config = module.exports.default || module.exports || {}
69+
return module.exports as T
70+
}
71+
72+
export const loadConfig = (configFile: string, cwd = process.cwd()): Configuration => {
73+
const exports = loadFile<{ default: Configuration } & Configuration>(configFile, cwd)
74+
75+
const config = exports.default || exports || {}
7076

7177
// could be tailwind config
7278
if (

src/match.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
export type Predicate =
3+
| ((this: undefined, value: any, key: undefined, object: any, matcher: undefined) => unknown)
4+
| (<T extends Predicates>(this: T, value: any, key: string, object: any, matcher: T) => unknown)
5+
6+
export interface RegExpLike {
7+
/**
8+
* Returns a Boolean value that indicates whether or not a pattern exists in a searched string.
9+
* @param string String on which to perform the search.
10+
*/
11+
test(string: string): boolean
12+
}
13+
14+
export type Matcher = Predicate | Predicates | RegExp | unknown
15+
16+
/**
17+
* Defines the predicate properties to be invoked with the corresponding property values of a given object.
18+
*/
19+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
20+
export interface Predicates extends Record<string | number | symbol, Matcher | Matcher[]> {
21+
// Support cyclic references
22+
}
23+
24+
export function match(
25+
value: unknown,
26+
predicate: Matcher,
27+
key?: string | undefined,
28+
object?: unknown,
29+
matcher?: Matcher | Matcher[],
30+
): boolean {
31+
if (isEqual(value, predicate)) {
32+
return true
33+
}
34+
35+
if (Array.isArray(predicate)) {
36+
return predicate.some((item) => match(value, item, key, object, matcher))
37+
}
38+
39+
if (typeof predicate == 'function') {
40+
return Boolean(predicate.call(matcher, value, key, object, matcher))
41+
}
42+
43+
if (typeof value == 'string' && isRegExpLike(predicate)) {
44+
return predicate.test(value)
45+
}
46+
47+
if (isObjectLike(value) && isObjectLike(predicate)) {
48+
return Object.keys(predicate).every((key) =>
49+
match((value as any)[key], (predicate as any)[key], key, value, predicate),
50+
)
51+
}
52+
53+
return false
54+
}
55+
56+
/**
57+
* Performs a [SameValueZero](https://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) comparison
58+
* between two values to determine if they are equivalent.
59+
*
60+
* **Note** SameValueZero differs from SameValue only in its treatment of `+0` and `-0`.
61+
* For SameValue comparison use `Object.is()`.
62+
*/
63+
function isEqual(value: unknown, other: unknown): boolean {
64+
return value === other || (Number.isNaN(value) && Number.isNaN(other))
65+
}
66+
67+
/**
68+
* Return `true` if `value` is an object-like.
69+
*
70+
* A value is object-like if it's not `null` and has a `typeof` result of `"object"`.
71+
*
72+
* **Note** Keep in mind that functions are objects too.
73+
*
74+
* @param value to check
75+
*/
76+
// eslint-disable-next-line @typescript-eslint/ban-types
77+
function isObjectLike(value: unknown): value is object {
78+
return value != null && typeof value == 'object'
79+
}
80+
81+
function isRegExpLike(value: unknown): value is RegExpLike {
82+
return isObjectLike(value) && typeof (value as RegExp).test == 'function'
83+
}

src/parser.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,38 @@ const test = suite('Parser')
1414
;([
1515
['', []],
1616
[' \t \n\r ', []],
17+
[
18+
'focus:',
19+
[
20+
{
21+
raw: '',
22+
value: 'focus:',
23+
name: '',
24+
prefix: '',
25+
important: false,
26+
negated: false,
27+
loc: { start: 6, end: 6 },
28+
spans: [{ start: 0, end: 6 }],
29+
variants: [{ name: 'focus', raw: 'focus:', value: 'focus:', loc: { start: 0, end: 6 } }],
30+
},
31+
],
32+
],
33+
[
34+
'focus: ',
35+
[
36+
{
37+
raw: '',
38+
value: 'focus:',
39+
name: '',
40+
prefix: '',
41+
important: false,
42+
negated: false,
43+
loc: { start: 6, end: 6 },
44+
spans: [{ start: 0, end: 6 }],
45+
variants: [{ name: 'focus', raw: 'focus:', value: 'focus:', loc: { start: 0, end: 6 } }],
46+
},
47+
],
48+
],
1749
[
1850
'underline',
1951
[

src/parser.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ export function astish(text: string, atPosition = Infinity): Group {
330330
case '\t':
331331
case '\n':
332332
case '\r':
333-
if (buffer) {
333+
if (buffer || node.kind == NodeKind.Variant) {
334334
parent = node = node.next = createIdentifier(
335335
node,
336336
parent,
@@ -358,7 +358,7 @@ export function astish(text: string, atPosition = Infinity): Group {
358358
}
359359

360360
// Consume remaining buffer or completion triggered at the end
361-
if (buffer || atPosition === text.length) {
361+
if (buffer || node.kind == NodeKind.Variant || atPosition === text.length) {
362362
node.next = createIdentifier(node, parent, buffer, start, true)
363363
}
364364

0 commit comments

Comments
 (0)