Skip to content

Commit 8963a36

Browse files
committed
Reorganize code for future extension
1 parent d2476bc commit 8963a36

File tree

5 files changed

+483
-481
lines changed

5 files changed

+483
-481
lines changed

src/completion.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import fetch from "node-fetch";
2+
import { parse, walk } from "css-tree";
3+
import { basename, dirname, extname, isAbsolute, join } from "path";
4+
import {
5+
CancellationToken,
6+
CompletionContext,
7+
CompletionItem,
8+
CompletionItemKind,
9+
CompletionItemProvider,
10+
CompletionList,
11+
Disposable,
12+
Position,
13+
ProviderResult,
14+
Range,
15+
TextDocument,
16+
Uri,
17+
workspace,
18+
} from "vscode";
19+
20+
export class ClassCompletionItemProvider implements CompletionItemProvider, Disposable {
21+
22+
readonly none = "__!NONE!__";
23+
readonly start = new Position(0, 0);
24+
readonly cache = new Map<string, Map<string, CompletionItem>>();
25+
readonly empty = new Set<string>();
26+
readonly extends = new Map<string, Set<string>>();
27+
readonly disposables: Disposable[] = [];
28+
readonly isRemote = /^https?:\/\//i;
29+
readonly canComplete = /(id|class|className)\s*=\s*(["'])(?:(?!\2).)*$/si;
30+
readonly findLinkRel = /rel\s*=\s*(["'])((?:(?!\1).)+)\1/si;
31+
readonly findLinkHref = /href\s*=\s*(["'])((?:(?!\1).)+)\1/si;
32+
readonly findExtends = /(?:{{<|{{>|{%)\s*(?:extends)?\s*"?([\.0-9_a-z-A-Z]+)"?\s*(?:%}|}})/i;
33+
34+
dispose() {
35+
let e;
36+
37+
while (e = this.disposables.pop()) {
38+
e.dispose();
39+
}
40+
41+
this.cache.clear();
42+
this.extends.clear();
43+
}
44+
45+
watchFile(uri: Uri, listener: (e: Uri) => any) {
46+
const watcher = workspace.createFileSystemWatcher(uri.fsPath, true);
47+
48+
this.disposables.push(
49+
watcher.onDidChange(listener),
50+
watcher.onDidDelete(listener),
51+
watcher);
52+
}
53+
54+
getStyleSheets(uri: Uri): string[] {
55+
return workspace.getConfiguration("css", uri).get<string[]>("styleSheets", []);
56+
}
57+
58+
parseTextToItems(text: string, items: Map<string, CompletionItem>) {
59+
walk(parse(text), node => {
60+
61+
let kind: CompletionItemKind;
62+
63+
switch (node.type) {
64+
case "ClassSelector":
65+
kind = CompletionItemKind.Enum;
66+
break;
67+
case "IdSelector":
68+
kind = CompletionItemKind.Value;
69+
break;
70+
default:
71+
return;
72+
}
73+
74+
items.set(node.name, new CompletionItem(node.name, kind));
75+
});
76+
}
77+
78+
fetchLocal(key: string, uri: Uri): Thenable<string> {
79+
return new Promise(resolve => {
80+
if (this.cache.has(key)) {
81+
resolve(key);
82+
} else {
83+
const folder = workspace.getWorkspaceFolder(uri);
84+
85+
const file = Uri.file(folder
86+
? join(isAbsolute(key)
87+
? folder.uri.fsPath
88+
: dirname(uri.fsPath), key)
89+
: join(dirname(uri.fsPath), key));
90+
91+
workspace.fs.readFile(file).then(content => {
92+
const items = new Map<string, CompletionItem>();
93+
94+
this.parseTextToItems(content.toString(), items);
95+
this.watchFile(file, e => this.cache.delete(key));
96+
this.cache.set(key, items);
97+
resolve(key);
98+
}, () => resolve(this.none));
99+
}
100+
});
101+
}
102+
103+
fetchRemote(key: string): Thenable<string> {
104+
return new Promise(resolve => {
105+
if (this.cache.has(key)) {
106+
resolve(key);
107+
} else {
108+
fetch(key).then(res => {
109+
const items = new Map<string, CompletionItem>();
110+
111+
if (res.ok) {
112+
res.text().then(text => {
113+
this.parseTextToItems(text, items);
114+
this.cache.set(key, items);
115+
resolve(key);
116+
}, () => resolve(this.none));
117+
} else {
118+
this.cache.set(key, items);
119+
resolve(key);
120+
}
121+
}, () => resolve(this.none));
122+
}
123+
});
124+
}
125+
126+
findStyleSheets(uri: Uri): Thenable<Set<string>> {
127+
return new Promise(resolve => {
128+
const keys = new Set<string>();
129+
const styleSheets = this.getStyleSheets(uri);
130+
const promises = [];
131+
132+
for (const key of styleSheets) {
133+
promises.push(this.isRemote.test(key)
134+
? this.fetchRemote(key).then(k => keys.add(k))
135+
: this.fetchLocal(key, uri).then(k => keys.add(k)));
136+
}
137+
138+
Promise.all(promises).then(() => resolve(keys));
139+
});
140+
}
141+
142+
findDocumentLinks(uri: Uri, text: string): Thenable<Set<string>> {
143+
return new Promise(resolve => {
144+
const findLinks = /<link([^>]+)>/gi;
145+
const keys = new Set<string>();
146+
const promises = [];
147+
148+
let link;
149+
150+
while ((link = findLinks.exec(text)) !== null) {
151+
const rel = this.findLinkRel.exec(link[1]);
152+
153+
if (rel && rel[2] === "stylesheet") {
154+
const href = this.findLinkHref.exec(link[1]);
155+
156+
if (href) {
157+
const key = href[2];
158+
159+
promises.push(this.isRemote.test(key)
160+
? this.fetchRemote(key).then(k => keys.add(k))
161+
: this.fetchLocal(key, uri).then(k => keys.add(k)));
162+
}
163+
}
164+
}
165+
166+
Promise.all(promises).then(() => resolve(keys));
167+
});
168+
}
169+
170+
findDocumentStyles(uri: Uri, text: string): Thenable<Set<string>> {
171+
return new Promise(resolve => {
172+
const key = uri.toString();
173+
const keys = new Set<string>([key]);
174+
const items = new Map<string, CompletionItem>();
175+
const findStyles = /<style[^>]*>([^<]+)<\/style>/gi;
176+
177+
let style;
178+
179+
while ((style = findStyles.exec(text)) !== null) {
180+
this.parseTextToItems(style[1], items);
181+
}
182+
183+
this.cache.set(key, items);
184+
resolve(keys);
185+
});
186+
}
187+
188+
findExtendedStyles(uri: Uri, text: string): Thenable<Set<string>> {
189+
return new Promise(resolve => {
190+
const parent = this.findExtends.exec(text);
191+
192+
if (parent) {
193+
const path = uri.fsPath;
194+
const ext = extname(path);
195+
const key = join(dirname(path), basename(parent[1], ext) + ext);
196+
const extend = this.extends.get(key);
197+
198+
if (extend) {
199+
resolve(extend);
200+
} else {
201+
const file = Uri.file(key);
202+
203+
workspace.fs.readFile(file).then(content => {
204+
const text = content.toString();
205+
206+
Promise.all([
207+
this.findDocumentLinks(file, text),
208+
this.findDocumentStyles(file, text),
209+
this.findExtendedStyles(file, text)
210+
]).then(sets => {
211+
const keys = new Set<string>();
212+
213+
sets.forEach(set => set.forEach(k => keys.add(k)));
214+
this.watchFile(file, e => this.extends.delete(key));
215+
this.extends.set(key, keys);
216+
resolve(keys);
217+
});
218+
219+
}, () => resolve(this.empty));
220+
}
221+
} else {
222+
resolve(this.empty);
223+
}
224+
});
225+
}
226+
227+
buildItems(sets: Set<string>[], kind: CompletionItemKind): CompletionItem[] {
228+
const items = new Map<string, CompletionItem>();
229+
const keys = new Set<string>();
230+
231+
sets.forEach(v => v.forEach(v => keys.add(v)));
232+
233+
keys.forEach(k => this.cache.get(k)?.forEach((v, k) => {
234+
if (kind === v.kind) {
235+
items.set(k, v);
236+
}
237+
}));
238+
239+
return [...items.values()];
240+
}
241+
242+
provideCompletionItems(
243+
document: TextDocument,
244+
position: Position,
245+
token: CancellationToken,
246+
context: CompletionContext): ProviderResult<CompletionItem[] | CompletionList<CompletionItem>> {
247+
248+
return new Promise((resolve, reject) => {
249+
if (token.isCancellationRequested) {
250+
reject();
251+
} else {
252+
const range = new Range(this.start, position);
253+
const text = document.getText(range);
254+
const canComplete = this.canComplete.exec(text);
255+
256+
if (canComplete) {
257+
const type = canComplete[1] === "id"
258+
? CompletionItemKind.Value
259+
: CompletionItemKind.Enum;
260+
261+
const uri = document.uri;
262+
263+
Promise.all([
264+
this.findStyleSheets(uri),
265+
this.findDocumentLinks(uri, text),
266+
this.findDocumentStyles(uri, text),
267+
this.findExtendedStyles(uri, text)
268+
]).then(keys => resolve(this.buildItems(keys, type)));
269+
} else {
270+
reject();
271+
}
272+
}
273+
});
274+
}
275+
}

0 commit comments

Comments
 (0)