Skip to content

Commit 096ee69

Browse files
authored
(feat) global var completions (#522)
Add option to set css files which will be checked for global vars which will appear in autocompletion. The watched files could be anything, so it could also be svelte files with global vars in their style tag. Also added a change listener to the ConfigManager to get notified of config changes which removes the need to reload the language server after changes in this case. #521
1 parent ec47e72 commit 096ee69

File tree

5 files changed

+113
-2
lines changed

5 files changed

+113
-2
lines changed

packages/language-server/src/ls-config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const defaultLSConfig: LSConfig = {
1616
},
1717
css: {
1818
enable: true,
19+
globals: '',
1920
diagnostics: { enable: true },
2021
hover: { enable: true },
2122
completions: { enable: true },
@@ -79,6 +80,7 @@ export interface LSTypescriptConfig {
7980

8081
export interface LSCSSConfig {
8182
enable: boolean;
83+
globals: string;
8284
diagnostics: {
8385
enable: boolean;
8486
};
@@ -145,6 +147,7 @@ type DeepPartial<T> = T extends CompilerWarningsSettings
145147

146148
export class LSConfigManager {
147149
private config: LSConfig = defaultLSConfig;
150+
private listeners: ((config: LSConfigManager) => void)[] = [];
148151

149152
/**
150153
* Updates config.
@@ -159,6 +162,8 @@ export class LSConfigManager {
159162
if (config.svelte?.compilerWarnings) {
160163
this.config.svelte.compilerWarnings = config.svelte.compilerWarnings;
161164
}
165+
166+
this.listeners.forEach((listener) => listener(this));
162167
}
163168

164169
/**
@@ -183,6 +188,13 @@ export class LSConfigManager {
183188
getConfig(): Readonly<LSConfig> {
184189
return this.config;
185190
}
191+
192+
/**
193+
* Register a listener which is invoked when the config changed.
194+
*/
195+
onChange(callback: (config: LSConfigManager) => void): void {
196+
this.listeners.push(callback);
197+
}
186198
}
187199

188200
export const lsConfig = new LSConfigManager();

packages/language-server/src/plugins/css/CSSPlugin.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
Position,
1212
Range,
1313
SymbolInformation,
14+
CompletionItem,
15+
CompletionItemKind,
1416
} from 'vscode-languageserver';
1517
import {
1618
Document,
@@ -33,6 +35,7 @@ import {
3335
} from '../interfaces';
3436
import { CSSDocument } from './CSSDocument';
3537
import { getLanguage, getLanguageService } from './service';
38+
import { GlobalVars } from './global-vars';
3639

3740
export class CSSPlugin
3841
implements
@@ -45,10 +48,16 @@ export class CSSPlugin
4548
private configManager: LSConfigManager;
4649
private cssDocuments = new WeakMap<Document, CSSDocument>();
4750
private triggerCharacters = ['.', ':', '-', '/'];
51+
private globalVars = new GlobalVars();
4852

4953
constructor(docManager: DocumentManager, configManager: LSConfigManager) {
5054
this.configManager = configManager;
5155

56+
this.globalVars.watchFiles(this.configManager.get('css.globals'));
57+
this.configManager.onChange((config) =>
58+
this.globalVars.watchFiles(config.get('css.globals')),
59+
);
60+
5261
docManager.on('documentChange', (document) =>
5362
this.cssDocuments.set(document, new CSSDocument(document)),
5463
);
@@ -149,14 +158,37 @@ export class CSSPlugin
149158
cssDocument.stylesheet,
150159
);
151160
return CompletionList.create(
152-
[...(results ? results.items : []), ...emmetResults.items].map((completionItem) =>
153-
mapCompletionItemToOriginal(cssDocument, completionItem),
161+
this.appendGlobalVars(
162+
[...(results ? results.items : []), ...emmetResults.items].map((completionItem) =>
163+
mapCompletionItemToOriginal(cssDocument, completionItem),
164+
),
154165
),
155166
// Emmet completions change on every keystroke, so they are never complete
156167
emmetResults.items.length > 0,
157168
);
158169
}
159170

171+
private appendGlobalVars(items: CompletionItem[]): CompletionItem[] {
172+
// Finding one value with that item kind means we are in a value completion scenario
173+
const value = items.find((item) => item.kind === CompletionItemKind.Value);
174+
if (!value) {
175+
return items;
176+
}
177+
178+
const additionalItems: CompletionItem[] = this.globalVars
179+
.getGlobalVars()
180+
.map((globalVar) => ({
181+
label: globalVar.name,
182+
detail: `${globalVar.filename}\n\n${globalVar.name}: ${globalVar.value}`,
183+
textEdit: value.textEdit && {
184+
...value.textEdit,
185+
newText: `var(${globalVar.name})`,
186+
},
187+
kind: CompletionItemKind.Value,
188+
}));
189+
return [...items, ...additionalItems];
190+
}
191+
160192
getDocumentColors(document: Document): ColorInformation[] {
161193
if (!this.featureEnabled('documentColors')) {
162194
return [];
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { watch, FSWatcher } from 'chokidar';
2+
import { readFile } from 'fs';
3+
import { isNotNullOrUndefined, flatten } from '../../utils';
4+
5+
const varRegex = /^\s*(--\w+.*?):\s*?([^;]*)/;
6+
7+
export interface GlobalVar {
8+
name: string;
9+
filename: string;
10+
value: string;
11+
}
12+
13+
export class GlobalVars {
14+
private fsWatcher?: FSWatcher;
15+
private globalVars = new Map<string, GlobalVar[]>();
16+
17+
watchFiles(filesToWatch: string): void {
18+
if (!filesToWatch) {
19+
return;
20+
}
21+
22+
if (this.fsWatcher) {
23+
this.fsWatcher.close();
24+
this.globalVars.clear();
25+
}
26+
27+
this.fsWatcher = watch(filesToWatch.split(','))
28+
.addListener('add', (file) => this.updateForFile(file))
29+
.addListener('change', (file) => {
30+
this.updateForFile(file);
31+
})
32+
.addListener('unlink', (file) => this.globalVars.delete(file));
33+
}
34+
35+
private updateForFile(filename: string) {
36+
// Inside a small timeout because it seems chikidar is "too fast"
37+
// and reading the file will then return empty content
38+
setTimeout(() => {
39+
readFile(filename, 'utf-8', (error, contents) => {
40+
if (error) {
41+
return;
42+
}
43+
44+
const globalVarsForFile = contents
45+
.split('\n')
46+
.map((line) => line.match(varRegex))
47+
.filter(isNotNullOrUndefined)
48+
.map((line) => ({ filename, name: line[1], value: line[2] }));
49+
this.globalVars.set(filename, globalVarsForFile);
50+
});
51+
}, 1000);
52+
}
53+
54+
getGlobalVars(): GlobalVar[] {
55+
return flatten([...this.globalVars.values()]);
56+
}
57+
}

packages/svelte-vscode/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ Enable code actions for TypeScript. _Default_: `true`
9898

9999
Enable the CSS plugin. _Default_: `true`
100100

101+
##### `svelte.plugin.css.globals`
102+
103+
Which css files should be checked for global variables (`--global-var: value;`). These variables are added to the css completions. String of comma-separated file paths or globs relative to workspace root.
104+
101105
##### `svelte.plugin.css.diagnostics`
102106

103107
Enable diagnostic messages for CSS. _Default_: `true`

packages/svelte-vscode/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@
110110
"title": "CSS",
111111
"description": "Enable the CSS plugin"
112112
},
113+
"svelte.plugin.css.globals": {
114+
"type": "string",
115+
"default": "",
116+
"title": "CSS: Global Files",
117+
"description": "Which css files should be checked for global variables (`--global-var: value;`). These variables are added to the css completions. String of comma-separated file paths or globs relative to workspace root."
118+
},
113119
"svelte.plugin.css.diagnostics.enable": {
114120
"type": "boolean",
115121
"default": true,

0 commit comments

Comments
 (0)