Add codeAction autofix support for LSP Server
MODIFY
internal/core/common/bridges/WorkerBridge.ts
internal/core/common/bridges/WorkerBridge.ts
@@ -279,6 +279,16 @@ export default class WorkerBridge extends Bridge {
direction: "server->client",
});
+ public getBuffer = this.createEvent<
+ {
+ ref: JSONFileReference;
+ },
+ string | undefined
+ >({
+ name: "getBuffer",
+ direction: "server->client",
+ });
+
public updateBuffer = this.createEvent<
{
ref: JSONFileReference;

MODIFY
internal/core/server/ServerRequest.ts
internal/core/server/ServerRequest.ts
@@ -865,6 +865,20 @@ export default class ServerRequest {
}
}
+ public async requestWorkerGetBuffer(
+ path: AbsoluteFilePath,
+ ): Promise<string | undefined> {
+ this.checkCancelled();
+
+ return this.wrapRequestDiagnostic(
+ "getBuffer",
+ path,
+ async (bridge, ref) => {
+ return bridge.getBuffer.call({ref});
+ },
+ );
+ }
+
public async requestWorkerUpdateBuffer(
path: AbsoluteFilePath,
content: string,

ADD
internal/core/server/lsp/LSPServer.test.md
+# `LSPServer.test.ts`
+
+**DO NOT MODIFY**. This file has been autogenerated. Run `rome test internal/core/server/lsp/LSPServer.test.ts --update-snapshots` to update.
+
+## `LSPServer`
+
+### `code actions`
+
+```javascript
+Array [
+ "Apply fix: lint/js/preferBlockStatements"
+ "Add suppression comment: lint/js/preferBlockStatements"
+ "Apply suggested fix: lint/js/doubleEquals"
+ "Add suppression comment: lint/js/doubleEquals"
+ "Rome: Fix All"
+]
+```
+
+### `console`
+
+```
+
+```
+
+### `edits`
+
+```javascript
+Array [
+ Object {
+ newText: "fun"
+ range: Object {
+ end: Object {
+ character: 0
+ line: 0
+ }
+ start: Object {
+ character: 0
+ line: 0
+ }
+ }
+ }
+ Object {
+ newText: "ti"
+ range: Object {
+ end: Object {
+ character: 1
+ line: 0
+ }
+ start: Object {
+ character: 1
+ line: 0
+ }
+ }
+ }
+ Object {
+ newText: ""
+ range: Object {
+ end: Object {
+ character: 5
+ line: 0
+ }
+ start: Object {
+ character: 3
+ line: 0
+ }
+ }
+ }
+ Object {
+ newText: ""
+ range: Object {
+ end: Object {
+ character: 12
+ line: 0
+ }
+ start: Object {
+ character: 9
+ line: 0
+ }
+ }
+ }
+ Object {
+ newText: ""
+ range: Object {
+ end: Object {
+ character: 18
+ line: 0
+ }
+ start: Object {
+ character: 15
+ line: 0
+ }
+ }
+ }
+ Object {
+ newText: "\n"
+ range: Object {
+ end: Object {
+ character: 20
+ line: 0
+ }
+ start: Object {
+ character: 20
+ line: 0
+ }
+ }
+ }
+]
+```
+
+### `files`
+
+```
+# .config/rome.json
+{
+ "files": {
+ "vendorPath": "../remote"
+ }
+}
+
+
+# foo.ts
+let foo
+if (foo == 2) foo
+
+```
+
+### `formatting`
+
+```javascript
+Array [
+ Object {
+ newText: ";"
+ range: Object {
+ end: Object {
+ character: 7
+ line: 0
+ }
+ start: Object {
+ character: 7
+ line: 0
+ }
+ }
+ }
+ Object {
+ newText: ";\n"
+ range: Object {
+ end: Object {
+ character: 17
+ line: 1
+ }
+ start: Object {
+ character: 17
+ line: 1
+ }
+ }
+ }
+]
+```
+
+### `formatting after didOpen`
+
+```javascript
+Array [
+ Object {
+ newText: ";\n"
+ range: Object {
+ end: Object {
+ character: 20
+ line: 0
+ }
+ start: Object {
+ character: 20
+ line: 0
+ }
+ }
+ }
+]
+```
+
+### `init`
+
+```javascript
+Object {
+ id: 10
+ result: Object {
+ serverInfo: Object {name: "rome"}
+ capabilities: Object {
+ codeActionProvider: true
+ documentFormattingProvider: true
+ textDocumentSync: Object {
+ change: 2
+ openClose: true
+ }
+ workspaceDirectories: Object {
+ changeNotifications: true
+ supported: true
+ }
+ executeCommandProvider: Object {
+ commands: Array [
+ "rome.check.decisions"
+ "rome.check.apply"
+ ]
+ }
+ }
+ }
+}
+```

ADD
internal/core/server/lsp/LSPServer.test.ts
+import {test} from "rome";
+import {createIntegrationTest} from "@internal/test-helpers";
+import LSPServer from "@internal/core/server/lsp/LSPServer";
+import {JSONObject} from "@internal/codec-json";
+import {Consumer, consumeUnknown} from "@internal/consume";
+
+function makeRange(
+ startLine: number,
+ startChar: number,
+ endLine: number,
+ endChar: number,
+) {
+ return {
+ start: {
+ line: startLine,
+ character: startChar,
+ },
+ end: {
+ line: endLine,
+ character: endChar,
+ },
+ };
+}
+
+function createMessage(res: JSONObject) {
+ const json = JSON.stringify(res);
+ return `Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`;
+}
+
+function consumeMessage(msg: string): Consumer {
+ const content = msg.split("\r\n\r\n", 2)[1];
+ const data = JSON.parse(content);
+ return consumeUnknown(data, "lsp/parse");
+}
+
+test(
+ "LSPServer",
+ createIntegrationTest(
+ {
+ files: {"foo.ts": "let foo\nif (foo == 2) foo"},
+ },
+ async (t, h) => {
+ const req = await h.createRequest();
+ const lsp = new LSPServer(req);
+ const {transport} = lsp;
+
+ await h.server.projectManager.assertProject(h.cwd);
+
+ function sendMessage(
+ msg: JSONObject,
+ callback: (c: Consumer, resolve: () => void) => void,
+ ) {
+ const out = createMessage(msg);
+ const promise = new Promise((resolve) => {
+ transport.writeEvent.subscribe((response) => {
+ const consumer = consumeMessage(response);
+ callback(consumer, resolve);
+ });
+ });
+ transport.append(out);
+ return promise;
+ }
+
+ await sendMessage(
+ {
+ id: 10,
+ method: "initialize",
+ params: {rootUri: h.cwd.join()},
+ },
+ (msg, resolve) => {
+ if (msg.get("id").asNumberOrVoid() === 10) {
+ t.namedSnapshot("init", msg.asJSONObject());
+ resolve();
+ }
+ },
+ );
+ await lsp.watchProjectEvent.wait();
+
+ await sendMessage(
+ {
+ id: 20,
+ method: "textDocument/formatting",
+ params: {
+ textDocument: {
+ uri: h.cwd.append("foo.ts").join(),
+ },
+ },
+ },
+ (msg, resolve) => {
+ if (msg.get("id").asNumberOrVoid() === 20) {
+ t.namedSnapshot("formatting", msg.get("result").asJSONArray());
+ resolve();
+ }
+ },
+ );
+
+ await sendMessage(
+ {
+ id: 30,
+ method: "textDocument/codeAction",
+ params: {
+ textDocument: {
+ uri: h.cwd.append("foo.ts").join(),
+ },
+ range: makeRange(0, 0, 2, 0),
+ },
+ },
+ (msg, resolve) => {
+ if (msg.get("id").asNumberOrVoid() === 30) {
+ const codeActions = msg.get("result").asMappedArray<string>((c) =>
+ c.get("title").asString()
+ );
+ t.namedSnapshot("code actions", codeActions);
+ resolve();
+ }
+ },
+ );
+
+ const didOpen = createMessage({
+ method: "textDocument/didOpen",
+ params: {
+ textDocument: {
+ uri: h.cwd.append("foo.ts").join(),
+ languageId: "typescript",
+ version: 1,
+ text: "const foo = () => {}",
+ },
+ },
+ });
+ transport.append(didOpen);
+
+ await sendMessage(
+ {
+ id: 40,
+ method: "textDocument/formatting",
+ params: {
+ textDocument: {
+ uri: h.cwd.append("foo.ts").join(),
+ },
+ },
+ },
+ (msg, resolve) => {
+ if (msg.get("id").asNumberOrVoid() === 40) {
+ t.namedSnapshot(
+ "formatting after didOpen",
+ msg.get("result").asJSONArray(),
+ );
+ resolve();
+ }
+ },
+ );
+
+ await sendMessage(
+ {
+ id: 50,
+ method: "workspace/executeCommand",
+ params: {
+ command: "rome.check.apply",
+ arguments: [h.cwd.append("foo.ts").join()],
+ },
+ },
+ (msg, resolve) => {
+ if (msg.get("method").asStringOrVoid() === "workspace/applyEdit") {
+ const edits = msg.get("params").get("edit").get("documentChanges").getIndex(
+ 0,
+ ).get("edits").asJSONArray();
+ t.namedSnapshot("edits", edits);
+ const response = createMessage({
+ id: msg.get("id").asNumber(),
+ result: {
+ applied: true,
+ },
+ });
+ transport.append(response);
+ resolve();
+ }
+ },
+ );
+
+ await sendMessage(
+ {
+ id: 60,
+ method: "shutdown",
+ },
+ (msg, resolve) => {
+ if (msg.get("id").asNumberOrVoid() === 60) {
+ resolve();
+ }
+ },
+ );
+ },
+ ),
+);

MODIFY
internal/core/server/lsp/LSPServer.ts
internal/core/server/lsp/LSPServer.ts
@@ -29,12 +29,18 @@ import {
import {LSPTransport} from "./protocol";
import LSPProgress from "./LSPProgress";
import {
+ convertDiagnosticLocationToLSPRange,
convertDiagnosticsToLSP,
diffTextEdits,
+ doRangesOverlap,
+ getDecisionFromAdviceAction,
+ getLSPRange,
getPathFromTextDocument,
getWorkerBufferPatches,
} from "./utils";
import {markup, readMarkup} from "@internal/markup";
+import {LSPCodeAction} from "./types";
+import {Event} from "@internal/events";
export default class LSPServer {
constructor(request: ServerRequest) {
@@ -45,6 +51,10 @@ export default class LSPServer {
this.lintSessionsPending = new AbsoluteFilePathSet();
this.lintSessions = new AbsoluteFilePathMap();
this.fileBuffers = new AbsoluteFilePathSet();
+ this.fileVersions = new AbsoluteFilePathMap();
+ this.pathToDiagnostics = new AbsoluteFilePathMap();
+
+ this.watchProjectEvent = new Event({name: "watchProject"});
request.endEvent.subscribe(async () => {
await this.shutdown();
@@ -73,8 +83,12 @@ export default class LSPServer {
private server: Server;
private fileBuffers: AbsoluteFilePathSet;
+ private fileVersions: AbsoluteFilePathMap<number>;
private lintSessionsPending: AbsoluteFilePathSet;
private lintSessions: AbsoluteFilePathMap<ServerRequest>;
+ private pathToDiagnostics: AbsoluteFilePathMap<Diagnostics>;
+
+ public watchProjectEvent: Event<AbsoluteFilePath, void>;
private logMessage(path: AbsoluteFilePath, message: string) {
this.server.logger.info(markup`[LSPServer] ${message}`);
@@ -188,6 +202,11 @@ export default class LSPServer {
});
processor.addDiagnostics(diagnostics);
+ this.pathToDiagnostics.set(
+ createAbsoluteFilePath(filename),
+ processor.getDiagnostics(),
+ );
+
this.transport.write({
method: "textDocument/publishDiagnostics",
params: {
@@ -211,6 +230,7 @@ export default class LSPServer {
const date = new Date();
this.logMessage(path, `Watching ${path.join()} at ${date.toTimeString()}`);
+ this.watchProjectEvent.send(path);
}
private async shutdown() {
@@ -265,6 +285,10 @@ export default class LSPServer {
change: 2,
},
documentFormattingProvider: true,
+ codeActionProvider: true,
+ executeCommandProvider: {
+ commands: ["rome.check.decisions", "rome.check.apply"],
+ },
workspaceDirectories: {
supported: true,
changeNotifications: true,
@@ -276,6 +300,132 @@ export default class LSPServer {
};
}
+ case "textDocument/codeAction": {
+ const path = getPathFromTextDocument(params.get("textDocument"));
+ const codeActionRange = getLSPRange(params.get("range"));
+
+ const codeActions: Array<LSPCodeAction> = [];
+ const seenDecisions = new Set<string>();
+
+ const diagnostics = this.pathToDiagnostics.get(path);
+ if (diagnostics === undefined) {
+ return codeActions;
+ }
+
+ for (const diag of diagnostics) {
+ const diagRange = convertDiagnosticLocationToLSPRange(diag.location);
+
+ if (!doRangesOverlap(diagRange, codeActionRange)) {
+ continue;
+ }
+ for (const advice of diag.description.advice) {
+ if (advice.type !== "action" || advice.extra === true) {
+ continue;
+ }
+
+ const decision = getDecisionFromAdviceAction(advice);
+ if (decision === undefined || seenDecisions.has(decision)) {
+ continue;
+ }
+ seenDecisions.add(decision);
+
+ codeActions.push({
+ title: `${readMarkup(advice.noun)}: ${diag.description.category}`,
+ command: {
+ title: "Rome: Check",
+ command: "rome.check.decisions",
+ arguments: [path.join(), decision],
+ },
+ });
+ }
+ }
+
+ codeActions.push({
+ title: "Rome: Fix All",
+ kind: "source.fixAll.rome",
+ command: {
+ title: "Rome: Fix All",
+ command: "rome.check.apply",
+ arguments: [path.join()],
+ },
+ });
+
+ return codeActions;
+ }
+
+ case "workspace/executeCommand": {
+ const command = params.get("command").asString();
+ const filename = params.get("arguments").getIndex(0).asString();
+
+ const path = createAbsoluteFilePath(filename);
+ const startVersion = this.fileVersions.get(path);
+
+ let req: PartialServerQueryRequest | undefined;
+
+ if (command === "rome.check.apply") {
+ req = {
+ commandName: "check",
+ args: [filename],
+ commandFlags: {apply: true},
+ noFileWrites: true,
+ };
+ }
+
+ if (command === "rome.check.decisions") {
+ const decisions = params.get("arguments").getIndex(1).asString();
+ req = {
+ commandName: "check",
+ args: [filename],
+ commandFlags: {decisions},
+ noFileWrites: true,
+ };
+ }
+
+ if (req === undefined) {
+ return null;
+ }
+
+ const response = await this.sendClientRequest(req);
+
+ if (response.type === "SUCCESS" || response.type === "DIAGNOSTICS") {
+ const original = await this.request.requestWorkerGetBuffer(path);
+ const saveFile = response.files[filename];
+ if (original === undefined || saveFile === undefined) {
+ return null;
+ }
+ const endVersion = this.fileVersions.get(path);
+ if (startVersion !== endVersion) {
+ this.logMessage(
+ path,
+ `Can't update ${filename} because it was modified,`,
+ );
+ return null;
+ }
+
+ const edits = diffTextEdits(original, saveFile.content);
+
+ await this.transport.request({
+ method: "workspace/applyEdit",
+ params: {
+ label: "Rome Action",
+ edit: {
+ documentChanges: [
+ {
+ textDocument: {
+ uri: `file://${filename}`,
+ version: endVersion,
+ },
+ edits,
+ },
+ ],
+ },
+ },
+ });
+ }
+
+ return null;
+ }
+
case "textDocument/formatting": {
const path = getPathFromTextDocument(params.get("textDocument"));
@@ -324,12 +474,14 @@ export default class LSPServer {
}
case "textDocument/didOpen": {
- const path = getPathFromTextDocument(params.get("textDocument"));
+ const textDocument = params.get("textDocument");
+ const path = getPathFromTextDocument(textDocument);
const project = this.server.projectManager.findLoadedProject(path);
if (project === undefined) {
return;
}
- const content = params.get("textDocument").get("text").asString();
+ this.fileVersions.set(path, textDocument.get("version").asNumber());
+ const content = textDocument.get("text").asString();
await this.request.requestWorkerUpdateBuffer(path, content);
this.fileBuffers.add(path);
this.logMessage(path, `Opened: ${path.join()}`);
@@ -337,10 +489,12 @@ export default class LSPServer {
}
case "textDocument/didChange": {
- const path = getPathFromTextDocument(params.get("textDocument"));
+ const textDocument = params.get("textDocument");
+ const path = getPathFromTextDocument(textDocument);
if (!this.fileBuffers.has(path)) {
return;
}
+ this.fileVersions.set(path, textDocument.get("version").asNumber());
const contentChanges = params.get("contentChanges");
if (contentChanges.getIndex(0).has("range")) {
@@ -359,6 +513,7 @@ export default class LSPServer {
return;
}
this.fileBuffers.delete(path);
+ this.fileVersions.delete(path);
await this.request.requestWorkerClearBuffer(path);
this.logMessage(path, `Closed: ${path.join()}`);
break;

MODIFY
internal/core/server/lsp/protocol.ts
internal/core/server/lsp/protocol.ts
import {Event} from "@internal/events";
import {JSONObject, JSONPropertyValue} from "@internal/codec-json";
import {Consumer, consumeUnknown} from "@internal/consume";
-import {LSPResponseMessage} from "./types";
+import {LSPRequestMessage, LSPResponseMessage} from "./types";
import {Reporter} from "@internal/cli-reporter";
import {AnyMarkup, markup} from "@internal/markup";
import prettyFormat from "@internal/pretty-format";
@@ -53,6 +53,8 @@ export class LSPTransport {
this.buffer = "";
this.bufferLength = 0;
this.reporter = reporter;
+ this.requestIdCounter = 0;
+ this.requestCallbacks = new Map();
this.requestEvent = new Event({name: "request"});
this.notificationEvent = new Event({name: "notification"});
@@ -65,6 +67,14 @@ export class LSPTransport {
private buffer: string;
private bufferLength: number;
private reporter: Reporter;
+ private requestIdCounter: number;
+ private requestCallbacks: Map<
+ number,
+ {
+ resolve: (data: Consumer) => void;
+ reject: (err: Consumer) => void;
+ }
+ >;
public notificationEvent: Event<MessageEvent, void>;
public errorEvent: Event<Error, void>;
@@ -77,6 +87,14 @@ export class LSPTransport {
this.writeEvent.send(out);
}
+ public async request(req: Omit<LSPRequestMessage, "id">): Promise<Consumer> {
+ return new Promise((resolve, reject) => {
+ const id = ++this.requestIdCounter;
+ this.requestCallbacks.set(id, {resolve, reject});
+ this.write({...req, id});
+ });
+ }
+
private normalizeMessage(content: string): undefined | Consumer {
try {
const data = JSON.parse(content);
@@ -99,6 +117,20 @@ export class LSPTransport {
}
if (!consumer.has("method")) {
+ if (!consumer.has("id")) {
+ return;
+ }
+ const id = consumer.get("id").asNumber();
+ const callbacks = this.requestCallbacks.get(id);
+ this.requestCallbacks.delete(id);
+ if (callbacks === undefined) {
+ return;
+ }
+ if (consumer.has("result")) {
+ callbacks.resolve(consumer.get("result"));
+ } else if (consumer.has("error")) {
+ callbacks.reject(consumer.get("error"));
+ }
return;
}

MODIFY
internal/core/server/lsp/types.ts
internal/core/server/lsp/types.ts
*/
import {Number0} from "@internal/ob1";
-import {JSONPropertyValue} from "@internal/codec-json";
+import {JSONArray, JSONObject, JSONPropertyValue} from "@internal/codec-json";
export type LSPRequestMessage = {
/**
@@ -22,7 +22,7 @@ export type LSPRequestMessage = {
/**
* The method's params.
*/
- params?: unknown;
+ params?: JSONArray | JSONObject;
};
export type LSPResponseMessage = {
@@ -266,3 +266,119 @@ export type LSPTextEdit = {
*/
newText: string;
};
+
+export type LSPCommand = {
+ /**
+ * Title of the command, like `save`.
+ */
+ title: string;
+ /**
+ * The identifier of the actual command handler.
+ */
+ command: string;
+ /**
+ * Arguments that the command handler should be
+ * invoked with.
+ */
+ arguments?: Array<string>;
+};
+
+export type LSPCodeActionKind = string;
+
+export type LSPCodeAction = {
+ /**
+ * A short, human-readable, title for this code action.
+ */
+ title: string;
+
+ /**
+ * The kind of the code action.
+ *
+ * Used to filter code actions.
+ */
+ kind?: LSPCodeActionKind;
+
+ /**
+ * The diagnostics that this code action resolves.
+ */
+ diagnostics?: Array<LSPDiagnostic>;
+
+ /**
+ * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted
+ * by keybindings.
+ *
+ * A quick fix should be marked preferred if it properly addresses the underlying error.
+ * A refactoring should be marked preferred if it is the most reasonable choice of actions to take.
+ *
+ * @since 3.15.0
+ */
+ isPreferred?: boolean;
+
+ /**
+ * The workspace edit this code action performs.
+ */
+ edit?: LSPWorkspaceEdit;
+
+ /**
+ * A command this code action executes. If a code action
+ * provides an edit and a command, first the edit is
+ * executed and then the command.
+ */
+ command?: LSPCommand;
+};
+
+export type LSPWorkspaceEdit = {
+ /**
+ * Holds changes to existing resources.
+ */
+ changes?: {
+ [uri: string]: Array<LSPTextEdit>;
+ };
+
+ /**
+ * Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes
+ * are either an array of `TextDocumentEdit`s to express changes to n different text documents
+ * where each text document edit addresses a specific version of a text document. Or it can contain
+ * above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations.
+ *
+ * Whether a client supports versioned document edits is expressed via
+ * `workspace.workspaceEdit.documentChanges` client capability.
+ *
+ * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then
+ * only plain `TextEdit`s using the `changes` property are supported.
+ */
+ documentChanges?: Array<LSPTextDocumentEdit>;
+};
+
+export type LSPTextDocumentEdit = {
+ /**
+ * The text document to change.
+ */
+ textDocument: LSPVersionedTextDocumentIdentifier;
+
+ /**
+ * The edits to be applied.
+ */
+ edits: Array<LSPTextEdit>;
+};
+
+export type LSPTextDocumentIdentifier = {
+ /**
+ * The text document's URI.
+ */
+ uri: LSPDocumentUri;
+};
+
+export type LSPVersionedTextDocumentIdentifier = LSPTextDocumentIdentifier & {
+ /**
+ * The version number of this document. If a versioned text document identifier
+ * is sent from the server to the client and the file is not open in the editor
+ * (the server has not received an open notification before) the server can send
+ * `null` to indicate that the version is known and the content on disk is the
+ * master (as speced with document content ownership).
+ *
+ * The version number of a document will increase after each change, including
+ * undo/redo. The number doesn't need to be consecutive.
+ */
+ version: number | null;
+};

MODIFY
internal/core/server/lsp/utils.ts
internal/core/server/lsp/utils.ts
@@ -11,7 +11,11 @@ import {
import stringDiff, {Diffs, diffConstants} from "@internal/string-diff";
import {Number0, ob1Coerce1To0, ob1Inc, ob1Number0} from "@internal/ob1";
import {Position} from "@internal/parser-core";
-import {DiagnosticLocation, Diagnostics} from "@internal/diagnostics";
+import {
+ DiagnosticAdviceAction,
+ DiagnosticLocation,
+ Diagnostics,
+} from "@internal/diagnostics";
import {Server} from "@internal/core";
import {WorkerBufferPatch} from "@internal/core/common/bridges/WorkerBridge";
@@ -155,16 +159,11 @@ export function diffTextEdits(
return edits;
}
-export function getWorkerBufferPatches(
- contentChanges: Consumer,
-): Array<WorkerBufferPatch> {
- return contentChanges.asMappedArray((change) => {
- const start = change.get("range").get("start");
- const end = change.get("range").get("end");
+export function getLSPRange(range: Consumer): LSPRange {
+ const start = range.get("start");
+ const end = range.get("end");
return {
- text: change.get("text").asString(),
- range: {
start: {
line: start.get("line").asZeroIndexedNumber(),
character: start.get("character").asZeroIndexedNumber(),
@@ -173,7 +172,41 @@ export function getWorkerBufferPatches(
line: end.get("line").asZeroIndexedNumber(),
character: end.get("character").asZeroIndexedNumber(),
},
- },
+ };
+}
+
+export function getWorkerBufferPatches(
+ contentChanges: Consumer,
+): Array<WorkerBufferPatch> {
+ return contentChanges.asMappedArray((change) => {
+ const range = getLSPRange(change.get("range"));
+
+ return {
+ text: change.get("text").asString(),
+ range,
};
});
}
+
+export function getDecisionFromAdviceAction(
+ advice: DiagnosticAdviceAction,
+): string | undefined {
+ const decisions = (advice?.commandFlags)?.decisions;
+ if (Array.isArray(decisions)) {
+ return decisions[0];
+ }
+ return;
+}
+
+export function doRangesOverlap(a: LSPRange, b: LSPRange) {
+ if (a.start.line > b.end.line || b.start.line > a.end.line) {
+ return false;
+ }
+ if (a.start.line === b.end.line && a.start.character > b.end.character) {
+ return false;
+ }
+ if (b.start.line === a.end.line && b.start.character > a.end.character) {
+ return false;
+ }
+ return true;
+}

MODIFY
internal/core/worker/Worker.ts
internal/core/worker/Worker.ts
@@ -250,6 +250,10 @@ export default class Worker {
};
});
+ bridge.getBuffer.subscribe((payload) => {
+ return this.getBuffer(convertTransportFileReference(payload.ref));
+ });
+
bridge.updateBuffer.subscribe((payload) => {
return this.updateBuffer(
convertTransportFileReference(payload.ref),
@@ -273,6 +277,11 @@ export default class Worker {
});
}
+ public getBuffer(ref: FileReference) {
+ this.logger.info(markup`Returned ${ref.real} buffer`);
+ return this.buffers.get(ref.real);
+ }
+
public clearBuffer({real}: FileReference) {
this.logger.info(markup`Cleared ${real} buffer`);
this.buffers.delete(real);

Add codeAction autofix support for LSP Server (#952)