fix(lsp): lint without unstable project-wide check watching
MODIFY
internal/core/server/commands/lsp.ts
internal/core/server/commands/lsp.ts
@@ -26,6 +26,7 @@ export default createServerCommand({
reporter.redirectOutToErr(true);
const lsp = new LSPServer(req);
+ lsp.init();
server.onLSPServer(req, lsp);
const {transport} = lsp;

MODIFY
internal/core/server/fs/FileAllocator.ts
internal/core/server/fs/FileAllocator.ts
@@ -164,12 +164,17 @@ export default class FileAllocator {
for (const event of events) {
const {path} = event;
+ // Workers handle cache eviction internally when updating buffers
+ if (event.type === "BUFFER_UPDATE") {
+ continue;
+ }
+
if (event.type === "DELETED") {
await this.handleDeleted(path);
continue;
}
- if (event.type === "DISK_UPDATE" || event.type === "BUFFER_UPDATE") {
+ if (event.type === "DISK_UPDATE") {
if (this.hasOwner(path)) {
// Get the worker
const workerId = this.getOwnerId(path);
@@ -180,7 +185,6 @@ export default class FileAllocator {
// Evict the file from cache
await this.evict(path, markup`file change`);
- if (event.type === "DISK_UPDATE") {
const {newStats, oldStats} = event;
// Verify that this file doesn't exceed any size limit
@@ -194,7 +198,6 @@ export default class FileAllocator {
}
workerManager.disown(workerId, oldStats);
workerManager.own(workerId, newStats);
- }
} else {
this.logger.info(
markup`No owner for eviction <emphasis>${path}</emphasis>`,
@@ -203,9 +206,10 @@ export default class FileAllocator {
}
}
- const paths = events.filter((event) => event.type !== "CREATED").map((event) =>
- event.path
- );
+ const paths = events.filter((event) =>
+ event.type === "DISK_UPDATE" || event.type === "DELETED"
+ ).map((event) => event.path);
+
if (
paths.length > 0 &&
(await this.server.projectManager.maybeEvictProjects(paths))

MODIFY
internal/core/server/lsp/LSPServer.ts
internal/core/server/lsp/LSPServer.ts
@@ -16,6 +16,7 @@ import {
DIAGNOSTIC_CATEGORIES,
Diagnostic,
DiagnosticsProcessor,
+ catchDiagnostics,
formatCategoryDescription,
} from "@internal/diagnostics";
import {
@@ -33,9 +34,11 @@ import {LSPTransport} from "./protocol";
import LSPProgress from "./LSPProgress";
import {convertDiagnosticsToLSP} from "./utils";
import {markup, readMarkup} from "@internal/markup";
-import {Event} from "@internal/events";
+import {Event, EventQueue} from "@internal/events";
import {CommandName} from "@internal/core/common/commands";
import {notificationHandlers, requestHandlers} from "./messages";
+import {TextDocumentSync} from "./types";
+import {promiseAllFrom} from "@internal/async";
type LSPProjectSession = {
request: ServerRequest;
@@ -54,6 +57,10 @@ export default class LSPServer {
this.fileVersions = new AbsoluteFilePathMap();
this.watchProjectEvent = new Event("LSPServer.watchProject");
+ this.textDocumentSyncEvent = new EventQueue(
+ "LSPServer.textDocumentSyncEvent",
+ {toDedupeKey: ({path}) => path.join()},
+ );
request.endEvent.subscribe(async () => {
await this.shutdown();
@@ -89,6 +96,60 @@ export default class LSPServer {
private projectSessions: AbsoluteFilePathMap<LSPProjectSession>;
public watchProjectEvent: Event<AbsoluteFilePath, void>;
+ public textDocumentSyncEvent: EventQueue<TextDocumentSync>;
+
+ public init() {
+ this.server.resources.add(
+ this.textDocumentSyncEvent.subscribe(async (events) => {
+ await promiseAllFrom(
+ events,
+ (event) => this.handleTextDocumentSyncEvent(event),
+ );
+ }),
+ );
+ }
+
+ private async handleTextDocumentSyncEvent({path, type}: TextDocumentSync) {
+ if (type === "DID_CLOSE") {
+ this.clearPublishedDiagnostics(path);
+ } else {
+ const diagnostics = await this.getLintDiagnostics(path);
+ if (diagnostics !== undefined) {
+ this.publishDiagnostics(path, diagnostics);
+ }
+ }
+ }
+
+ private async getLintDiagnostics(path: AbsoluteFilePath) {
+ const {value, diagnostics} = await catchDiagnostics(async () => {
+ return this.request.requestWorkerLint(
+ path,
+ {applySafeFixes: false, save: false},
+ );
+ });
+ if (value === undefined) {
+ this.logDiagnostics(path, diagnostics);
+ return;
+ }
+ this.diagnosticsProcessor.removePath(path);
+ this.diagnosticsProcessor.addSuppressions(value.suppressions);
+ this.diagnosticsProcessor.addDiagnostics(value.diagnostics);
+ return this.diagnosticsProcessor.getDiagnosticsForPath(path);
+ }
+
+ private publishDiagnostics(path: AbsoluteFilePath, diagnostics: Diagnostic[]) {
+ this.transport.write({
+ method: "textDocument/publishDiagnostics",
+ params: {
+ uri: `file://${path.join()}`,
+ diagnostics: convertDiagnosticsToLSP(diagnostics, this.server),
+ },
+ });
+ }
+
+ private clearPublishedDiagnostics(path: AbsoluteFilePath) {
+ this.publishDiagnostics(path, []);
+ }
private createDiagnosticsProcessor(): DiagnosticsProcessor {
// We want to filter pendingFixes because we'll autoformat the file on save if necessary and it's just noise

MODIFY
internal/core/server/lsp/messages/general.ts
internal/core/server/lsp/messages/general.ts
@@ -12,8 +12,10 @@ export const exit: LSPNotificationHandler = async () => {
await safeProcessExit(0);
};
-export const initialized: LSPNotificationHandler = async (lsp) => {
- await lsp.watchPendingProjects();
+export const initialized: LSPNotificationHandler = async () => {
+ // TODO: Re-enable project-wide check watching
+ // Disabled for stability
+ // await lsp.watchPendingProjects();
};
export const initialize: LSPRequestHandler = async (lsp, params) => {

MODIFY
internal/core/server/lsp/messages/textDocument.ts
internal/core/server/lsp/messages/textDocument.ts
@@ -106,6 +106,7 @@ export const didOpen: LSPNotificationHandler = async (lsp, params) => {
await lsp.request.requestWorkerUpdateBuffer(path, content);
lsp.fileBuffers.add(path);
lsp.logMessage(path, `Opened: ${path.join()}`);
+ lsp.textDocumentSyncEvent.push({path, type: "DID_OPEN"});
};
export const didChange: LSPNotificationHandler = async (lsp, params) => {
@@ -124,6 +125,7 @@ export const didChange: LSPNotificationHandler = async (lsp, params) => {
const content = contentChanges.getIndex(0).get("text").asString();
await lsp.request.requestWorkerUpdateBuffer(path, content);
}
+ lsp.textDocumentSyncEvent.push({path, type: "DID_CHANGE"});
};
export const didClose: LSPNotificationHandler = async (lsp, params) => {
@@ -135,4 +137,5 @@ export const didClose: LSPNotificationHandler = async (lsp, params) => {
lsp.fileVersions.delete(path);
await lsp.request.requestWorkerClearBuffer(path);
lsp.logMessage(path, `Closed: ${path.join()}`);
+ lsp.textDocumentSyncEvent.push({path, type: "DID_CLOSE"});
};

MODIFY
internal/core/server/lsp/types.ts
internal/core/server/lsp/types.ts
*/
import {JSONArray, JSONObject, JSONPropertyValue} from "@internal/codec-config";
+import {AbsoluteFilePath} from "@internal/path";
export type LSPRequestMessage = {
/**
@@ -381,3 +382,17 @@ export type LSPVersionedTextDocumentIdentifier = LSPTextDocumentIdentifier & {
*/
version: number | null;
};
+
+export type TextDocumentSync =
+ | {
+ path: AbsoluteFilePath;
+ type: "DID_OPEN";
+ }
+ | {
+ path: AbsoluteFilePath;
+ type: "DID_CHANGE";
+ }
+ | {
+ path: AbsoluteFilePath;
+ type: "DID_CLOSE";
+ };

fix(lsp): lint without unstable project-wide check watching (#1610)