Skip to content

Commit 62137a1

Browse files
committed
Reuse attribute extractor for definition provider
Refactor to use the class attribute extractor for the definition provider so that it works consistently with the completion provider.
1 parent 7148251 commit 62137a1

File tree

3 files changed

+153
-142
lines changed

3 files changed

+153
-142
lines changed

src/common/class-attribute-matcher.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
type ClassAttributeMatcher =
2+
{
3+
type: "regexp"
4+
classMatchRegex: RegExp;
5+
classPrefix?: string;
6+
splitChar?: string;
7+
} | {
8+
type: "jsx";
9+
};
10+
11+
export default ClassAttributeMatcher;

src/extension.ts

Lines changed: 14 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {
77
ExtensionContext, languages, Location, Position, Range, TextDocument, Uri, window,
88
workspace,
99
} from "vscode";
10+
import type ClassAttributeMatcher from "./common/class-attribute-matcher";
1011
import CssClassDefinition from "./common/css-class-definition";
1112
import Fetcher from "./fetcher";
1213
import Notifier from "./notifier";
1314
import ParseEngineGateway from "./parse-engine-gateway";
15+
import ClassAttributeExtractor from "./parse-engines/common/class-attribute-extractor";
1416
import IParseOptions from "./parse-engines/common/parse-options";
1517

