Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-adults-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte-check': patch
---

fix: prevent file watcher issue
6 changes: 6 additions & 0 deletions .changeset/thirty-seas-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'svelte-language-server': patch
'svelte-check': patch
---

perf: check if file content changed in tsconfig file watch
29 changes: 26 additions & 3 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,11 @@ async function createLanguageService(
) {
if (
kind === ts.FileWatcherEventKind.Changed &&
!configFileModified(fileName, modifiedTime ?? tsSystem.getModifiedTime?.(fileName))
!configFileModified(
fileName,
modifiedTime ?? tsSystem.getModifiedTime?.(fileName),
docContext
)
) {
return;
}
Expand Down Expand Up @@ -1328,7 +1332,8 @@ function createWatchDependedConfigCallback(docContext: LanguageServiceDocumentCo
kind === ts.FileWatcherEventKind.Changed &&
!configFileModified(
fileName,
modifiedTime ?? docContext.tsSystem.getModifiedTime?.(fileName)
modifiedTime ?? docContext.tsSystem.getModifiedTime?.(fileName),
docContext
)
) {
return;
Expand Down Expand Up @@ -1360,7 +1365,11 @@ function createWatchDependedConfigCallback(docContext: LanguageServiceDocumentCo
/**
* check if file content is modified instead of attributes changed
*/
function configFileModified(fileName: string, modifiedTime: Date | undefined) {
function configFileModified(
fileName: string,
modifiedTime: Date | undefined,
docContext: LanguageServiceDocumentContext
) {
const previousModifiedTime = configFileModifiedTime.get(fileName);
if (!modifiedTime || !previousModifiedTime) {
return true;
Expand All @@ -1371,6 +1380,20 @@ function configFileModified(fileName: string, modifiedTime: Date | undefined) {
}

configFileModifiedTime.set(fileName, modifiedTime);

const oldSourceFile =
parsedTsConfigInfo.get(fileName)?.parsedCommandLine?.options.configFile ??
docContext.extendedConfigCache.get(fileName)?.extendedResult;

if (
oldSourceFile &&
typeof oldSourceFile === 'object' &&
'kind' in oldSourceFile &&
typeof oldSourceFile.text === 'string' &&
oldSourceFile.text === docContext.tsSystem.readFile(fileName)
) {
return false;
}
return true;
}

Expand Down
54 changes: 28 additions & 26 deletions packages/svelte-check/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,17 @@ class DiagnosticsWatcher {
.on('unlink', (path) => this.removeDocument(path))
.on('change', (path) => this.updateDocument(path, false));

this.updateWatchedDirectories();
if (this.ignoreInitialAdd) {
getDiagnostics(this.workspaceUri, this.writer, this.svelteCheck);
}
this.updateWildcardWatcher().then(() => {
// ensuring the typescript program is built after wildcard watchers are added
// so that individual file watchers added from onFileSnapshotCreated
// run after the wildcard ones
if (this.ignoreInitialAdd) {
getDiagnostics(this.workspaceUri, this.writer, this.svelteCheck);
}
});
}

private isSubdir(candidate: string, parent: string) {
private isSubDir(candidate: string, parent: string) {
const c = path.resolve(candidate);
const p = path.resolve(parent);
return c === p || c.startsWith(p + path.sep);
Expand All @@ -210,37 +214,37 @@ class DiagnosticsWatcher {
const sorted = [...new Set(dirs.map((d) => path.resolve(d)))].sort();
const result: string[] = [];
for (const dir of sorted) {
if (!result.some((p) => this.isSubdir(dir, p))) {
if (!result.some((p) => this.isSubDir(dir, p))) {
result.push(dir);
}
}
return result;
}

addWatchDirectory(dir: string) {
if (!dir) return;
if (!dir) {
return;
}

// Skip if already covered by an existing watched directory
for (const existing of this.currentWatchedDirs) {
if (this.isSubdir(dir, existing)) {
if (this.isSubDir(dir, existing)) {
return;
}
}
// If new dir is a parent of existing ones, unwatch children
const toRemove: string[] = [];

// Don't remove existing watchers, chokidar `unwatch` ignores future events from that path instead of closing the watcher in some cases
for (const existing of this.currentWatchedDirs) {
if (this.isSubdir(existing, dir)) {
toRemove.push(existing);
if (this.isSubDir(existing, dir)) {
this.currentWatchedDirs.delete(existing);
}
}
if (toRemove.length) {
this.watcher.unwatch(toRemove);
for (const r of toRemove) this.currentWatchedDirs.delete(r);
}

this.watcher.add(dir);
this.currentWatchedDirs.add(dir);
}

private async updateWatchedDirectories() {
private async updateWildcardWatcher() {
const watchDirs = await this.svelteCheck.getWatchDirectories();
const desired = this.minimizeDirs(
(watchDirs?.map((d) => d.path) || [this.workspaceUri.fsPath]).map((p) =>
Expand All @@ -249,15 +253,13 @@ class DiagnosticsWatcher {
);

const current = new Set([...this.currentWatchedDirs].map((p) => path.resolve(p)));
const desiredSet = new Set(desired);

const toAdd = desired.filter((d) => !current.has(d));
const toRemove = [...current].filter((d) => !desiredSet.has(d));

if (toAdd.length) this.watcher.add(toAdd);
if (toRemove.length) this.watcher.unwatch(toRemove);
if (toAdd.length) {
this.watcher.add(toAdd);
}

this.currentWatchedDirs = new Set(desired);
this.currentWatchedDirs = new Set([...current, ...toAdd]);
}

private async updateDocument(path: string, isNew: boolean) {
Expand All @@ -280,9 +282,9 @@ class DiagnosticsWatcher {
this.scheduleDiagnostics();
}

updateWatchers() {
updateWildcardWatchers() {
clearTimeout(this.pendingWatcherUpdate);
this.pendingWatcherUpdate = setTimeout(() => this.updateWatchedDirectories(), 1000);
this.pendingWatcherUpdate = setTimeout(() => this.updateWildcardWatcher(), 1000);
}

scheduleDiagnostics() {
Expand Down Expand Up @@ -342,7 +344,7 @@ parseOptions(async (opts) => {
// Wire callbacks that can reference the watcher instance created below
let watcher: DiagnosticsWatcher;
svelteCheckOptions.onProjectReload = () => {
watcher.updateWatchers();
watcher.updateWildcardWatchers();
watcher.scheduleDiagnostics();
};
svelteCheckOptions.onFileSnapshotCreated = (filePath: string) => {
Expand Down