diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts index b08cdc9ec..d54f53977 100644 --- a/packages/language-server/src/ls-config.ts +++ b/packages/language-server/src/ls-config.ts @@ -16,6 +16,7 @@ const defaultLSConfig: LSConfig = { }, css: { enable: true, + globals: '', diagnostics: { enable: true }, hover: { enable: true }, completions: { enable: true }, @@ -79,6 +80,7 @@ export interface LSTypescriptConfig { export interface LSCSSConfig { enable: boolean; + globals: string; diagnostics: { enable: boolean; }; @@ -145,6 +147,7 @@ type DeepPartial = T extends CompilerWarningsSettings export class LSConfigManager { private config: LSConfig = defaultLSConfig; + private listeners: ((config: LSConfigManager) => void)[] = []; /** * Updates config. @@ -159,6 +162,8 @@ export class LSConfigManager { if (config.svelte?.compilerWarnings) { this.config.svelte.compilerWarnings = config.svelte.compilerWarnings; } + + this.listeners.forEach((listener) => listener(this)); } /** @@ -183,6 +188,13 @@ export class LSConfigManager { getConfig(): Readonly { return this.config; } + + /** + * Register a listener which is invoked when the config changed. + */ + onChange(callback: (config: LSConfigManager) => void): void { + this.listeners.push(callback); + } } export const lsConfig = new LSConfigManager(); diff --git a/packages/language-server/src/plugins/css/CSSPlugin.ts b/packages/language-server/src/plugins/css/CSSPlugin.ts index 4fb0fb22d..0fefed511 100644 --- a/packages/language-server/src/plugins/css/CSSPlugin.ts +++ b/packages/language-server/src/plugins/css/CSSPlugin.ts @@ -11,6 +11,8 @@ import { Position, Range, SymbolInformation, + CompletionItem, + CompletionItemKind, } from 'vscode-languageserver'; import { Document, @@ -33,6 +35,7 @@ import { } from '../interfaces'; import { CSSDocument } from './CSSDocument'; import { getLanguage, getLanguageService } from './service'; +import { GlobalVars } from './global-vars'; export class CSSPlugin implements @@ -45,10 +48,16 @@ export class CSSPlugin private configManager: LSConfigManager; private cssDocuments = new WeakMap(); private triggerCharacters = ['.', ':', '-', '/']; + private globalVars = new GlobalVars(); constructor(docManager: DocumentManager, configManager: LSConfigManager) { this.configManager = configManager; + this.globalVars.watchFiles(this.configManager.get('css.globals')); + this.configManager.onChange((config) => + this.globalVars.watchFiles(config.get('css.globals')), + ); + docManager.on('documentChange', (document) => this.cssDocuments.set(document, new CSSDocument(document)), ); @@ -149,14 +158,37 @@ export class CSSPlugin cssDocument.stylesheet, ); return CompletionList.create( - [...(results ? results.items : []), ...emmetResults.items].map((completionItem) => - mapCompletionItemToOriginal(cssDocument, completionItem), + this.appendGlobalVars( + [...(results ? results.items : []), ...emmetResults.items].map((completionItem) => + mapCompletionItemToOriginal(cssDocument, completionItem), + ), ), // Emmet completions change on every keystroke, so they are never complete emmetResults.items.length > 0, ); } + private appendGlobalVars(items: CompletionItem[]): CompletionItem[] { + // Finding one value with that item kind means we are in a value completion scenario + const value = items.find((item) => item.kind === CompletionItemKind.Value); + if (!value) { + return items; + } + + const additionalItems: CompletionItem[] = this.globalVars + .getGlobalVars() + .map((globalVar) => ({ + label: globalVar.name, + detail: `${globalVar.filename}\n\n${globalVar.name}: ${globalVar.value}`, + textEdit: value.textEdit && { + ...value.textEdit, + newText: `var(${globalVar.name})`, + }, + kind: CompletionItemKind.Value, + })); + return [...items, ...additionalItems]; + } + getDocumentColors(document: Document): ColorInformation[] { if (!this.featureEnabled('documentColors')) { return []; diff --git a/packages/language-server/src/plugins/css/global-vars.ts b/packages/language-server/src/plugins/css/global-vars.ts new file mode 100644 index 000000000..2495b13d2 --- /dev/null +++ b/packages/language-server/src/plugins/css/global-vars.ts @@ -0,0 +1,57 @@ +import { watch, FSWatcher } from 'chokidar'; +import { readFile } from 'fs'; +import { isNotNullOrUndefined, flatten } from '../../utils'; + +const varRegex = /^\s*(--\w+.*?):\s*?([^;]*)/; + +export interface GlobalVar { + name: string; + filename: string; + value: string; +} + +export class GlobalVars { + private fsWatcher?: FSWatcher; + private globalVars = new Map(); + + watchFiles(filesToWatch: string): void { + if (!filesToWatch) { + return; + } + + if (this.fsWatcher) { + this.fsWatcher.close(); + this.globalVars.clear(); + } + + this.fsWatcher = watch(filesToWatch.split(',')) + .addListener('add', (file) => this.updateForFile(file)) + .addListener('change', (file) => { + this.updateForFile(file); + }) + .addListener('unlink', (file) => this.globalVars.delete(file)); + } + + private updateForFile(filename: string) { + // Inside a small timeout because it seems chikidar is "too fast" + // and reading the file will then return empty content + setTimeout(() => { + readFile(filename, 'utf-8', (error, contents) => { + if (error) { + return; + } + + const globalVarsForFile = contents + .split('\n') + .map((line) => line.match(varRegex)) + .filter(isNotNullOrUndefined) + .map((line) => ({ filename, name: line[1], value: line[2] })); + this.globalVars.set(filename, globalVarsForFile); + }); + }, 1000); + } + + getGlobalVars(): GlobalVar[] { + return flatten([...this.globalVars.values()]); + } +} diff --git a/packages/svelte-vscode/README.md b/packages/svelte-vscode/README.md index bf83031e9..4b620c5e3 100644 --- a/packages/svelte-vscode/README.md +++ b/packages/svelte-vscode/README.md @@ -98,6 +98,10 @@ Enable code actions for TypeScript. _Default_: `true` Enable the CSS plugin. _Default_: `true` +##### `svelte.plugin.css.globals` + +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. + ##### `svelte.plugin.css.diagnostics` Enable diagnostic messages for CSS. _Default_: `true` diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 7739db56e..52d3102c1 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -110,6 +110,12 @@ "title": "CSS", "description": "Enable the CSS plugin" }, + "svelte.plugin.css.globals": { + "type": "string", + "default": "", + "title": "CSS: Global Files", + "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." + }, "svelte.plugin.css.diagnostics.enable": { "type": "boolean", "default": true,