Skip to content

Commit f36d85e

Browse files
committed
feat: cache heavy processing tasks and separate them into parser module
1 parent 789e60e commit f36d85e

File tree

7 files changed

+202
-167
lines changed

7 files changed

+202
-167
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@
118118
},
119119
"dependencies": {
120120
"fast-glob": "^3.2.5",
121-
"memoize-one": "^5.1.1",
122121
"polished": "^4.1.1",
123122
"postcss": "^8.2.8"
124123
},

src/constants.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CSSVarDeclarations } from "./main";
2+
13
export type SupportedExtensionNames =
24
| "css"
35
| "scss"
@@ -200,6 +202,20 @@ export const CSS3Colors = [
200202
"yellowgreen",
201203
];
202204

205+
export type CSSVarRecord = { [path: string]: CSSVarDeclarations[] };
206+
export const CACHE: {
207+
cssVars: CSSVarRecord;
208+
fileMetas: {
209+
[path: string]: {
210+
path: string;
211+
lastModified: number;
212+
};
213+
};
214+
} = {
215+
cssVars: {},
216+
fileMetas: {},
217+
};
218+
203219
// export const FILTER_REGEX = /[\s:](--|var)\(?[\w-]*/;
204220
/**
205221
* For now I am not supporting `var` keyword,

src/extension.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
Range,
88
} from "vscode";
99
import { DEFAULT_CONFIG, FILTER_REGEX } from "./constants";
10-
import { createCompletionItems, parseFiles, setup } from "./main";
10+
import { createCompletionItems, setup } from "./main";
11+
import { parseFiles } from "./parser";
1112

1213
const restrictIntellisense = (text: string) => {
1314
return !FILTER_REGEX.test(text) || /^[\s\t]*-{1,2}\w?$/.test(text);

src/main.ts

Lines changed: 22 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,16 @@
11
import { CompletionItem, CompletionItemKind, workspace } from "vscode";
22
import { resolve } from "path";
3-
import { readFile, stat, existsSync } from "fs";
4-
import { promisify } from "util";
5-
import postcss, { Node } from "postcss";
63
import fastGlob from "fast-glob";
74
import { NoWorkspaceError } from "./errors";
85
import {
96
Config,
7+
CSSVarRecord,
108
DEFAULT_CONFIG,
119
EXTENSION_NAME,
1210
mapShortToFullExtension,
1311
SupportedExtensionNames,
1412
} from "./constants";
15-
import memoize from "memoize-one";
16-
import { getColor, getVariableDeclarations, isObjectProperty } from "./utils";
17-
18-
//#region Utilities
19-
const readFileAsync = promisify(readFile);
20-
const statAsync = promisify(stat);
21-
22-
type CSSVarRecord = { [path: string]: CSSVarDeclarations[] };
23-
const cache: {
24-
cssVars: CSSVarRecord;
25-
fileMetas: {
26-
[path: string]: {
27-
path: string;
28-
lastModified: number;
29-
};
30-
};
31-
} = {
32-
cssVars: {},
33-
fileMetas: {},
34-
};
35-
36-
//#endregion Utilities
13+
import { getCSSDeclarationArray, isObjectProperty } from "./utils";
3714

3815
/**
3916
* Sets up the Plugin
@@ -85,101 +62,27 @@ export interface CSSVarDeclarations {
8562
property: string;
8663
value: string;
8764
theme: string;
65+
color?: string;
8866
}
8967

90-
const cssParseAsync = (file: string) => {
91-
return postcss([]).process(file, {
92-
from: undefined,
93-
});
94-
};
95-
96-
/**
97-
* Parses a plain CSS file (even SCSS files, if they are pure CSS)
98-
* and retrives all the CSS variables present in all the selected
99-
* files. Parsing is done only once when plugin activates,
100-
* and everytime any file gets modified.
101-
*/
102-
export const parseFiles = async function (
103-
config: Config
104-
): Promise<CSSVarRecord> {
105-
//#region Remove Delete File Path variables
106-
const deletedPath = Object.keys(cache.cssVars).filter(
107-
path => !existsSync(path)
108-
);
109-
if (deletedPath.length > 0) {
110-
deletedPath.forEach(path => {
111-
delete cache.cssVars[path];
112-
delete cache.fileMetas[path];
113-
});
114-
}
115-
//#endregion
116-
let cssVars: CSSVarRecord = cache.cssVars;
117-
const isModified =
118-
Object.keys(cache.fileMetas).length !== config.files.length;
119-
for (const path of config.files) {
120-
const cachedFileMeta = cache.fileMetas[path];
121-
const meta = await statAsync(path);
122-
const lastModified = meta.mtimeMs;
123-
if (
124-
isModified ||
125-
!cachedFileMeta ||
126-
lastModified !== cachedFileMeta.lastModified
127-
) {
128-
// Read and Parse File, only when file has modified
129-
const file = await readFileAsync(path, { encoding: "utf8" });
130-
const css = await cssParseAsync(file);
131-
cssVars = {
132-
...cssVars,
133-
[path]: css.root.nodes.reduce<CSSVarDeclarations[]>(
134-
(declarations, node: Node) => {
135-
declarations = declarations.concat(
136-
getVariableDeclarations(config, node)
137-
);
138-
return declarations;
139-
},
140-
[]
141-
),
142-
};
68+
export const createCompletionItems = (
69+
cssVars: CSSVarRecord,
70+
predicate?: (cssVar: CSSVarDeclarations) => boolean
71+
) => {
72+
const vars = getCSSDeclarationArray(cssVars);
73+
return vars.reduce<CompletionItem[]>((items, cssVar) => {
74+
if (!predicate || predicate(cssVar)) {
75+
const KIND = cssVar.color
76+
? CompletionItemKind.Color
77+
: CompletionItemKind.Variable;
78+
const extra = cssVar.theme !== "" ? `\n\nTheme: [${cssVar.theme}]` : "";
79+
const propertyName = `${cssVar.property}`;
80+
const item = new CompletionItem(propertyName, KIND);
81+
item.detail = `Value: ${cssVar.value}${extra}`;
82+
item.documentation = cssVar.color || cssVar.value;
83+
item.insertText = `var(${cssVar.property});`;
84+
items.push(item);
14385
}
144-
if (!cachedFileMeta) {
145-
cache.fileMetas[path] = {
146-
path,
147-
lastModified,
148-
};
149-
} else {
150-
cache.fileMetas[path].lastModified = lastModified;
151-
}
152-
}
153-
154-
cache.cssVars = cssVars;
155-
return cache.cssVars;
86+
return items;
87+
}, []);
15688
};
157-
158-
export const createCompletionItems = memoize(
159-
(
160-
cssVars: CSSVarRecord,
161-
predicate?: (cssVar: CSSVarDeclarations) => boolean
162-
) => {
163-
const vars = Object.keys(cssVars).reduce(
164-
(acc, key) => acc.concat(cssVars[key]),
165-
[] as CSSVarDeclarations[]
166-
);
167-
return vars.reduce<CompletionItem[]>((items, cssVar) => {
168-
if (!predicate || predicate(cssVar)) {
169-
const color = getColor(cssVar.value, vars);
170-
const KIND = color.success
171-
? CompletionItemKind.Color
172-
: CompletionItemKind.Variable;
173-
const extra = cssVar.theme !== "" ? `\n\nTheme: [${cssVar.theme}]` : "";
174-
const propertyName = `${cssVar.property}`;
175-
const item = new CompletionItem(propertyName, KIND);
176-
item.detail = `Value: ${cssVar.value}${extra}`;
177-
item.documentation = color.color;
178-
item.insertText = `var(${cssVar.property});`;
179-
items.push(item);
180-
}
181-
return items;
182-
}, []);
183-
},
184-
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
185-
);

src/parser.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { readFile, existsSync, stat } from "fs";
2+
import postcss, { Declaration, Node, Rule } from "postcss";
3+
import { promisify } from "util";
4+
import {
5+
CACHE,
6+
CSS_VAR_REGEX,
7+
Config,
8+
CSSVarRecord,
9+
SUPPORTED_CSS_RULE_TYPES,
10+
} from "./constants";
11+
12+
import { CSSVarDeclarations } from "./main";
13+
import { getColor, getCSSDeclarationArray } from "./utils";
14+
15+
const readFileAsync = promisify(readFile);
16+
const statAsync = promisify(stat);
17+
18+
const cssParseAsync = (file: string) => {
19+
return postcss([]).process(file, {
20+
from: undefined,
21+
});
22+
};
23+
24+
/**
25+
* This is an impure function, to update CACHE
26+
* when any file is deleted.
27+
*/
28+
const updateCacheOnFileDelete = () => {
29+
const deletedPath = Object.keys(CACHE.cssVars).filter(
30+
path => !existsSync(path)
31+
);
32+
if (deletedPath.length > 0) {
33+
deletedPath.forEach(path => {
34+
delete CACHE.cssVars[path];
35+
delete CACHE.fileMetas[path];
36+
});
37+
}
38+
};
39+
40+
export const isNodeType = <T extends Node>(
41+
node: Node,
42+
type: string
43+
): node is T => {
44+
return !!node.type.match(type);
45+
};
46+
47+
/**
48+
* Get CSS Variable Declarations Array
49+
* from PostCSS AST of a CSS file.
50+
*/
51+
export function getVariableDeclarations(
52+
config: Config,
53+
node: Node,
54+
theme?: string | null
55+
): CSSVarDeclarations[] {
56+
let declarations: CSSVarDeclarations[] = [];
57+
if (
58+
isNodeType<Declaration>(node, SUPPORTED_CSS_RULE_TYPES[1]) &&
59+
CSS_VAR_REGEX.test(node.prop)
60+
) {
61+
declarations.push({
62+
property: node.prop,
63+
value: node.value,
64+
theme: theme || "",
65+
});
66+
} else if (isNodeType<Rule>(node, SUPPORTED_CSS_RULE_TYPES[0])) {
67+
const [theme] = config.themes.filter(theme => node.selector.match(theme));
68+
if (!config.excludeThemedVariables || !theme) {
69+
for (const _node of node.nodes) {
70+
const decls = getVariableDeclarations(config, _node, theme);
71+
declarations = declarations.concat(decls);
72+
}
73+
}
74+
}
75+
return declarations;
76+
}
77+
78+
/**
79+
* Parse a CSS file, and cache generated AST
80+
* into CSSVarDeclarations[].
81+
*/
82+
const parseFile = async function (path: string, config: Config) {
83+
const file = await readFileAsync(path, { encoding: "utf8" });
84+
const css = await cssParseAsync(file);
85+
return {
86+
[path]: css.root.nodes.reduce<CSSVarDeclarations[]>(
87+
(declarations, node: Node) => {
88+
declarations = declarations.concat(
89+
getVariableDeclarations(config, node)
90+
);
91+
return declarations;
92+
},
93+
[]
94+
),
95+
};
96+
};
97+
98+
/**
99+
* Parses a plain CSS file (even SCSS files, if they are pure CSS)
100+
* and retrives all the CSS variables present in all the selected
101+
* files. Parsing is done only once when plugin activates,
102+
* and everytime any file gets modified.
103+
*/
104+
export const parseFiles = async function (
105+
config: Config
106+
): Promise<CSSVarRecord> {
107+
updateCacheOnFileDelete();
108+
109+
let cssVars: CSSVarRecord = CACHE.cssVars;
110+
const isModified =
111+
Object.keys(CACHE.fileMetas).length !== config.files.length;
112+
113+
for (const path of config.files) {
114+
const cachedFileMeta = CACHE.fileMetas[path];
115+
const meta = await statAsync(path);
116+
const lastModified = meta.mtimeMs;
117+
if (
118+
isModified ||
119+
!cachedFileMeta ||
120+
lastModified !== cachedFileMeta.lastModified
121+
) {
122+
// Read and Parse File, only when file has modified
123+
cssVars = {
124+
...cssVars,
125+
...(await parseFile(path, config)),
126+
};
127+
}
128+
if (!cachedFileMeta) {
129+
CACHE.fileMetas[path] = {
130+
path,
131+
lastModified,
132+
};
133+
} else {
134+
CACHE.fileMetas[path].lastModified = lastModified;
135+
}
136+
}
137+
138+
if (CACHE.cssVars !== cssVars) {
139+
// Get Color for each, and modify the cssVar Record.
140+
const vars = getCSSDeclarationArray(cssVars);
141+
vars.forEach(cssVar => {
142+
const color = getColor(cssVar.value, vars);
143+
if (color.success) {
144+
cssVar.color = color.color;
145+
}
146+
});
147+
}
148+
149+
CACHE.cssVars = cssVars;
150+
return CACHE.cssVars;
151+
};

0 commit comments

Comments
 (0)