fix(watcher): derive fallback file patterns
This commit is contained in:
@@ -7,7 +7,7 @@ import * as net from "node:net";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import type { FileSystemWatcher } from "vscode-languageserver-protocol";
|
import type { FileSystemWatcher } from "vscode-languageserver-protocol";
|
||||||
import { LspClient } from "./client.ts";
|
import { LspClient } from "./client.ts";
|
||||||
import { findRoot, findServerById, pathToUri, uriToPath } from "./root.ts";
|
import { findRoot, findServerById, pathToUri } from "./root.ts";
|
||||||
import type { ServerConfig } from "./types.ts";
|
import type { ServerConfig } from "./types.ts";
|
||||||
import { WorkspaceWatcher, type FileEvent } from "./watcher.ts";
|
import { WorkspaceWatcher, type FileEvent } from "./watcher.ts";
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
|
const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
|
||||||
const WATCHER_READY_TIMEOUT_MS = 5000;
|
const WATCHER_READY_TIMEOUT_MS = 5000;
|
||||||
const FILE_CHANGE_DELETED = 3;
|
const FILE_CHANGE_DELETED = 3;
|
||||||
|
const WATCH_KIND_CREATE = 1;
|
||||||
|
const WATCH_KIND_CHANGE = 2;
|
||||||
const WATCH_KIND_DELETE = 4;
|
const WATCH_KIND_DELETE = 4;
|
||||||
|
|
||||||
// Client Entry - One LspClient per (server.id, rootDir), plus the bookkeeping
|
// Client Entry - One LspClient per (server.id, rootDir), plus the bookkeeping
|
||||||
@@ -43,6 +45,7 @@ interface ClientEntry {
|
|||||||
lastUsed: number;
|
lastUsed: number;
|
||||||
watcher: WorkspaceWatcher | null;
|
watcher: WorkspaceWatcher | null;
|
||||||
unsubscribeWatchers: (() => void) | null;
|
unsubscribeWatchers: (() => void) | null;
|
||||||
|
usesDerivedWatchers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = new Map<string, ClientEntry>();
|
const entries = new Map<string, ClientEntry>();
|
||||||
@@ -96,6 +99,7 @@ async function getOrCreateEntry(
|
|||||||
lastUsed: Date.now(),
|
lastUsed: Date.now(),
|
||||||
watcher: null,
|
watcher: null,
|
||||||
unsubscribeWatchers: null,
|
unsubscribeWatchers: null,
|
||||||
|
usesDerivedWatchers: false,
|
||||||
};
|
};
|
||||||
entries.set(key, entry);
|
entries.set(key, entry);
|
||||||
try {
|
try {
|
||||||
@@ -118,11 +122,23 @@ async function attachWatcher(entry: ClientEntry): Promise<void> {
|
|||||||
|
|
||||||
function watcherPatterns(entry: ClientEntry): FileSystemWatcher[] {
|
function watcherPatterns(entry: ClientEntry): FileSystemWatcher[] {
|
||||||
const registered = entry.client.getFileWatchers();
|
const registered = entry.client.getFileWatchers();
|
||||||
const openedDeletes = [...entry.opened.keys()].map((uri) => ({
|
entry.usesDerivedWatchers = registered.length === 0;
|
||||||
globPattern: uriToPath(uri).split(path.sep).join("/"),
|
return registered.length > 0 ? registered : derivedServerWatchers(entry.server);
|
||||||
kind: WATCH_KIND_DELETE,
|
}
|
||||||
}));
|
|
||||||
return [...registered, ...openedDeletes];
|
function derivedServerWatchers(server: ServerConfig): FileSystemWatcher[] {
|
||||||
|
const extensions = server.match
|
||||||
|
.map((m) => m.replace(/^\./, ""))
|
||||||
|
.filter((m) => /^[A-Za-z0-9_-]+$/.test(m));
|
||||||
|
if (extensions.length === 0) return [];
|
||||||
|
|
||||||
|
const globPattern = extensions.length === 1
|
||||||
|
? `**/*.${extensions[0]}`
|
||||||
|
: `**/*.{${extensions.join(",")}}`;
|
||||||
|
return [{
|
||||||
|
globPattern,
|
||||||
|
kind: WATCH_KIND_CREATE | WATCH_KIND_CHANGE | WATCH_KIND_DELETE,
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshWatcher(entry: ClientEntry): Promise<void> {
|
async function refreshWatcher(entry: ClientEntry): Promise<void> {
|
||||||
@@ -178,6 +194,9 @@ function forwardEvents(entry: ClientEntry, events: FileEvent[]): void {
|
|||||||
entry.client.sendNotification("workspace/didChangeWatchedFiles", {
|
entry.client.sendNotification("workspace/didChangeWatchedFiles", {
|
||||||
changes: events,
|
changes: events,
|
||||||
});
|
});
|
||||||
|
if (entry.usesDerivedWatchers) {
|
||||||
|
evict(entry, "derived watcher event");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(`watcher send failed`, entry.server.id, (err as Error).message);
|
log(`watcher send failed`, entry.server.id, (err as Error).message);
|
||||||
}
|
}
|
||||||
@@ -220,7 +239,6 @@ async function syncFile(
|
|||||||
if (prev === undefined) {
|
if (prev === undefined) {
|
||||||
entry.client.openDocument(filePath);
|
entry.client.openDocument(filePath);
|
||||||
entry.opened.set(uri, stat.mtimeMs);
|
entry.opened.set(uri, stat.mtimeMs);
|
||||||
await refreshWatcher(entry);
|
|
||||||
return { uri, changed: true };
|
return { uri, changed: true };
|
||||||
} else if (prev !== stat.mtimeMs) {
|
} else if (prev !== stat.mtimeMs) {
|
||||||
entry.client.notifyChange(filePath);
|
entry.client.notifyChange(filePath);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface DiagResult {
|
|||||||
[serverId: string]: { diagnostics?: { message: string }[] };
|
[serverId: string]: { diagnostics?: { message: string }[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("watcher: typescript handles deleted opened files", { skip: skip ?? undefined }, () => {
|
describe("watcher: typescript handles derived file patterns", { skip: skip ?? undefined }, () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
let mainFile: string;
|
let mainFile: string;
|
||||||
let helperFile: string;
|
let helperFile: string;
|
||||||
@@ -81,6 +81,35 @@ describe("watcher: typescript handles deleted opened files", { skip: skip ?? und
|
|||||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears missing module after an unopened imported file is created", async () => {
|
||||||
|
fs.rmSync(helperFile);
|
||||||
|
|
||||||
|
const missing = (await runCliJson(
|
||||||
|
[mainFile, "diagnostics", '{"timeoutMs":5000}'],
|
||||||
|
env,
|
||||||
|
)) as DiagResult;
|
||||||
|
assert.ok(
|
||||||
|
(missing["typescript-language-server"]?.diagnostics ?? []).some((d) =>
|
||||||
|
d.message.includes("Cannot find module './helper'")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(helperFile, "export function helper(): number {\n return 1;\n}\n");
|
||||||
|
|
||||||
|
const result = await pollUntil(
|
||||||
|
async () =>
|
||||||
|
(await runCliJson(
|
||||||
|
[mainFile, "diagnostics", '{"timeoutMs":3000}'],
|
||||||
|
env,
|
||||||
|
)) as DiagResult,
|
||||||
|
(r) => (r["typescript-language-server"]?.diagnostics ?? []).length === 0,
|
||||||
|
15000,
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(result["typescript-language-server"]?.diagnostics ?? [], []);
|
||||||
|
});
|
||||||
|
|
||||||
it("reports missing module after an opened imported file is deleted", async () => {
|
it("reports missing module after an opened imported file is deleted", async () => {
|
||||||
const initial = (await runCliJson(
|
const initial = (await runCliJson(
|
||||||
[mainFile, "diagnostics", '{"timeoutMs":5000}'],
|
[mainFile, "diagnostics", '{"timeoutMs":5000}'],
|
||||||
|
|||||||
Reference in New Issue
Block a user