Skip to content

Commit dbfb47a

Browse files
authored
fix: watch svelte files and project files outside workspace (#2299)
#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.
1 parent bdfa37a commit dbfb47a

14 files changed

+342
-101
lines changed

packages/language-server/src/lib/FallbackWatcher.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { FSWatcher, watch } from 'chokidar';
22
import { debounce } from 'lodash';
33
import { join } from 'path';
4-
import { DidChangeWatchedFilesParams, FileChangeType, FileEvent } from 'vscode-languageserver';
4+
import {
5+
DidChangeWatchedFilesParams,
6+
FileChangeType,
7+
FileEvent,
8+
RelativePattern
9+
} from 'vscode-languageserver';
510
import { pathToUrl } from '../utils';
11+
import { fileURLToPath } from 'url';
612

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

@@ -14,10 +20,10 @@ export class FallbackWatcher {
1420

1521
private undeliveredFileEvents: FileEvent[] = [];
1622

17-
constructor(glob: string, workspacePaths: string[]) {
23+
constructor(recursivePatterns: string, workspacePaths: string[]) {
1824
const gitOrNodeModules = /\.git|node_modules/;
1925
this.watcher = watch(
20-
workspacePaths.map((workspacePath) => join(workspacePath, glob)),
26+
workspacePaths.map((workspacePath) => join(workspacePath, recursivePatterns)),
2127
{
2228
ignored: (path: string) =>
2329
gitOrNodeModules.test(path) &&
@@ -65,6 +71,18 @@ export class FallbackWatcher {
6571
this.callbacks.push(callback);
6672
}
6773

74+
watchDirectory(patterns: RelativePattern[]) {
75+
for (const pattern of patterns) {
76+
const basePath = fileURLToPath(
77+
typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri
78+
);
79+
if (!basePath) {
80+
continue;
81+
}
82+
this.watcher.add(join(basePath, pattern.pattern));
83+
}
84+
}
85+
6886
dispose() {
6987
this.watcher.close();
7088
}

packages/language-server/src/lib/documents/DocumentManager.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,16 @@ export class DocumentManager {
4747
let document: Document;
4848
if (this.documents.has(textDocument.uri)) {
4949
document = this.documents.get(textDocument.uri)!;
50+
document.openedByClient = openedByClient;
5051
document.setText(textDocument.text);
5152
} else {
5253
document = this.createDocument(textDocument);
54+
document.openedByClient = openedByClient;
5355
this.documents.set(textDocument.uri, document);
5456
this.notify('documentOpen', document);
5557
}
5658

5759
this.notify('documentChange', document);
58-
document.openedByClient = openedByClient;
5960

6061
return document;
6162
}

packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts

+61-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { dirname, join } from 'path';
22
import ts from 'typescript';
3-
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
3+
import { RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver';
44
import { Document, DocumentManager } from '../../lib/documents';
55
import { LSConfigManager } from '../../ls-config';
66
import {
@@ -22,7 +22,7 @@ import {
2222
import { createProjectService } from './serviceCache';
2323
import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager';
2424
import { isSubPath } from './utils';
25-
import { FileMap } from '../../lib/documents/fileCollection';
25+
import { FileMap, FileSet } from '../../lib/documents/fileCollection';
2626

2727
interface LSAndTSDocResolverOptions {
2828
notifyExceedSizeLimit?: () => void;
@@ -39,6 +39,8 @@ interface LSAndTSDocResolverOptions {
3939
onProjectReloaded?: () => void;
4040
watch?: boolean;
4141
tsSystem?: ts.System;
42+
watchDirectory?: (patterns: RelativePattern[]) => void;
43+
nonRecursiveWatchPattern?: string;
4244
}
4345

4446
export class LSAndTSDocResolver {
@@ -94,7 +96,17 @@ export class LSAndTSDocResolver {
9496
}
9597
});
9698

97-
this.watchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames);
99+
this.packageJsonWatchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames);
100+
this.watchedDirectories = new FileSet(this.tsSystem.useCaseSensitiveFileNames);
101+
102+
// workspaceUris are already watched during initialization
103+
for (const root of this.workspaceUris) {
104+
const rootPath = urlToPath(root);
105+
if (rootPath) {
106+
this.watchedDirectories.add(rootPath);
107+
}
108+
}
109+
98110
this.lsDocumentContext = {
99111
ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
100112
createDocument: this.createDocument,
@@ -105,7 +117,11 @@ export class LSAndTSDocResolver {
105117
onProjectReloaded: this.options?.onProjectReloaded,
106118
watchTsConfig: !!this.options?.watch,
107119
tsSystem: this.tsSystem,
108-
projectService: projectService
120+
projectService,
121+
watchDirectory: this.options?.watchDirectory
122+
? this.watchDirectory.bind(this)
123+
: undefined,
124+
nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern
109125
};
110126
}
111127

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

133149
private userPreferencesAccessor: { preferences: ts.UserPreferences };
134-
private readonly watchers: FileMap<ts.FileWatcher>;
135-
150+
private readonly packageJsonWatchers: FileMap<ts.FileWatcher>;
136151
private lsDocumentContext: LanguageServiceDocumentContext;
152+
private readonly watchedDirectories: FileSet;
137153

138154
async getLSForPath(path: string) {
139155
return (await this.getTSService(path)).getService();
@@ -209,15 +225,15 @@ export class LSAndTSDocResolver {
209225
this.docManager.releaseDocument(uri);
210226
}
211227

212-
async invalidateModuleCache(filePath: string) {
213-
await forAllServices((service) => service.invalidateModuleCache(filePath));
228+
async invalidateModuleCache(filePaths: string[]) {
229+
await forAllServices((service) => service.invalidateModuleCache(filePaths));
214230
}
215231

216232
/**
217233
* Updates project files in all existing ts services
218234
*/
219-
async updateProjectFiles() {
220-
await forAllServices((service) => service.updateProjectFiles());
235+
async updateProjectFiles(watcherNewFiles: string[]) {
236+
await forAllServices((service) => service.scheduleProjectFileUpdate(watcherNewFiles));
221237
}
222238

223239
/**
@@ -227,6 +243,20 @@ export class LSAndTSDocResolver {
227243
path: string,
228244
changes?: TextDocumentContentChangeEvent[]
229245
): Promise<void> {
246+
await this.updateExistingFile(path, (service) => service.updateTsOrJsFile(path, changes));
247+
}
248+
249+
async updateExistingSvelteFile(path: string): Promise<void> {
250+
const newDocument = this.createDocument(path, this.tsSystem.readFile(path) ?? '');
251+
await this.updateExistingFile(path, (service) => {
252+
service.updateSnapshot(newDocument);
253+
});
254+
}
255+
256+
private async updateExistingFile(
257+
path: string,
258+
cb: (service: LanguageServiceContainer) => void
259+
) {
230260
path = normalizePath(path);
231261
// Only update once because all snapshots are shared between
232262
// services. Since we don't have a current version of TS/JS
@@ -235,7 +265,7 @@ export class LSAndTSDocResolver {
235265
await forAllServices((service) => {
236266
if (service.hasFile(path) && !didUpdate) {
237267
didUpdate = true;
238-
service.updateTsOrJsFile(path, changes);
268+
cb(service);
239269
}
240270
});
241271
}
@@ -290,8 +320,8 @@ export class LSAndTSDocResolver {
290320
return {
291321
...sys,
292322
readFile: (path, encoding) => {
293-
if (path.endsWith('package.json') && !this.watchers.has(path)) {
294-
this.watchers.set(
323+
if (path.endsWith('package.json') && !this.packageJsonWatchers.has(path)) {
324+
this.packageJsonWatchers.set(
295325
path,
296326
watchFile(path, this.onPackageJsonWatchChange.bind(this), 3_000)
297327
);
@@ -309,8 +339,8 @@ export class LSAndTSDocResolver {
309339
const normalizedPath = projectService?.toPath(path);
310340

311341
if (onWatchChange === ts.FileWatcherEventKind.Deleted) {
312-
this.watchers.get(path)?.close();
313-
this.watchers.delete(path);
342+
this.packageJsonWatchers.get(path)?.close();
343+
this.packageJsonWatchers.delete(path);
314344
packageJsonCache?.delete(normalizedPath);
315345
} else {
316346
packageJsonCache?.addOrUpdate(normalizedPath);
@@ -345,4 +375,20 @@ export class LSAndTSDocResolver {
345375
this.globalSnapshotsManager.updateTsOrJsFile(snapshot.filePath);
346376
});
347377
}
378+
379+
private watchDirectory(patterns: RelativePattern[]) {
380+
if (!this.options?.watchDirectory || patterns.length === 0) {
381+
return;
382+
}
383+
384+
for (const pattern of patterns) {
385+
const uri = typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri;
386+
for (const watched of this.watchedDirectories) {
387+
if (isSubPath(watched, uri, this.getCanonicalFileName)) {
388+
return;
389+
}
390+
}
391+
}
392+
this.options.watchDirectory(patterns);
393+
}
348394
}

packages/language-server/src/plugins/typescript/SnapshotManager.ts

+52-7
Original file line numberDiff line numberDiff line change
@@ -99,35 +99,51 @@ export class SnapshotManager {
9999

100100
private readonly projectFileToOriginalCasing: Map<string, string>;
101101
private getCanonicalFileName: GetCanonicalFileName;
102+
private watchingCanonicalDirectories: Map<string, ts.WatchDirectoryFlags> | undefined;
102103

103104
private readonly watchExtensions = [
104105
ts.Extension.Dts,
106+
ts.Extension.Dcts,
107+
ts.Extension.Dmts,
105108
ts.Extension.Js,
109+
ts.Extension.Cjs,
110+
ts.Extension.Mjs,
106111
ts.Extension.Jsx,
107112
ts.Extension.Ts,
113+
ts.Extension.Mts,
114+
ts.Extension.Cts,
108115
ts.Extension.Tsx,
109-
ts.Extension.Json
116+
ts.Extension.Json,
117+
'.svelte'
110118
];
111119

112120
constructor(
113121
private globalSnapshotsManager: GlobalSnapshotsManager,
114122
private fileSpec: TsFilesSpec,
115123
private workspaceRoot: string,
124+
private tsSystem: ts.System,
116125
projectFiles: string[],
117-
useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames
126+
wildcardDirectories: ts.MapLike<ts.WatchDirectoryFlags> | undefined
118127
) {
119128
this.onSnapshotChange = this.onSnapshotChange.bind(this);
120129
this.globalSnapshotsManager.onChange(this.onSnapshotChange);
121-
this.documents = new FileMap(useCaseSensitiveFileNames);
130+
this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames);
122131
this.projectFileToOriginalCasing = new Map();
123-
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
132+
this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames);
124133

125134
projectFiles.forEach((originalCasing) =>
126135
this.projectFileToOriginalCasing.set(
127136
this.getCanonicalFileName(originalCasing),
128137
originalCasing
129138
)
130139
);
140+
141+
this.watchingCanonicalDirectories = new Map(
142+
Object.entries(wildcardDirectories ?? {}).map(([dir, flags]) => [
143+
this.getCanonicalFileName(dir),
144+
flags
145+
])
146+
);
131147
}
132148

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

147-
updateProjectFiles(): void {
148-
const { include, exclude } = this.fileSpec;
163+
areIgnoredFromNewFileWatch(watcherNewFiles: string[]): boolean {
164+
const { include } = this.fileSpec;
149165

150166
// Since we default to not include anything,
151167
// just don't waste time on this
168+
if (include?.length === 0 || !this.watchingCanonicalDirectories) {
169+
return true;
170+
}
171+
172+
for (const newFile of watcherNewFiles) {
173+
const path = this.getCanonicalFileName(normalizePath(newFile));
174+
if (this.projectFileToOriginalCasing.has(path)) {
175+
continue;
176+
}
177+
178+
for (const [dir, flags] of this.watchingCanonicalDirectories) {
179+
if (path.startsWith(dir)) {
180+
if (!(flags & ts.WatchDirectoryFlags.Recursive)) {
181+
const relative = path.slice(dir.length);
182+
if (relative.includes('/')) {
183+
continue;
184+
}
185+
}
186+
return false;
187+
}
188+
}
189+
}
190+
191+
return true;
192+
}
193+
194+
updateProjectFiles(): void {
195+
const { include, exclude } = this.fileSpec;
196+
152197
if (include?.length === 0) {
153198
return;
154199
}
155200

156-
const projectFiles = ts.sys
201+
const projectFiles = this.tsSystem
157202
.readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include)
158203
.map(normalizePath);
159204

0 commit comments

Comments
 (0)