Skip to content

Commit 518c5d8

Browse files
committed
Refactor and fix bugs
- Reload completion providers when the language modes configurations change - Rename configurations - Reuse list of HTML and JavaScript based languages to feed the Emmet completion provider
1 parent 4ab6c92 commit 518c5d8

File tree

2 files changed

+148
-94
lines changed

2 files changed

+148
-94
lines changed

package.json

+21-21
Original file line numberDiff line numberDiff line change
@@ -47,40 +47,40 @@
4747
"default": false,
4848
"description": "Enables completion when you're writing Emmet abbreviations."
4949
},
50-
"html-css-class-completion.enabledJavascriptLanguages": {
50+
"html-css-class-completion.HTMLLanguages": {
5151
"type": "array",
52-
"description": "A list of Javascript languages where suggestions are enabled.",
52+
"description": "A list of HTML based languages where suggestions are enabled.",
5353
"default": [
54-
"typescriptreact",
55-
"javascript",
56-
"javascriptreact"
54+
"html",
55+
"vue",
56+
"razor",
57+
"blade",
58+
"handlebars",
59+
"twig",
60+
"django-html",
61+
"php",
62+
"markdown",
63+
"erb",
64+
"ejs",
65+
"svelte"
5766
]
5867
},
59-
"html-css-class-completion.enabledCSSLanguages": {
68+
"html-css-class-completion.CSSLanguages": {
6069
"type": "array",
61-
"description": "A list of CSS languages where suggestions are enabled.",
70+
"description": "A list of CSS based languages where suggestions are enabled.",
6271
"default": [
6372
"css",
6473
"sass",
6574
"scss"
6675
]
6776
},
68-
"html-css-class-completion.enabledHTMLLanguages": {
77+
"html-css-class-completion.JavaScriptLanguages": {
6978
"type": "array",
70-
"description": "A list of HTML languages where suggestions are enabled.",
79+
"description": "A list of JavaScript based languages where suggestions are enabled.",
7180
"default": [
72-
"html",
73-
"django-html",
74-
"razor",
75-
"php",
76-
"blade",
77-
"vue",
78-
"twig",
79-
"markdown",
80-
"erb",
81-
"handlebars",
82-
"ejs",
83-
"svelte"
81+
"javascript",
82+
"javascriptreact",
83+
"typescriptreact"
8484
]
8585
}
8686
}

src/extension.ts

+127-73
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,30 @@ import Fetcher from "./fetcher";
1212
import Notifier from "./notifier";
1313
import ParseEngineGateway from "./parse-engine-gateway";
1414

15-
const notifier: Notifier = new Notifier("html-css-class-completion.cache");
15+
enum Command {
16+
Cache = "html-css-class-completion.cache",
17+
}
18+
19+
enum Configuration {
20+
IncludeGlobPattern = "html-css-class-completion.includeGlobPattern",
21+
ExcludeGlobPattern = "html-css-class-completion.excludeGlobPattern",
22+
EnableEmmetSupport = "html-css-class-completion.enableEmmetSupport",
23+
HTMLLanguages = "html-css-class-completion.HTMLLanguages",
24+
CSSLanguages = "html-css-class-completion.CSSLanguages",
25+
JavaScriptLanguages = "html-css-class-completion.JavaScriptLanguages",
26+
}
27+
28+
const notifier: Notifier = new Notifier(Command.Cache);
1629
let uniqueDefinitions: CssClassDefinition[] = [];
1730

1831
const completionTriggerChars = ['"', "'", " ", "."];
1932

2033
let caching = false;
2134

22-
const emmetDisposables: Array<{ dispose(): any }> = [];
35+
const htmlDisposables: Disposable[] = [];
36+
const cssDisposables: Disposable[] = [];
37+
const javaScriptDisposables: Disposable[] = [];
38+
const emmetDisposables: Disposable[] = [];
2339

2440
async function cache(): Promise<void> {
2541
try {
@@ -77,76 +93,125 @@ async function cache(): Promise<void> {
7793
}
7894
}
7995

80-
function provideCompletionItemsGenerator(languageSelector: string, classMatchRegex: RegExp,
81-
classPrefix: string = "", splitChar: string = " ") {
82-
return languages.registerCompletionItemProvider(languageSelector, {
83-
provideCompletionItems(document: TextDocument, position: Position): CompletionItem[] {
84-
const start: Position = new Position(position.line, 0);
85-
const range: Range = new Range(start, position);
86-
const text: string = document.getText(range);
87-
88-
// Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute
89-
const rawClasses: RegExpMatchArray = text.match(classMatchRegex);
90-
if (!rawClasses || rawClasses.length === 1) {
91-
return [];
92-
}
96+
const registerCompletionProvider = (
97+
languageSelector: string,
98+
classMatchRegex: RegExp,
99+
classPrefix = "",
100+
splitChar = " "
101+
) => languages.registerCompletionItemProvider(languageSelector, {
102+
provideCompletionItems(document: TextDocument, position: Position): CompletionItem[] {
103+
const start: Position = new Position(position.line, 0);
104+
const range: Range = new Range(start, position);
105+
const text: string = document.getText(range);
106+
107+
// Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute
108+
const rawClasses: RegExpMatchArray = text.match(classMatchRegex);
109+
if (!rawClasses || rawClasses.length === 1) {
110+
return [];
111+
}
93112

94-
// Will store the classes found on the class attribute
95-
const classesOnAttribute = rawClasses[1].split(splitChar);
113+
// Will store the classes found on the class attribute
114+
const classesOnAttribute = rawClasses[1].split(splitChar);
96115

97-
// Creates a collection of CompletionItem based on the classes already cached
98-
const completionItems = uniqueDefinitions.map((definition) => {
99-
const completionItem = new CompletionItem(definition.className, CompletionItemKind.Variable);
100-
const completionClassName = `${classPrefix}${definition.className}`;
116+
// Creates a collection of CompletionItem based on the classes already cached
117+
const completionItems = uniqueDefinitions.map((definition) => {
118+
const completionItem = new CompletionItem(definition.className, CompletionItemKind.Variable);
119+
const completionClassName = `${classPrefix}${definition.className}`;
101120

102-
completionItem.filterText = completionClassName;
103-
completionItem.insertText = completionClassName;
121+
completionItem.filterText = completionClassName;
122+
completionItem.insertText = completionClassName;
104123

105-
return completionItem;
106-
});
124+
return completionItem;
125+
});
107126

108-
// Removes from the collection the classes already specified on the class attribute
109-
for (const classOnAttribute of classesOnAttribute) {
110-
for (let j = 0; j < completionItems.length; j++) {
111-
if (completionItems[j].insertText === classOnAttribute) {
112-
completionItems.splice(j, 1);
113-
}
127+
// Removes from the collection the classes already specified on the class attribute
128+
for (const classOnAttribute of classesOnAttribute) {
129+
for (let j = 0; j < completionItems.length; j++) {
130+
if (completionItems[j].insertText === classOnAttribute) {
131+
completionItems.splice(j, 1);
114132
}
115133
}
134+
}
116135

117-
return completionItems;
118-
},
119-
}, ...completionTriggerChars);
120-
}
121-
122-
function enableEmmetSupport(disposables: Disposable[]) {
123-
const emmetRegex = /(?=\.)([\w-\. ]*$)/;
124-
const languageModes = ["html", "django-html", "razor", "php", "blade", "vue", "twig", "markdown", "erb",
125-
"handlebars", "ejs", "typescriptreact", "javascript", "javascriptreact"];
126-
languageModes.forEach((language) => {
127-
emmetDisposables.push(provideCompletionItemsGenerator(language, emmetRegex, "", "."));
128-
});
136+
return completionItems;
137+
},
138+
}, ...completionTriggerChars);
139+
140+
const registerHTMLProviders = (disposables: Disposable[]) =>
141+
workspace.getConfiguration()
142+
.get<string[]>(Configuration.HTMLLanguages)
143+
.forEach((extension) => {
144+
disposables.push(registerCompletionProvider(extension, /class=["|']([\w- ]*$)/));
145+
});
146+
147+
const registerCSSProviders = (disposables: Disposable[]) =>
148+
workspace.getConfiguration()
149+
.get<string[]>(Configuration.CSSLanguages)
150+
.forEach((extension) => {
151+
// The @apply rule was a CSS proposal which has since been abandoned,
152+
// check the proposal for more info: http://tabatkins.github.io/specs/css-apply-rule/
153+
// Its support should probably be removed
154+
disposables.push(registerCompletionProvider(extension, /@apply ([.\w- ]*$)/, "."));
155+
});
156+
157+
const registerJavaScriptProviders = (disposables: Disposable[]) =>
158+
workspace.getConfiguration()
159+
.get<string[]>(Configuration.JavaScriptLanguages)
160+
.forEach((extension) => {
161+
disposables.push(registerCompletionProvider(extension, /className=["|']([\w- ]*$)/));
162+
disposables.push(registerCompletionProvider(extension, /class=["|']([\w- ]*$)/));
163+
});
164+
165+
function registerEmmetProviders(disposables: Disposable[]) {
166+
const emmetRegex = /(?=\.)([\w-. ]*$)/;
167+
168+
const registerProviders = (modes: string[]) => {
169+
modes.forEach((language) => {
170+
disposables.push(registerCompletionProvider(language, emmetRegex, "", "."));
171+
});
172+
};
173+
174+
registerProviders(
175+
workspace.getConfiguration().get<string[]>(Configuration.HTMLLanguages)
176+
);
177+
registerProviders(
178+
workspace.getConfiguration().get<string[]>(Configuration.JavaScriptLanguages)
179+
);
129180
}
130181

131-
function disableEmmetSupport(disposables: Disposable[]) {
132-
for (const emmetDisposable of disposables) {
133-
emmetDisposable.dispose();
134-
}
182+
function unregisterProviders(disposables: Disposable[]) {
183+
disposables.forEach(disposable => disposable.dispose());
184+
disposables.length = 0;
135185
}
136186

137187
export async function activate(context: ExtensionContext): Promise<void> {
138188
const disposables: Disposable[] = [];
139189
workspace.onDidChangeConfiguration(async (e) => {
140190
try {
141-
if (e.affectsConfiguration("html-css-class-completion.includeGlobPattern") ||
142-
e.affectsConfiguration("html-css-class-completion.excludeGlobPattern")) {
191+
if (e.affectsConfiguration(Configuration.IncludeGlobPattern) ||
192+
e.affectsConfiguration(Configuration.ExcludeGlobPattern)) {
143193
await cache();
144194
}
145195

146-
if (e.affectsConfiguration("html-css-class-completion.enableEmmetSupport")) {
196+
if (e.affectsConfiguration(Configuration.EnableEmmetSupport)) {
147197
const isEnabled = workspace.getConfiguration()
148-
.get<boolean>("html-css-class-completion.enableEmmetSupport");
149-
isEnabled ? enableEmmetSupport(emmetDisposables) : disableEmmetSupport(emmetDisposables);
198+
.get<boolean>(Configuration.EnableEmmetSupport);
199+
isEnabled ? registerEmmetProviders(emmetDisposables) : unregisterProviders(emmetDisposables);
200+
}
201+
202+
if (e.affectsConfiguration(Configuration.HTMLLanguages)) {
203+
unregisterProviders(htmlDisposables);
204+
registerHTMLProviders(htmlDisposables);
205+
}
206+
207+
if (e.affectsConfiguration(Configuration.CSSLanguages)) {
208+
unregisterProviders(cssDisposables);
209+
registerCSSProviders(cssDisposables);
210+
}
211+
212+
if (e.affectsConfiguration(Configuration.JavaScriptLanguages)) {
213+
unregisterProviders(javaScriptDisposables);
214+
registerJavaScriptProviders(javaScriptDisposables);
150215
}
151216
} catch (err) {
152217
const newErr = new VError(err, "Failed to automatically reload the extension after the configuration change");
@@ -156,7 +221,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
156221
}, null, disposables);
157222
context.subscriptions.push(...disposables);
158223

159-
context.subscriptions.push(commands.registerCommand("html-css-class-completion.cache", async () => {
224+
context.subscriptions.push(commands.registerCommand(Command.Cache, async () => {
160225
if (caching) {
161226
return;
162227
}
@@ -173,27 +238,13 @@ export async function activate(context: ExtensionContext): Promise<void> {
173238
}
174239
}));
175240

176-
// Enable Emmet Completion on startup if param is set to true
177-
if (workspace.getConfiguration().get<boolean>("html-css-class-completion.enableEmmetSupport")) {
178-
enableEmmetSupport(emmetDisposables);
241+
if (workspace.getConfiguration().get<boolean>(Configuration.EnableEmmetSupport)) {
242+
registerEmmetProviders(emmetDisposables);
179243
}
180244

181-
// Javascript based extensions
182-
workspace.getConfiguration().get<string[]>("html-css-class-completion.enabledJavascriptLanguages").forEach((extension) => {
183-
context.subscriptions.push(provideCompletionItemsGenerator(extension, /className=["|']([\w- ]*$)/));
184-
context.subscriptions.push(provideCompletionItemsGenerator(extension, /class=["|']([\w- ]*$)/));
185-
});
186-
187-
// HTML based extensions
188-
workspace.getConfiguration().get<string[]>("html-css-class-completion.enabledHTMLLanguages").forEach((extension) => {
189-
context.subscriptions.push(provideCompletionItemsGenerator(extension, /class=["|']([\w- ]*$)/));
190-
});
191-
192-
// CSS based extensions
193-
workspace.getConfiguration().get<string[]>("html-css-class-completion.enabledCSSLanguages").forEach((extension) => {
194-
// Support for Tailwind CSS
195-
context.subscriptions.push(provideCompletionItemsGenerator(extension, /@apply ([\.\w- ]*$)/, "."));
196-
});
245+
registerHTMLProviders(htmlDisposables);
246+
registerCSSProviders(cssDisposables);
247+
registerJavaScriptProviders(javaScriptDisposables);
197248

198249
caching = true;
199250
try {
@@ -208,5 +259,8 @@ export async function activate(context: ExtensionContext): Promise<void> {
208259
}
209260

210261
export function deactivate(): void {
211-
emmetDisposables.forEach((disposable) => disposable.dispose());
262+
unregisterProviders(htmlDisposables);
263+
unregisterProviders(cssDisposables);
264+
unregisterProviders(javaScriptDisposables);
265+
unregisterProviders(emmetDisposables);
212266
}

0 commit comments

Comments
 (0)