Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: watch svelte files and project files outside workspace #2299

Merged
merged 11 commits into from
Jun 24, 2024
24 changes: 21 additions & 3 deletions packages/language-server/src/lib/FallbackWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { FSWatcher, watch } from 'chokidar';
import { debounce } from 'lodash';
import { join } from 'path';
import { DidChangeWatchedFilesParams, FileChangeType, FileEvent } from 'vscode-languageserver';
import {
DidChangeWatchedFilesParams,
FileChangeType,
FileEvent,
RelativePattern
} from 'vscode-languageserver';
import { pathToUrl } from '../utils';
import { fileURLToPath } from 'url';

type DidChangeHandler = (para: DidChangeWatchedFilesParams) => void;

Expand All @@ -14,10 +20,10 @@ export class FallbackWatcher {

private undeliveredFileEvents: FileEvent[] = [];

constructor(glob: string, workspacePaths: string[]) {
constructor(recursivePatterns: string, workspacePaths: string[]) {
const gitOrNodeModules = /\.git|node_modules/;
this.watcher = watch(
workspacePaths.map((workspacePath) => join(workspacePath, glob)),
workspacePaths.map((workspacePath) => join(workspacePath, recursivePatterns)),
{
ignored: (path: string) =>
gitOrNodeModules.test(path) &&
Expand Down Expand Up @@ -65,6 +71,18 @@ export class FallbackWatcher {
this.callbacks.push(callback);
}

watchDirectory(patterns: RelativePattern[]) {
for (const pattern of patterns) {
const basePath = fileURLToPath(
typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri
);
if (!basePath) {
continue;
}
this.watcher.add(join(basePath, pattern.pattern));
}
}

dispose() {
this.watcher.close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,16 @@ export class DocumentManager {
let document: Document;
if (this.documents.has(textDocument.uri)) {
document = this.documents.get(textDocument.uri)!;
document.openedByClient = openedByClient;
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
document.setText(textDocument.text);
} else {
document = this.createDocument(textDocument);
document.openedByClient = openedByClient;
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
this.documents.set(textDocument.uri, document);
this.notify('documentOpen', document);
}

this.notify('documentChange', document);
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
document.openedByClient = openedByClient;

return document;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dirname, join } from 'path';
import ts from 'typescript';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
import { RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver';
import { Document, DocumentManager } from '../../lib/documents';
import { LSConfigManager } from '../../ls-config';
import {
Expand All @@ -22,7 +22,7 @@ import {
import { createProjectService } from './serviceCache';
import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager';
import { isSubPath } from './utils';
import { FileMap } from '../../lib/documents/fileCollection';
import { FileMap, FileSet } from '../../lib/documents/fileCollection';

interface LSAndTSDocResolverOptions {
notifyExceedSizeLimit?: () => void;
Expand All @@ -39,6 +39,8 @@ interface LSAndTSDocResolverOptions {
onProjectReloaded?: () => void;
watch?: boolean;
tsSystem?: ts.System;
watchDirectory?: (patterns: RelativePattern[]) => void;
nonRecursiveWatchPattern?: string;
}

export class LSAndTSDocResolver {
Expand Down Expand Up @@ -94,7 +96,17 @@ export class LSAndTSDocResolver {
}
});

this.watchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames);
this.packageJsonWatchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames);
this.watchedDirectories = new FileSet(this.tsSystem.useCaseSensitiveFileNames);

// workspaceUris are already watched during initialization
for (const root of this.workspaceUris) {
const rootPath = urlToPath(root);
if (rootPath) {
this.watchedDirectories.add(rootPath);
}
}

this.lsDocumentContext = {
ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
createDocument: this.createDocument,
Expand All @@ -105,7 +117,11 @@ export class LSAndTSDocResolver {
onProjectReloaded: this.options?.onProjectReloaded,
watchTsConfig: !!this.options?.watch,
tsSystem: this.tsSystem,
projectService: projectService
projectService,
watchDirectory: this.options?.watchDirectory
? this.watchDirectory.bind(this)
: undefined,
nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern
};
}

Expand All @@ -131,9 +147,9 @@ export class LSAndTSDocResolver {
private getCanonicalFileName: GetCanonicalFileName;

private userPreferencesAccessor: { preferences: ts.UserPreferences };
private readonly watchers: FileMap<ts.FileWatcher>;

private readonly packageJsonWatchers: FileMap<ts.FileWatcher>;
private lsDocumentContext: LanguageServiceDocumentContext;
private readonly watchedDirectories: FileSet;

async getLSForPath(path: string) {
return (await this.getTSService(path)).getService();
Expand Down Expand Up @@ -209,15 +225,15 @@ export class LSAndTSDocResolver {
this.docManager.releaseDocument(uri);
}

async invalidateModuleCache(filePath: string) {
await forAllServices((service) => service.invalidateModuleCache(filePath));
async invalidateModuleCache(filePaths: string[]) {
await forAllServices((service) => service.invalidateModuleCache(filePaths));
}

/**
* Updates project files in all existing ts services
*/
async updateProjectFiles() {
await forAllServices((service) => service.updateProjectFiles());
async updateProjectFiles(watcherNewFiles: string[]) {
await forAllServices((service) => service.scheduleProjectFileUpdate(watcherNewFiles));
}

/**
Expand All @@ -227,6 +243,20 @@ export class LSAndTSDocResolver {
path: string,
changes?: TextDocumentContentChangeEvent[]
): Promise<void> {
await this.updateExistingFile(path, (service) => service.updateTsOrJsFile(path, changes));
}

async updateExistingSvelteFile(path: string): Promise<void> {
const newDocument = this.createDocument(path, this.tsSystem.readFile(path) ?? '');
await this.updateExistingFile(path, (service) => {
service.updateSnapshot(newDocument);
});
}

private async updateExistingFile(
path: string,
cb: (service: LanguageServiceContainer) => void
) {
path = normalizePath(path);
// Only update once because all snapshots are shared between
// services. Since we don't have a current version of TS/JS
Expand All @@ -235,7 +265,7 @@ export class LSAndTSDocResolver {
await forAllServices((service) => {
if (service.hasFile(path) && !didUpdate) {
didUpdate = true;
service.updateTsOrJsFile(path, changes);
cb(service);
}
});
}
Expand Down Expand Up @@ -290,8 +320,8 @@ export class LSAndTSDocResolver {
return {
...sys,
readFile: (path, encoding) => {
if (path.endsWith('package.json') && !this.watchers.has(path)) {
this.watchers.set(
if (path.endsWith('package.json') && !this.packageJsonWatchers.has(path)) {
this.packageJsonWatchers.set(
path,
watchFile(path, this.onPackageJsonWatchChange.bind(this), 3_000)
);
Expand All @@ -309,8 +339,8 @@ export class LSAndTSDocResolver {
const normalizedPath = projectService?.toPath(path);

if (onWatchChange === ts.FileWatcherEventKind.Deleted) {
this.watchers.get(path)?.close();
this.watchers.delete(path);
this.packageJsonWatchers.get(path)?.close();
this.packageJsonWatchers.delete(path);
packageJsonCache?.delete(normalizedPath);
} else {
packageJsonCache?.addOrUpdate(normalizedPath);
Expand Down Expand Up @@ -345,4 +375,20 @@ export class LSAndTSDocResolver {
this.globalSnapshotsManager.updateTsOrJsFile(snapshot.filePath);
});
}

private watchDirectory(patterns: RelativePattern[]) {
if (!this.options?.watchDirectory || patterns.length === 0) {
return;
}

for (const pattern of patterns) {
const uri = typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri;
for (const watched of this.watchedDirectories) {
if (isSubPath(watched, uri, this.getCanonicalFileName)) {
return;
}
}
}
this.options.watchDirectory(patterns);
}
}
59 changes: 52 additions & 7 deletions packages/language-server/src/plugins/typescript/SnapshotManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,35 +99,51 @@ export class SnapshotManager {

private readonly projectFileToOriginalCasing: Map<string, string>;
private getCanonicalFileName: GetCanonicalFileName;
private watchingCanonicalDirectories: Map<string, ts.WatchDirectoryFlags> | undefined;

private readonly watchExtensions = [
ts.Extension.Dts,
ts.Extension.Dcts,
ts.Extension.Dmts,
ts.Extension.Js,
ts.Extension.Cjs,
ts.Extension.Mjs,
ts.Extension.Jsx,
ts.Extension.Ts,
ts.Extension.Mts,
ts.Extension.Cts,
ts.Extension.Tsx,
ts.Extension.Json
ts.Extension.Json,
'.svelte'
];

constructor(
private globalSnapshotsManager: GlobalSnapshotsManager,
private fileSpec: TsFilesSpec,
private workspaceRoot: string,
private tsSystem: ts.System,
projectFiles: string[],
useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames
wildcardDirectories: ts.MapLike<ts.WatchDirectoryFlags> | undefined
) {
this.onSnapshotChange = this.onSnapshotChange.bind(this);
this.globalSnapshotsManager.onChange(this.onSnapshotChange);
this.documents = new FileMap(useCaseSensitiveFileNames);
this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames);
this.projectFileToOriginalCasing = new Map();
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames);

projectFiles.forEach((originalCasing) =>
this.projectFileToOriginalCasing.set(
this.getCanonicalFileName(originalCasing),
originalCasing
)
);

this.watchingCanonicalDirectories = new Map(
Object.entries(wildcardDirectories ?? {}).map(([dir, flags]) => [
this.getCanonicalFileName(dir),
flags
])
);
}

private onSnapshotChange(fileName: string, document: DocumentSnapshot | undefined) {
Expand All @@ -144,16 +160,45 @@ export class SnapshotManager {
}
}

updateProjectFiles(): void {
const { include, exclude } = this.fileSpec;
areIgnoredFromNewFileWatch(watcherNewFiles: string[]): boolean {
const { include } = this.fileSpec;

// Since we default to not include anything,
// just don't waste time on this
if (include?.length === 0 || !this.watchingCanonicalDirectories) {
return true;
}

for (const newFile of watcherNewFiles) {
const path = this.getCanonicalFileName(normalizePath(newFile));
if (this.projectFileToOriginalCasing.has(path)) {
continue;
}

for (const [dir, flags] of this.watchingCanonicalDirectories) {
if (path.startsWith(dir)) {
if (!(flags & ts.WatchDirectoryFlags.Recursive)) {
const relative = path.slice(dir.length);
if (relative.includes('/')) {
continue;
}
}
return false;
}
}
}

return true;
}

updateProjectFiles(): void {
const { include, exclude } = this.fileSpec;

if (include?.length === 0) {
return;
}

const projectFiles = ts.sys
const projectFiles = this.tsSystem
.readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include)
.map(normalizePath);

Expand Down
Loading