Skip to content

Commit

Permalink
fix: watch svelte files and project files outside workspace (#2299)
Browse files Browse the repository at this point in the history
#2233 #2393

update project files(tsconfig.include) when a new client file is opened. So files included in both tsocnfig.json will be loaded into the respecting language service.
  • Loading branch information
jasonlyu123 committed Jun 24, 2024
1 parent bdfa37a commit dbfb47a
Show file tree
Hide file tree
Showing 14 changed files with 342 additions and 101 deletions.
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;
document.setText(textDocument.text);
} else {
document = this.createDocument(textDocument);
document.openedByClient = openedByClient;
this.documents.set(textDocument.uri, document);
this.notify('documentOpen', document);
}

this.notify('documentChange', document);
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

0 comments on commit dbfb47a

Please sign in to comment.