diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 2963ffe11..756faa539 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -40,11 +40,13 @@ }, "devDependencies": { "@types/estree": "^0.0.42", + "@types/globrex": "^0.1.4", "@types/lodash": "^4.14.116", "@types/mocha": "^9.1.0", "@types/node": "^18.0.0", "@types/sinon": "^7.5.2", "cross-env": "^7.0.2", + "globrex": "^0.1.2", "mocha": "^9.2.0", "sinon": "^11.0.0", "ts-node": "^10.0.0" diff --git a/packages/language-server/src/lib/FallbackWatcher.ts b/packages/language-server/src/lib/FallbackWatcher.ts index 4af7e71ff..52f63496f 100644 --- a/packages/language-server/src/lib/FallbackWatcher.ts +++ b/packages/language-server/src/lib/FallbackWatcher.ts @@ -9,6 +9,7 @@ import { } from 'vscode-languageserver'; import { pathToUrl } from '../utils'; import { fileURLToPath } from 'url'; +import { Stats } from 'fs'; type DidChangeHandler = (para: DidChangeWatchedFilesParams) => void; @@ -20,18 +21,20 @@ export class FallbackWatcher { private undeliveredFileEvents: FileEvent[] = []; - constructor(recursivePatterns: string, workspacePaths: string[]) { + constructor(watchExtensions: string[], workspacePaths: string[]) { const gitOrNodeModules = /\.git|node_modules/; - this.watcher = watch( - workspacePaths.map((workspacePath) => join(workspacePath, recursivePatterns)), - { - ignored: gitOrNodeModules, - // typescript would scan the project files on init. - // We only need to know what got updated. - ignoreInitial: true, - ignorePermissionErrors: true - } - ); + const ignoredExtensions = (fileName: string, stats?: Stats) => { + return ( + stats?.isFile() === true && !watchExtensions.some((ext) => fileName.endsWith(ext)) + ); + }; + this.watcher = watch(workspacePaths, { + ignored: [gitOrNodeModules, ignoredExtensions], + // typescript would scan the project files on init. + // We only need to know what got updated. + ignoreInitial: true, + ignorePermissionErrors: true + }); this.watcher .on('add', (path) => this.onFSEvent(path, FileChangeType.Created)) diff --git a/packages/language-server/src/plugins/css/CSSPlugin.ts b/packages/language-server/src/plugins/css/CSSPlugin.ts index 1147bef81..d5a38d427 100644 --- a/packages/language-server/src/plugins/css/CSSPlugin.ts +++ b/packages/language-server/src/plugins/css/CSSPlugin.ts @@ -50,6 +50,7 @@ import { StyleAttributeDocument } from './StyleAttributeDocument'; import { getDocumentContext } from '../documentContext'; import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; import { indentBasedFoldingRangeForTag } from '../../lib/foldingRange/indentFolding'; +import { isNotNullOrUndefined, urlToPath } from '../../utils'; export class CSSPlugin implements @@ -68,7 +69,7 @@ export class CSSPlugin private cssLanguageServices: CSSLanguageServices; private workspaceFolders: WorkspaceFolder[]; private triggerCharacters = ['.', ':', '-', '/']; - private globalVars = new GlobalVars(); + private globalVars: GlobalVars; constructor( docManager: DocumentManager, @@ -80,6 +81,10 @@ export class CSSPlugin this.workspaceFolders = workspaceFolders; this.configManager = configManager; this.updateConfigs(); + const workspacePaths = workspaceFolders + .map((folder) => urlToPath(folder.uri)) + .filter(isNotNullOrUndefined); + this.globalVars = new GlobalVars(workspacePaths); this.globalVars.watchFiles(this.configManager.get('css.globals')); this.configManager.onChange((config) => { diff --git a/packages/language-server/src/plugins/css/global-vars.ts b/packages/language-server/src/plugins/css/global-vars.ts index 2495b13d2..06c8a571a 100644 --- a/packages/language-server/src/plugins/css/global-vars.ts +++ b/packages/language-server/src/plugins/css/global-vars.ts @@ -1,6 +1,8 @@ -import { watch, FSWatcher } from 'chokidar'; +import { FSWatcher, watch } from 'chokidar'; import { readFile } from 'fs'; -import { isNotNullOrUndefined, flatten } from '../../utils'; +import globrex from 'globrex'; +import { join } from 'path'; +import { flatten, isNotNullOrUndefined, normalizePath } from '../../utils'; const varRegex = /^\s*(--\w+.*?):\s*?([^;]*)/; @@ -12,19 +14,46 @@ export interface GlobalVar { export class GlobalVars { private fsWatcher?: FSWatcher; + private watchedFiles: string | undefined; private globalVars = new Map(); + private readonly workspaceRoot: string[]; + + constructor(workspaceRoot: string[]) { + this.workspaceRoot = workspaceRoot; + } watchFiles(filesToWatch: string): void { - if (!filesToWatch) { + if (!filesToWatch || this.watchedFiles === filesToWatch) { return; } + this.watchedFiles = filesToWatch; if (this.fsWatcher) { this.fsWatcher.close(); this.globalVars.clear(); } - this.fsWatcher = watch(filesToWatch.split(',')) + const paths = new Set(); + const includePatterns = new Set(); + + for (const root of this.workspaceRoot) { + for (const filePath of filesToWatch.split(',')) { + if (!filePath.includes('*')) { + paths.add(filePath); + continue; + } + + const normalizedPath = normalizePath(join(root, filePath)); + includePatterns.add(normalizedPath); + const pathSegments = normalizedPath.split('**'); + let directory = pathSegments[0] || '.'; + paths.add(directory); + } + } + + this.fsWatcher = watch(Array.from(paths), { + ignored: this.createIgnoreMatcher(includePatterns) + }) .addListener('add', (file) => this.updateForFile(file)) .addListener('change', (file) => { this.updateForFile(file); @@ -32,6 +61,20 @@ export class GlobalVars { .addListener('unlink', (file) => this.globalVars.delete(file)); } + private createIgnoreMatcher(includePatterns: Set) { + if (includePatterns.size === 0) { + return undefined; + } + + const regexList = Array.from(includePatterns).map( + (pattern) => globrex(pattern, { globstar: true }).regex + ); + + return (path: string) => { + return !regexList.some((regex) => regex.test(path)); + }; + } + private updateForFile(filename: string) { // Inside a small timeout because it seems chikidar is "too fast" // and reading the file will then return empty content diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 026edff5d..9e7f4eeda 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -106,7 +106,9 @@ export function startServer(options?: LSOptions) { // Include Svelte files to better deal with scenarios such as switching git branches // where files that are not opened in the client could change - const nonRecursiveWatchPattern = '*.{ts,js,mts,mjs,cjs,cts,json,svelte}'; + const watchExtensions = ['.ts', '.js', '.mts', '.mjs', '.cjs', '.cts', '.json', '.svelte']; + const nonRecursiveWatchPattern = + '*.{' + watchExtensions.map((ext) => ext.slice(1)).join(',') + '}'; const recursiveWatchPattern = '**/' + nonRecursiveWatchPattern; connection.onInitialize((evt) => { @@ -120,7 +122,7 @@ export function startServer(options?: LSOptions) { if (!evt.capabilities.workspace?.didChangeWatchedFiles) { const workspacePaths = workspaceUris.map(urlToPath).filter(isNotNullOrUndefined); - watcher = new FallbackWatcher(recursiveWatchPattern, workspacePaths); + watcher = new FallbackWatcher(watchExtensions, workspacePaths); watcher.onDidChangeWatchedFiles(onDidChangeWatchedFiles); watchDirectory = (patterns) => { @@ -338,7 +340,7 @@ export function startServer(options?: LSOptions) { connection?.client.register(DidChangeWatchedFilesNotification.type, { watchers: [ { - // Editors have exlude configs, such as VSCode with `files.watcherExclude`, + // Editors have exclude configs, such as VSCode with `files.watcherExclude`, // which means it's safe to watch recursively here globPattern: recursiveWatchPattern } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7d19793f..e234ca0f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ importers: '@types/estree': specifier: ^0.0.42 version: 0.0.42 + '@types/globrex': + specifier: ^0.1.4 + version: 0.1.4 '@types/lodash': specifier: ^4.14.116 version: 4.14.194 @@ -97,6 +100,9 @@ importers: cross-env: specifier: ^7.0.2 version: 7.0.3 + globrex: + specifier: ^0.1.2 + version: 0.1.2 mocha: specifier: ^9.2.0 version: 9.2.2 @@ -473,6 +479,9 @@ packages: '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/globrex@0.1.4': + resolution: {integrity: sha512-qm82zaOxfn8Us/GGjNrQQ1XfCBUDV86DxQgIQq/p1zGHlt0xnbUiabNjN9rZUhMNvvIE2gg8iTW+GMfw0TnnLg==} + '@types/lodash@4.14.194': resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} @@ -1553,6 +1562,8 @@ snapshots: '@types/minimatch': 5.1.2 '@types/node': 18.19.46 + '@types/globrex@0.1.4': {} + '@types/lodash@4.14.194': {} '@types/minimatch@5.1.2': {}