1618
enum Command {
@@ -129,128 +131,14 @@ async function cache() {
129131

130132
const registerCompletionProvider = (
131133
languageSelector: string,
132-
matcherMode: CompletionMatcherMode,
134+
matcher: ClassAttributeMatcher,
133135
classPrefix = "",
134136
) => languages.registerCompletionItemProvider(languageSelector, {
135137
provideCompletionItems(document: TextDocument, position: Position): CompletionItem[] {
136-
const start: Position = new Position(position.line, 0);
137-
const range: Range = new Range(start, position);
138-
const text: string = document.getText(range);
139-
140-
// Classes already written in the completion target. These classes are excluded.
141-
const classesOnAttribute: string[] = [];
142-
143-
switch (matcherMode.type) {
144-
case "regexp": {
145-
const { classMatchRegex, splitChar = " " } = matcherMode;
146-
// Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute.
147-
// Unless matched, completion isn't provided at the position.
148-
const rawClasses: RegExpMatchArray | null = text.match(classMatchRegex);
149-
if (!rawClasses || rawClasses.length === 1) {
150-
return [];
151-
}
152-
153-
// Will store the classes found on the class attribute.
154-
classesOnAttribute.push(...rawClasses[1].split(splitChar));
155-
break;
156-
}
157-
case "javascript": {
158-
const REGEXP1 = /className=(?:{?"|{?'|{?`)([-\w,@\\:\[\] ]*$)/;
159-
const REGEXP2 = /class=(?:{?"|{?')([-\w,@\\:\[\] ]*$)/;
160-
161-
let matched = false;
162-
163-
// Apply two regexp rules.
164-
for (const regexp of [REGEXP1, REGEXP2]) {
165-
const rawClasses = text.match(regexp);
166-
if (!rawClasses || rawClasses.length === 1) {
167-
continue;
168-
}
169-
170-
matched = true;
171-
classesOnAttribute.push(...rawClasses[1].split(" "));
172-
}
173-
174-
// Special case for `className={}`,
175-
// e.g. `className={"widget " + (p ? "widget--modified" : "")}.
176-
// The completion is provided if the position is in the braces and in a string literal.
177-
const attributeIndex = text.lastIndexOf("className={");
178-
if (attributeIndex >= 0) {
179-
const start = attributeIndex + "className={".length;
180-
let index = start;
181-
182-
// Stack to find matching braces and quotes.
183-
// Whenever an open brace or opening quote is found, push it.
184-
// When the closer is found, pop it.
185-
let stack: string[] = [];
186-
187-
const inQuote = () => {
188-
const top = stack.at(-1);
189-
return top === "\"" || top === "'" || top === "`";
190-
};
191-
192-
for (; index < text.length; index++) {
193-
const char = text[index];
194-
if (stack.length === 0 && char === "}") {
195-
break;
196-
}
197-
switch (char) {
198-
case "{":
199-
stack.push("{");
200-
break;
201-
202-
case "}": {
203-
const last = stack.at(-1);
204-
if (last === "{" || last === "${") {
205-
stack.pop();
206-
}
207-
break;
208-
}
209-
case "\"":
210-
case "'":
211-
case "`":
212-
if (stack.at(-1) === char) {
213-
stack.pop();
214-
} else {
215-
stack.push(char);
216-
}
217-
break;
218-
219-
// Escape sequence (e.g. `\"`.)
220-
case "\\":
221-
if (inQuote() && index + 1 < text.length) {
222-
index++;
223-
}
224-
break;
225-
226-
// String interpolation (`${...}`.)
227-
case "$":
228-
if (stack.at(-1) === "`" && index + 1 < text.length && text[index + 1] === "{") {
229-
stack.push("${");
230-
index++;
231-
}
232-
break;
233-
}
234-
}
235-
236-
if (index === text.length && inQuote()) {
237-
matched = true;
238-
239-
// Roughly extract all tokens that look like css name.
240-
// (E.g. in `className={"a" + (b ? "" : "")}`, both "a" and "b" are matched.)
241-
const wordMatches = text.slice(start).match(/[-\w,@\\:\[\]]+/g);
242-
if (wordMatches != null && wordMatches.length >= 1) {
243-
classesOnAttribute.push(...wordMatches);
244-
}
245-
}
246-
}
247-
248-
if (!matched) {
249-
// Unless any rule is matched, completion isn't provided at the position.
250-
return [];
251-
}
252-
break;
253-
}
138+
// Check if the cursor is on class attribute and collect class names on the attribute.
139+
const classesOnAttribute = ClassAttributeExtractor.extract(document, position, matcher);
140+
if (classesOnAttribute == null) {
141+
return [];
254142
}
255143

256144
const wordRangeAtPosition = document.getWordRangeAtPosition(position, /[-\w,@\\:\[\]]+/);
@@ -284,28 +172,12 @@ const registerCompletionProvider = (
284172
},
285173
}, ...completionTriggerChars);
286174

287-
type CompletionMatcherMode =
288-
{
289-
type: "regexp"
290-
classMatchRegex: RegExp
291-
classPrefix?: string
292-
splitChar?: string
293-
} | {
294-
type: "javascript"
295-
}
296-
297-
const registerDefinitionProvider = (languageSelector: string, classMatchRegex: RegExp) => languages.registerDefinitionProvider(languageSelector, {
175+
const registerDefinitionProvider = (languageSelector: string, matcher: ClassAttributeMatcher) => languages.registerDefinitionProvider(languageSelector, {
298176
provideDefinition(document, position, _token) {
299-
// Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute
300-
{
301-
const start: Position = new Position(position.line, 0);
302-
const range: Range = new Range(start, position);
303-
const text: string = document.getText(range);
304-
305-
const rawClasses: RegExpMatchArray | null = text.match(classMatchRegex);
306-
if (!rawClasses || rawClasses.length === 1) {
307-
return;
308-
}
177+
// Check if the cursor is on class attribute.
178+
const classesOnAttribute = ClassAttributeExtractor.extract(document, position, matcher);
179+
if (classesOnAttribute == null) {
180+
return;
309181
}
310182

311183
const range: Range | undefined = document.getWordRangeAtPosition(position, /[-\w,@\\:\[\]]+/);
@@ -347,8 +219,8 @@ const registerJavaScriptProviders = (disposables: Disposable[]) =>
347219
workspace.getConfiguration()
348220
.get<string[]>(Configuration.JavaScriptLanguages)
349221
?.forEach((extension) => {
350-
disposables.push(registerCompletionProvider(extension, { type: "javascript" }));
351-
disposables.push(registerDefinitionProvider(extension, /class(?:Name)?=(?:\{?["'`])([-\w,@\\:\[\] ]*$)/));
222+
disposables.push(registerCompletionProvider(extension, { type: "jsx" }));
223+
disposables.push(registerDefinitionProvider(extension, { type: "jsx" }));
352224
});
353225

354226
function registerEmmetProviders(disposables: Disposable[]) {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { TextDocument, Position, Range } from "vscode";
2+
import ClassAttributeMatcher from "../../common/class-attribute-matcher";
3+
4+
class ClassAttributeExtractor {
5+
static extract(document: TextDocument, position: Position, matcher: ClassAttributeMatcher): string[] | null {
6+
const start: Position = new Position(position.line, 0);
7+
const range: Range = new Range(start, position);
8+
const text: string = document.getText(range);
9+
10+
// Classes already written in the completion target. These classes are excluded.
11+
const classesOnAttribute: string[] = [];
12+
13+
switch (matcher.type) {
14+
case "regexp": {
15+
const { classMatchRegex, splitChar = " " } = matcher;
16+
// Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute.
17+
// Unless matched, completion isn't provided at the position.
18+
const rawClasses: RegExpMatchArray | null = text.match(classMatchRegex);
19+
if (!rawClasses || rawClasses.length === 1) {
20+
return null;
21+
}
22+
23+
// Will store the classes found on the class attribute.
24+
classesOnAttribute.push(...rawClasses[1].split(splitChar));
25+
break;
26+
}
27+
case "jsx": {
28+
// Pattern that matches the text between `class` attribute name and the cursor,
29+
// e.g. `className={"table__row md:w-[200px] `.
30+
const REGEXP = /class(?:Name)?=(?:{?["'`])([\w-@:\/ ]*$)/;
31+
32+
let matched = false;
33+
34+
// Apply the regexp rule.
35+
{
36+
const rawClasses = text.match(REGEXP);
37+
if (rawClasses && rawClasses.length >= 2) {
38+
matched = true;
39+
classesOnAttribute.push(...rawClasses[1].split(" "));
40+
}
41+
}
42+
43+
// Special case for `className={}`,
44+
// e.g. `className={"widget " + (p ? "widget--modified" : "")}.
45+
// The completion is provided if the position is in the braces and in a string literal.
46+
const attributeIndex = text.lastIndexOf("className={");
47+
if (attributeIndex >= 0) {
48+
const start = attributeIndex + "className={".length;
49+
let index = start;
50+
51+
// Stack to find matching braces and quotes.
52+
// Whenever an open brace or opening quote is found, push it.
53+
// When the closer is found, pop it.
54+
let stack: string[] = [];
55+
56+
const inQuote = () => {
57+
const top = stack.at(-1);
58+
return top === "\"" || top === "'" || top === "`";
59+
};
60+
61+
for (; index < text.length; index++) {
62+
const char = text[index];
63+
if (stack.length === 0 && char === "}") {
64+
break;
65+
}
66+
switch (char) {
67+
case "{":
68+
stack.push("{");
69+
break;
70+
71+
case "}": {
72+
const last = stack.at(-1);
73+
if (last === "{" || last === "${") {
74+
stack.pop();
75+
}
76+
break;
77+
}
78+
case "\"":
79+
case "'":
80+
case "`":
81+
if (stack.at(-1) === char) {
82+
stack.pop();
83+
} else {
84+
stack.push(char);
85+
}
86+
break;
87+
88+
// Escape sequence (e.g. `\"`.)
89+
case "\\":
90+
if (inQuote() && index + 1 < text.length) {
91+
index++;
92+
}
93+
break;
94+
95+
// String interpolation (`${...}`.)
96+
case "$":
97+
if (stack.at(-1) === "`" && index + 1 < text.length && text[index + 1] === "{") {
98+
stack.push("${");
99+
index++;
100+
}
101+
break;
102+
}
103+
}
104+
105+
if (index === text.length && inQuote()) {
106+
matched = true;
107+
108+
// Roughly extract all tokens that look like css name.
109+
// (E.g. in `className={"a" + (b ? "" : "")}`, both "a" and "b" are matched.)
110+
const wordMatches = text.slice(start).match(/[-\w,@\\:\[\]]+/g);
111+
if (wordMatches != null && wordMatches.length >= 1) {
112+
classesOnAttribute.push(...wordMatches);
113+
}
114+
}
115+
}
116+
117+
if (!matched) {
118+
// Unless any rule is matched, completion isn't provided at the position.
119+
return null;
120+
}
121+
break;
122+
}
123+
}
124+
return classesOnAttribute;
125+
}
126+
}
127+
128+
export default ClassAttributeExtractor;

0 commit comments

Comments
 (0)