Skip to content

Commit

Permalink
fix: migrate glob file watch for chokidar v4 (#2551)
Browse files Browse the repository at this point in the history
#2539

chokidar v4 removes all glob support. This migrates fallback watcher and CSS global var to use the ignored config
  • Loading branch information
jasonlyu123 authored Oct 31, 2024
1 parent 02847e0 commit 7c23767
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 19 deletions.
2 changes: 2 additions & 0 deletions packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 14 additions & 11 deletions packages/language-server/src/lib/FallbackWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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))
Expand Down
7 changes: 6 additions & 1 deletion packages/language-server/src/plugins/css/CSSPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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) => {
Expand Down
51 changes: 47 additions & 4 deletions packages/language-server/src/plugins/css/global-vars.ts
Original file line number Diff line number Diff line change
@@ -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*?([^;]*)/;

Expand All @@ -12,26 +14,67 @@ export interface GlobalVar {

export class GlobalVars {
private fsWatcher?: FSWatcher;
private watchedFiles: string | undefined;
private globalVars = new Map<string, GlobalVar[]>();
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<string>();
const includePatterns = new Set<string>();

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);
})
.addListener('unlink', (file) => this.globalVars.delete(file));
}

private createIgnoreMatcher(includePatterns: Set<string>) {
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
Expand Down
8 changes: 5 additions & 3 deletions packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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
}
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7c23767

Please sign in to comment.