Add incremental sync to LSPServer with buffer patching
MODIFY
packages/@romefrontend/core/common/bridges/WorkerBridge.ts
packages/@romefrontend/core/common/bridges/WorkerBridge.ts
@@ -32,6 +32,7 @@ import {AnalyzeDependencyResult} from "../types/analyzeDependencies";
import {InlineSnapshotUpdates} from "@romefrontend/core/test-worker/SnapshotManager";
import {FileNotFound} from "@romefrontend/core/common/FileNotFound";
import {createAbsoluteFilePath} from "@romefrontend/path";
+import {Number0} from "@romefrontend/ob1";
export type WorkerProjects = Array<{
id: number;
@@ -123,6 +124,19 @@ export type WorkerLintResult = {
suppressions: DiagnosticSuppressions;
};
+export type WorkerBufferPosition = {
+ line: Number0;
+ character: Number0;
+};
+
+export type WorkerBufferPatch = {
+ range: {
+ start: WorkerBufferPosition;
+ end: WorkerBufferPosition;
+ };
+ text: string;
+};
+
export default class WorkerBridge extends Bridge {
log = this.createEvent<string, void>({
name: "log",
@@ -265,6 +279,17 @@ export default class WorkerBridge extends Bridge {
direction: "server->client",
});
+ patchBuffer = this.createEvent<
+ {
+ file: JSONFileReference;
+ patches: Array<WorkerBufferPatch>;
+ },
+ string
+ >({
+ name: "patchBuffer",
+ direction: "server->client",
+ });
+
clearBuffer = this.createEvent<
{
file: JSONFileReference;

MODIFY
packages/@romefrontend/core/server/ServerRequest.ts
packages/@romefrontend/core/server/ServerRequest.ts
@@ -58,6 +58,7 @@ import {TransformStageName} from "@romefrontend/compiler";
import WorkerBridge, {
PrefetchedModuleSignatures,
WorkerAnalyzeDependencyResult,
+ WorkerBufferPatch,
WorkerCompileResult,
WorkerCompilerOptions,
WorkerFormatResult,
@@ -919,6 +920,25 @@ export default class ServerRequest {
);
}
+ async requestWorkerPatchBuffer(
+ path: AbsoluteFilePath,
+ patches: Array<WorkerBufferPatch>,
+ ): Promise<string> {
+ this.checkCancelled();
+
+ return this.wrapRequestDiagnostic(
+ "patchBuffer",
+ path,
+ async (bridge, file) => {
+ const buffer = await bridge.patchBuffer.call({file, patches});
+ this.server.memoryFs.addBuffer(path, buffer);
+ this.server.refreshFileEvent.send(path);
+ return buffer;
+ },
+ {noRetry: true},
+ );
+ }
+
async requestWorkerClearBuffer(path: AbsoluteFilePath): Promise<void> {
this.checkCancelled();

MODIFY
packages/@romefrontend/core/server/lsp/LSPServer.ts
packages/@romefrontend/core/server/lsp/LSPServer.ts
@@ -32,6 +32,7 @@ import {
convertDiagnosticsToLSP,
diffTextEdits,
getPathFromTextDocument,
+ getWorkerBufferPatches,
} from "./utils";
export default class LSPServer {
@@ -129,7 +130,7 @@ export default class LSPServer {
return new LSPProgress(this.transport, this.request.reporter, opts);
}
- async watchProject(path: AbsoluteFilePath) {
+ async initProject(path: AbsoluteFilePath) {
if (this.lintSessions.has(path) || this.lintSessionsPending.has(path)) {
return;
}
@@ -147,6 +148,11 @@ export default class LSPServer {
const req = this.createFakeServerRequest("lsp_project", [path.join()]);
await req.init();
+ // This is not awaited so it doesn't delay the initialize response
+ this.watchProject(path, req);
+ }
+
+ async watchProject(path: AbsoluteFilePath, req: ServerRequest) {
const linter = new Linter(
req,
{
@@ -234,13 +240,13 @@ export default class LSPServer {
case "initialize": {
const rootUri = params.get("rootUri");
if (rootUri.exists()) {
- this.watchProject(createAbsoluteFilePath(rootUri.asString()));
+ await this.initProject(createAbsoluteFilePath(rootUri.asString()));
}
const workspaceFolders = params.get("workspaceFolders");
if (workspaceFolders.exists()) {
for (const elem of workspaceFolders.asArray()) {
- this.watchProject(getPathFromTextDocument(elem));
+ await this.initProject(getPathFromTextDocument(elem));
}
}
@@ -248,8 +254,8 @@ export default class LSPServer {
capabilities: {
textDocumentSync: {
openClose: true,
- // This sends over the full text on change. We should make this incremental later
- change: 1,
+ // This sends over incremental patches on change
+ change: 2,
},
documentFormattingProvider: true,
workspaceFolders: {
@@ -299,7 +305,7 @@ export default class LSPServer {
switch (method) {
case "workspace/didChangeWorkspaceFolders": {
for (const elem of params.get("added").asArray()) {
- this.watchProject(getPathFromTextDocument(elem));
+ await this.initProject(getPathFromTextDocument(elem));
}
for (const elem of params.get("removed").asArray()) {
this.unwatchProject(getPathFromTextDocument(elem));
@@ -307,18 +313,34 @@ export default class LSPServer {
break;
}
- case "textDocument/didChange": {
+ case "textDocument/didOpen": {
const path = getPathFromTextDocument(params.get("textDocument"));
- const content = params.get("contentChanges").asArray()[0].get("text").asString();
+ const content = params.get("textDocument").get("text").asString();
await this.request.requestWorkerUpdateBuffer(path, content);
this.fileBuffers.add(path);
+ this.logMessage(path, `Opened: ${path.join()}`);
+ break;
+ }
+
+ case "textDocument/didChange": {
+ const path = getPathFromTextDocument(params.get("textDocument"));
+ const contentChanges = params.get("contentChanges");
+
+ if (contentChanges.asArray()[0].has("range")) {
+ const patches = getWorkerBufferPatches(contentChanges);
+ await this.request.requestWorkerPatchBuffer(path, patches);
+ } else {
+ const content = contentChanges.asArray()[0].get("text").asString();
+ await this.request.requestWorkerUpdateBuffer(path, content);
+ }
break;
}
- case "textDocument/didSave": {
+ case "textDocument/didClose": {
const path = getPathFromTextDocument(params.get("textDocument"));
this.fileBuffers.delete(path);
await this.request.requestWorkerClearBuffer(path);
+ this.logMessage(path, `Closed: ${path.join()}`);
break;
}
}

MODIFY
packages/@romefrontend/core/server/lsp/utils.ts
packages/@romefrontend/core/server/lsp/utils.ts
@@ -13,6 +13,7 @@ import {Number0, ob1Coerce1To0, ob1Inc, ob1Number0} from "@romefrontend/ob1";
import {Position} from "@romefrontend/parser-core";
import {DiagnosticLocation, Diagnostics} from "@romefrontend/diagnostics";
import {Server} from "@romefrontend/core";
+import {WorkerBufferPatch} from "@romefrontend/core/common/bridges/WorkerBridge";
export function convertPositionToLSP(pos: undefined | Position): LSPPosition {
if (pos === undefined) {
@@ -153,3 +154,26 @@ export function diffTextEdits(
return edits;
}
+
+export function getWorkerBufferPatches(
+ contentChanges: Consumer,
+): Array<WorkerBufferPatch> {
+ return contentChanges.asArray().map((change) => {
+ const start = change.get("range").get("start");
+ const end = change.get("range").get("end");
+
+ return {
+ text: change.get("text").asString(),
+ range: {
+ start: {
+ line: start.get("line").asZeroIndexedNumber(),
+ character: start.get("character").asZeroIndexedNumber(),
+ },
+ end: {
+ line: end.get("line").asZeroIndexedNumber(),
+ character: end.get("character").asZeroIndexedNumber(),
+ },
+ },
+ };
+ });
+}

MODIFY
packages/@romefrontend/core/worker/Worker.ts
packages/@romefrontend/core/worker/Worker.ts
import {ModuleSignature, TypeCheckProvider} from "@romefrontend/js-analysis";
import WorkerBridge, {
PrefetchedModuleSignatures,
+ WorkerBufferPatch,
WorkerParseOptions,
WorkerPartialManifest,
WorkerPartialManifests,
@@ -37,6 +38,7 @@ import {getFileHandlerAssert} from "../common/file-handlers/index";
import {TransformProjectDefinition} from "@romefrontend/compiler";
import WorkerAPI from "./WorkerAPI";
import {FileNotFound} from "../common/FileNotFound";
+import {applyWorkerBufferPatch} from "./utils/applyWorkerBufferPatch";
export type ParseResult = {
ast: AnyRoot;
@@ -229,6 +231,13 @@ export default class Worker {
);
});
+ bridge.patchBuffer.subscribe((payload) => {
+ return this.patchBuffer(
+ convertTransportFileReference(payload.file),
+ payload.patches,
+ );
+ });
+
bridge.clearBuffer.subscribe((payload) => {
return this.clearBuffer(convertTransportFileReference(payload.file));
});
@@ -257,6 +266,26 @@ export default class Worker {
);
}
+ patchBuffer(ref: FileReference, patches: Array<WorkerBufferPatch>) {
+ this.logger.info(`Patched ${ref.real.toMarkup()} buffer`);
+ let buffer = this.buffers.get(ref.real);
+ if (buffer === undefined) {
+ throw new Error(`Can't find buffer to patch for ${ref.real.join()}`);
+ }
+ // Patches must be applied sequentially
+ for (const patch of patches) {
+ buffer = applyWorkerBufferPatch(buffer, patch);
+
+ if (buffer === undefined) {
+ throw new Error(`Invalid patch for buffer of ${ref.real.join()}`);
+ }
+ }
+
+ this.buffers.set(ref.real, buffer);
+ this.evict(ref.real);
+ return buffer;
+ }
+
async getTypeCheckProvider(
projectId: number,
prefetchedModuleSignatures: PrefetchedModuleSignatures = {},

ADD
packages/@romefrontend/core/worker/utils/applyWorkerBufferPatch.ts
+import {Number0, ob1Get0, ob1Inc, ob1Number0} from "@romefrontend/ob1";
+import {WorkerBufferPatch} from "@romefrontend/core/common/bridges/WorkerBridge";
+
+export function applyWorkerBufferPatch(
+ original: string,
+ {range: {start, end}, text}: WorkerBufferPatch,
+) {
+ let currLine: Number0 = ob1Number0;
+ let currChar: Number0 = ob1Number0;
+ let cursor: Number0 = ob1Number0;
+
+ let buffer: string | undefined;
+
+ // Offset based on UTF-16 code units
+ while (ob1Get0(cursor) <= original.length) {
+ // Start position
+ if (currLine === start.line && currChar === start.character) {
+ // Include anything before the start of the patch range
+ const preText = original.slice(0, ob1Get0(cursor));
+ buffer = preText + text;
+ }
+
+ // End position
+ if (currLine === end.line && currChar === end.character) {
+ if (buffer === undefined) {
+ // Start position was not encountered
+ return undefined;
+ }
+ // Append anything after the end of the patch range
+ const postText = original.slice(ob1Get0(cursor));
+ buffer += postText;
+ return buffer;
+ }
+
+ switch (original[ob1Get0(cursor)]) {
+ case "\n": {
+ currLine = ob1Inc(currLine);
+ currChar = ob1Number0;
+ break;
+ }
+ case "\r": {
+ currLine = ob1Inc(currLine);
+ currChar = ob1Number0;
+
+ // \r\n should only advance 1 line
+ if (original[ob1Get0(cursor) + 1] === "\n") {
+ cursor = ob1Inc(cursor);
+ }
+ break;
+ }
+ default: {
+ currChar = ob1Inc(currChar);
+ }
+ }
+
+ cursor = ob1Inc(cursor);
+ }
+
+ // End position was not encountered
+ return undefined;
+}

Add incremental sync to LSPServer with buffer patching (#775)