Overhaul LSP Client (#752)

* Simplify LSP Client internals
* Streamline control flow between Client, IO, and Buffer classes
* Create canonical, obvious place to implement filters on incoming and outgoing LSP messages
* Remove legacy WS LSP support
This commit is contained in:
David Kincaid
2024-11-18 10:53:59 -05:00
committed by GitHub
parent fd637d0641
commit 694feea1bc
9 changed files with 288 additions and 399 deletions

View File

@@ -296,21 +296,6 @@
"default": false,
"description": "Whether extra space should be removed from function parameter lists"
},
"godotTools.lsp.serverProtocol": {
"type": [
"string"
],
"enum": [
"ws",
"tcp"
],
"default": "tcp",
"enumDescriptions": [
"Use the WebSocket protocol to connect to Godot 3.2 and Godot 3.2.1",
"Use the TCP protocol to connect to Godot 3.2.2 and newer versions"
],
"description": "The server protocol of the GDScript language server.\nYou must restart VSCode after changing this value."
},
"godotTools.lsp.serverHost": {
"type": "string",
"default": "127.0.0.1",

View File

@@ -134,7 +134,7 @@ function copy_resource_path(uri: vscode.Uri) {
}
async function list_classes() {
await globals.lsp.client.list_classes();
await globals.docsProvider.list_native_classes();
}
async function switch_scene_script() {

View File

@@ -1,18 +1,19 @@
import * as vscode from "vscode";
import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient";
import {
createLogger,
get_configuration,
get_free_port,
get_project_dir,
get_project_version,
set_context,
register_command,
set_configuration,
createLogger,
set_context,
verify_godot_version,
} from "../utils";
import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
import { subProcess, killSubProcesses } from "../utils/subspawn";
import { killSubProcesses, subProcess } from "../utils/subspawn";
import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient";
const log = createLogger("lsp.manager", { output: "Godot LSP" });
@@ -38,10 +39,8 @@ export class ClientConnectionManager {
private connectedVersion = "";
constructor(private context: vscode.ExtensionContext) {
this.context = context;
this.client = new GDScriptLanguageClient(context);
this.client.watch_status(this.on_client_status_changed.bind(this));
this.client = new GDScriptLanguageClient();
this.client.events.on("status", this.on_client_status_changed.bind(this));
setInterval(() => {
this.retry_callback();
@@ -60,7 +59,7 @@ export class ClientConnectionManager {
this.start_language_server();
this.reconnectionAttempts = 0;
this.target = TargetLSP.HEADLESS;
this.client.connect_to_server(this.target);
this.client.connect(this.target);
}),
register_command("stopLanguageServer", this.stop_language_server.bind(this)),
register_command("checkStatus", this.on_status_item_click.bind(this)),
@@ -81,7 +80,7 @@ export class ClientConnectionManager {
}
this.reconnectionAttempts = 0;
this.client.connect_to_server(this.target);
this.client.connect(this.target);
}
private stop_language_server() {
@@ -269,7 +268,7 @@ export class ClientConnectionManager {
this.reconnectionAttempts = 0;
set_context("connectedToLSP", true);
this.status = ManagerStatus.CONNECTED;
if (!this.client.started) {
if (this.client.needsStart()) {
this.context.subscriptions.push(this.client.start());
}
break;
@@ -305,7 +304,7 @@ export class ClientConnectionManager {
const maxAttempts = get_configuration("lsp.autoReconnect.attempts");
if (autoRetry && this.reconnectionAttempts <= maxAttempts - 1) {
this.reconnectionAttempts++;
this.client.connect_to_server(this.target);
this.client.connect(this.target);
this.retry = true;
return;
}

View File

@@ -1,21 +1,17 @@
import EventEmitter from "node:events";
import * as vscode from "vscode";
import {
LanguageClient,
type LanguageClientOptions,
type NotificationMessage,
type RequestMessage,
type ResponseMessage,
type ServerOptions,
} from "vscode-languageclient/node";
import { EventEmitter } from "node:events";
import { get_configuration, createLogger } from "../utils";
import {
type Message,
type MessageIO,
MessageIOReader,
MessageIOWriter,
TCPMessageIO,
WebSocketMessageIO,
} from "./MessageIO";
import { globals } from "../extension";
import { createLogger, get_configuration } from "../utils";
import { MessageIO } from "./MessageIO";
const log = createLogger("lsp.client", { output: "Godot LSP" });
@@ -30,80 +26,80 @@ export enum TargetLSP {
EDITOR = 1,
}
const CUSTOM_MESSAGE = "gdscript_client/";
export type Target = {
host: string;
port: number;
type: TargetLSP;
};
type HoverResult = {
contents: {
kind: string;
value: string;
};
range: {
end: {
character: number;
line: number;
};
start: {
character: number;
line: number;
};
};
};
type HoverResponseMesssage = {
id: number;
jsonrpc: string;
result: HoverResult;
};
export default class GDScriptLanguageClient extends LanguageClient {
public readonly io: MessageIO =
get_configuration("lsp.serverProtocol") === "ws" ? new WebSocketMessageIO() : new TCPMessageIO();
private _status_changed_callbacks: ((v: ClientStatus) => void)[] = [];
private _initialize_request: Message = null;
private messageHandler: MessageHandler = null;
public io: MessageIO = new MessageIO();
public target: TargetLSP = TargetLSP.EDITOR;
public port = -1;
public lastPortTried = -1;
public sentMessages = new Map();
public lastSymbolHovered = "";
private _started = false;
public get started(): boolean {
return this._started;
}
events = new EventEmitter();
private _status: ClientStatus;
public get status(): ClientStatus {
return this._status;
}
public set status(v: ClientStatus) {
if (this._status !== v) {
this._status = v;
for (const callback of this._status_changed_callbacks) {
callback(v);
}
}
this._status = v;
this.events.emit("status", this._status);
}
public watch_status(callback: (v: ClientStatus) => void) {
if (this._status_changed_callbacks.indexOf(callback) === -1) {
this._status_changed_callbacks.push(callback);
}
}
constructor() {
const serverOptions: ServerOptions = () => {
return new Promise((resolve, reject) => {
resolve({ reader: this.io.reader, writer: this.io.writer });
});
};
constructor(private context: vscode.ExtensionContext) {
super(
"GDScriptLanguageClient",
() => {
return new Promise((resolve, reject) => {
resolve({ reader: new MessageIOReader(this.io), writer: new MessageIOWriter(this.io) });
});
const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: "file", language: "gdscript" },
{ scheme: "untitled", language: "gdscript" },
],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
},
{
// Register the server for plain text documents
documentSelector: [
{ scheme: "file", language: "gdscript" },
{ scheme: "untitled", language: "gdscript" },
],
synchronize: {
// Notify the server about file changes to '.gd files contain in the workspace
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
},
},
);
};
super("GDScriptLanguageClient", serverOptions, clientOptions);
this.status = ClientStatus.PENDING;
this.io.on("disconnected", this.on_disconnected.bind(this));
this.io.on("connected", this.on_connected.bind(this));
this.io.on("message", this.on_message.bind(this));
this.io.on("send_message", this.on_send_message.bind(this));
this.messageHandler = new MessageHandler(this.io);
this.io.on("disconnected", this.on_disconnected.bind(this));
this.io.requestFilter = this.request_filter.bind(this);
this.io.responseFilter = this.response_filter.bind(this);
this.io.notificationFilter = this.notification_filter.bind(this);
}
public async list_classes() {
await globals.docsProvider.list_native_classes();
}
connect_to_server(target: TargetLSP = TargetLSP.EDITOR) {
connect(target: TargetLSP = TargetLSP.EDITOR) {
this.target = target;
this.status = ClientStatus.PENDING;
@@ -123,70 +119,74 @@ export default class GDScriptLanguageClient extends LanguageClient {
const host = get_configuration("lsp.serverHost");
log.info(`attempting to connect to LSP at ${host}:${port}`);
this.io.connect_to_language_server(host, port);
this.io.connect(host, port);
}
start() {
this._started = true;
return super.start();
}
private on_send_message(message: RequestMessage) {
private request_filter(message: RequestMessage) {
this.sentMessages.set(message.id, message);
if (message.method === "initialize") {
this._initialize_request = message;
// discard outgoing messages that we know aren't supported
if (message.method === "didChangeWatchedFiles") {
return;
}
if (message.method === "workspace/symbol") {
return;
}
return message;
}
private on_message(message: ResponseMessage | NotificationMessage) {
const msgString = JSON.stringify(message);
private response_filter(message: ResponseMessage) {
const sentMessage = this.sentMessages.get(message.id);
if (sentMessage?.method === "textDocument/hover") {
// fix markdown contents
let value: string = (message as HoverResponseMesssage).result.contents.value;
if (value) {
// this is a dirty hack to fix language server sending us prerendered
// markdown but not correctly stripping leading #'s, leading to
// docstrings being displayed as titles
value = value.replace(/\n[#]+/g, "\n");
// This is a dirty hack to fix the language server sending us
// invalid file URIs
// This should be forward-compatible, meaning that it will work
// with the current broken version, AND the fixed future version.
const match = msgString.match(/"target":"file:\/\/[^\/][^"]*"/);
if (match) {
const count = (message["result"] as Array<object>).length;
for (let i = 0; i < count; i++) {
const x: string = message["result"][i]["target"];
message["result"][i]["target"] = x.replace("file://", "file:///");
// fix bbcode line breaks
value = value.replaceAll("`br`", "\n\n");
// fix bbcode code boxes
value = value.replace("`codeblocks`", "");
value = value.replace("`/codeblocks`", "");
value = value.replace("`gdscript`", "\nGDScript:\n```gdscript");
value = value.replace("`/gdscript`", "```");
value = value.replace("`csharp`", "\nC#:\n```csharp");
value = value.replace("`/csharp`", "```");
(message as HoverResponseMesssage).result.contents.value = value;
}
}
if ("method" in message && message.method === "gdscript/capabilities") {
return message;
}
private notification_filter(message: NotificationMessage) {
if (message.method === "gdscript_client/changeWorkspace") {
//
}
if (message.method === "gdscript/capabilities") {
globals.docsProvider.register_capabilities(message);
}
if ("id" in message) {
const sentMessage = this.sentMessages.get(message.id);
if (sentMessage && sentMessage.method === "textDocument/hover") {
// fix markdown contents
let value: string = message.result["contents"]?.value;
if (value) {
// this is a dirty hack to fix language server sending us prerendered
// markdown but not correctly stripping leading #'s, leading to
// docstrings being displayed as titles
value = value.replace(/\n[#]+/g, "\n");
// if (message.method === "textDocument/publishDiagnostics") {
// for (const diagnostic of message.params.diagnostics) {
// if (diagnostic.code === 6) {
// log.debug("UNUSED_SIGNAL", diagnostic);
// return;
// }
// if (diagnostic.code === 2) {
// log.debug("UNUSED_VARIABLE", diagnostic);
// return;
// }
// }
// }
// fix bbcode line breaks
value = value.replaceAll("`br`", "\n\n");
// fix bbcode code boxes
value = value.replace("`codeblocks`", "");
value = value.replace("`/codeblocks`", "");
value = value.replace("`gdscript`", "\nGDScript:\n```gdscript");
value = value.replace("`/gdscript`", "```");
value = value.replace("`csharp`", "\nC#:\n```csharp");
value = value.replace("`/csharp`", "```");
message.result["contents"].value = value;
}
}
}
this.messageHandler.on_message(message);
return message;
}
public async get_symbol_at_position(uri: vscode.Uri, position: vscode.Position) {
@@ -194,13 +194,13 @@ export default class GDScriptLanguageClient extends LanguageClient {
textDocument: { uri: uri.toString() },
position: { line: position.line, character: position.character },
};
const response = await this.sendRequest("textDocument/hover", params);
const response: HoverResult = await this.sendRequest("textDocument/hover", params);
return this.parse_hover_response(response);
return this.parse_hover_result(response);
}
private parse_hover_response(message) {
const contents = message["contents"];
private parse_hover_result(message: HoverResult) {
const contents = message.contents;
let decl: string;
if (Array.isArray(contents)) {
@@ -229,9 +229,6 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
private on_connected() {
if (this._initialize_request) {
this.io.writer.write(this._initialize_request);
}
this.status = ClientStatus.CONNECTED;
const host = get_configuration("lsp.serverHost");
@@ -249,7 +246,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
log.info(`attempting to connect to LSP at ${host}:${port}`);
this.lastPortTried = port;
this.io.connect_to_language_server(host, port);
this.io.connect(host, port);
return;
}
}
@@ -257,39 +254,3 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.status = ClientStatus.DISCONNECTED;
}
}
class MessageHandler extends EventEmitter {
private io: MessageIO = null;
constructor(io: MessageIO) {
super();
this.io = io;
}
// changeWorkspace(params: { path: string }) {
// vscode.window.showErrorMessage("The GDScript language server can't work properly!\nThe open workspace is different from the editor's.", 'Reload', 'Ignore').then(item => {
// if (item == "Reload") {
// let folderUrl = vscode.Uri.file(params.path);
// vscode.commands.executeCommand('vscode.openFolder', folderUrl, false);
// }
// });
// }
on_message(message: any) {
// FIXME: Hot fix VSCode 1.42 hover position
if (message?.result?.range && message.result.contents) {
message.result.range = undefined;
}
// What does this do?
if (message?.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) {
const method = (message.method as string).substring(CUSTOM_MESSAGE.length, message.method.length);
if (this[method]) {
const ret = this[method](message.params);
if (ret) {
this.io.writer.write(ret);
}
}
}
}
}

View File

@@ -3,21 +3,28 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { createLogger } from "../utils";
const log = createLogger("lsp.buf");
const DefaultSize: number = 8192;
const CR: number = Buffer.from("\r", "ascii")[0];
const LF: number = Buffer.from("\n", "ascii")[0];
const CRLF: string = "\r\n";
export default class MessageBuffer {
private encoding: BufferEncoding;
private index: number;
private buffer: Buffer;
type Headers = { [key: string]: string };
constructor(encoding = "utf8") {
this.encoding = encoding as BufferEncoding;
this.index = 0;
this.buffer = Buffer.allocUnsafe(DefaultSize);
}
export default class MessageBuffer {
private encoding: BufferEncoding = "utf8";
private index = 0;
private buffer: Buffer = Buffer.allocUnsafe(DefaultSize);
private nextMessageLength: number;
private messageToken: number;
private partialMessageTimer: NodeJS.Timeout | undefined;
private _partialMessageTimeout = 10000;
constructor(private reader) {}
public append(chunk: Buffer | string): void {
let toAppend: Buffer = <Buffer>chunk;
@@ -41,8 +48,7 @@ export default class MessageBuffer {
this.index += toAppend.length;
}
public tryReadHeaders(): { [key: string]: string } | undefined {
let result: { [key: string]: string } | undefined = undefined;
public tryReadHeaders(): Headers | undefined {
let current = 0;
while (
current + 3 < this.index &&
@@ -55,19 +61,19 @@ export default class MessageBuffer {
}
// No header / body separator found (e.g CRLFCRLF)
if (current + 3 >= this.index) {
return result;
return undefined;
}
result = Object.create(null);
const result = Object.create(null);
const headers = this.buffer.toString("ascii", 0, current).split(CRLF);
for (const header of headers) {
for (const header of headers) {
const index: number = header.indexOf(":");
if (index === -1) {
throw new Error("Message header must separate key and value using :");
}
const key = header.substr(0, index);
const value = header.substr(index + 1).trim();
result![key] = value;
}
result[key] = value;
}
const nextStart = current + 4;
this.buffer = this.buffer.slice(nextStart);
@@ -86,7 +92,66 @@ export default class MessageBuffer {
return result;
}
public get numberOfBytes(): number {
return this.index;
public ready() {
if (this.nextMessageLength === -1) {
const headers = this.tryReadHeaders();
if (!headers) {
return;
}
const contentLength = headers["Content-Length"];
if (!contentLength) {
log.warn("Header must provide a Content-Length property.");
return;
}
const length = Number.parseInt(contentLength);
if (Number.isNaN(length)) {
log.warn("Content-Length value must be a number.");
return;
}
this.nextMessageLength = length;
}
const msg = this.tryReadContent(this.nextMessageLength);
if (!msg) {
log.warn("haven't recieved full message");
this.setPartialMessageTimer();
return;
}
this.clearPartialMessageTimer();
this.nextMessageLength = -1;
this.messageToken++;
return msg;
}
public reset() {
this.nextMessageLength = -1;
this.messageToken = 0;
this.partialMessageTimer = undefined;
}
private clearPartialMessageTimer(): void {
if (this.partialMessageTimer) {
clearTimeout(this.partialMessageTimer);
this.partialMessageTimer = undefined;
}
}
private setPartialMessageTimer(): void {
this.clearPartialMessageTimer();
if (this._partialMessageTimeout <= 0) {
return;
}
this.partialMessageTimer = setTimeout(
(token, timeout) => {
this.partialMessageTimer = undefined;
if (token === this.messageToken) {
this.reader.firePartialMessage({ messageToken: token, waitingTime: timeout });
this.setPartialMessageTimer();
}
},
this._partialMessageTimeout,
this.messageToken,
this._partialMessageTimeout,
);
}
}

View File

@@ -10,7 +10,6 @@ import {
type MessageWriter,
} from "vscode-jsonrpc";
import { EventEmitter } from "node:events";
import { WebSocket, type Data } from "ws";
import { Socket } from "net";
import MessageBuffer from "./MessageBuffer";
import { createLogger } from "../utils";
@@ -20,252 +19,132 @@ const log = createLogger("lsp.io", { output: "Godot LSP" });
export type Message = RequestMessage | ResponseMessage | NotificationMessage;
export class MessageIO extends EventEmitter {
reader: MessageIOReader = null;
writer: MessageIOWriter = null;
reader = new MessageIOReader(this);
writer = new MessageIOWriter(this);
public send_message(message: string) {
// virtual
}
requestFilter: (msg: RequestMessage) => RequestMessage = (msg) => msg;
responseFilter: (msg: ResponseMessage) => ResponseMessage = (msg) => msg;
notificationFilter: (msg: NotificationMessage) => NotificationMessage = (msg) => msg;
protected on_message(chunk: Data) {
const message = chunk.toString();
this.emit("data", message);
}
socket: Socket = null;
messageCache: string[] = [];
on_send_message(message: any) {
this.emit("send_message", message);
}
on_message_callback(message: any) {
this.emit("message", message);
}
async connect_to_language_server(host: string, port: number): Promise<void> {
// virtual
}
}
export class WebSocketMessageIO extends MessageIO {
private socket: WebSocket = null;
public send_message(message: string) {
if (this.socket) {
this.socket.send(message);
}
}
async connect_to_language_server(host: string, port: number): Promise<void> {
async connect(host: string, port: number): Promise<void> {
log.debug(`connecting to ${host}:${port}`);
return new Promise((resolve, reject) => {
this.socket = null;
const ws = new WebSocket(`ws://${host}:${port}`);
ws.on("open", () => {
this.on_connected(ws);
resolve();
});
ws.on("message", this.on_message.bind(this));
ws.on("error", this.on_disconnected.bind(this));
ws.on("close", this.on_disconnected.bind(this));
});
}
protected on_connected(socket: WebSocket) {
this.socket = socket;
this.emit("connected");
}
protected on_disconnected() {
this.socket = null;
this.emit("disconnected");
}
}
export class TCPMessageIO extends MessageIO {
private socket: Socket = null;
public send_message(message: string) {
if (this.socket) {
this.socket.write(message);
}
}
async connect_to_language_server(host: string, port: number): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = null;
const socket = new Socket();
socket.connect(port, host);
socket.on("connect", () => {
this.on_connected(socket);
this.socket = socket;
while (this.messageCache.length > 0) {
const msg = this.messageCache.shift();
this.socket.write(msg);
}
this.emit("connected");
resolve();
});
socket.on("data", this.on_message.bind(this));
socket.on("end", this.on_disconnected.bind(this));
socket.on("close", this.on_disconnected.bind(this));
socket.on("error", this.on_error.bind(this));
socket.on("data", (chunk: Buffer) => {
this.emit("data", chunk.toString());
});
// socket.on("end", this.on_disconnected.bind(this));
socket.on("error", () => {
this.socket = null;
this.emit("disconnected");
});
socket.on("close", () => {
this.socket = null;
this.emit("disconnected");
});
});
}
protected on_connected(socket: Socket) {
this.socket = socket;
this.emit("connected");
}
protected on_disconnected() {
this.socket = null;
this.emit("disconnected");
}
protected on_error(error) {
// TODO: handle errors?
write(message: string) {
if (this.socket) {
this.socket.write(message);
} else {
this.messageCache.push(message);
}
}
}
export class MessageIOReader extends AbstractMessageReader implements MessageReader {
private io: MessageIO;
private callback: DataCallback;
private buffer: MessageBuffer;
private nextMessageLength: number;
private messageToken: number;
private partialMessageTimer: NodeJS.Timeout | undefined;
private _partialMessageTimeout: number;
callback: DataCallback;
private buffer = new MessageBuffer(this);
public constructor(io: MessageIO, encoding: BufferEncoding = "utf8") {
constructor(public io: MessageIO) {
super();
this.io = io;
this.io.reader = this;
this.buffer = new MessageBuffer(encoding);
this._partialMessageTimeout = 10000;
}
public set partialMessageTimeout(timeout: number) {
this._partialMessageTimeout = timeout;
}
listen(callback: DataCallback): Disposable {
this.buffer.reset();
public get partialMessageTimeout(): number {
return this._partialMessageTimeout;
}
public listen(callback: DataCallback): Disposable {
this.nextMessageLength = -1;
this.messageToken = 0;
this.partialMessageTimer = undefined;
this.callback = callback;
this.io.on("data", this.onData.bind(this));
this.io.on("data", this.on_data.bind(this));
this.io.on("error", this.fireError.bind(this));
this.io.on("close", this.fireClose.bind(this));
return;
}
private onData(data: Buffer | string): void {
private on_data(data: Buffer | string): void {
this.buffer.append(data);
while (true) {
if (this.nextMessageLength === -1) {
const headers = this.buffer.tryReadHeaders();
if (!headers) {
return;
}
const contentLength = headers["Content-Length"];
if (!contentLength) {
throw new Error("Header must provide a Content-Length property.");
}
const length = Number.parseInt(contentLength);
if (Number.isNaN(length)) {
throw new Error("Content-Length value must be a number.");
}
this.nextMessageLength = length;
// Take the encoding form the header. For compatibility
// treat both utf-8 and utf8 as node utf8
}
const msg = this.buffer.tryReadContent(this.nextMessageLength);
if (msg === null) {
/** We haven't received the full message yet. */
this.setPartialMessageTimer();
const msg = this.buffer.ready();
if (!msg) {
return;
}
this.clearPartialMessageTimer();
this.nextMessageLength = -1;
this.messageToken++;
const json = JSON.parse(msg);
// allow message to be modified
let modified: ResponseMessage | NotificationMessage;
if ("id" in json) {
modified = this.io.responseFilter(json);
} else if ("method" in json) {
modified = this.io.notificationFilter(json);
} else {
log.warn("rx [unhandled]:", json);
}
log.debug("rx:", json);
if (!modified) {
log.debug("rx [discarded]:", json);
return;
}
log.debug("rx:", modified);
this.callback(json);
// callback
this.io.on_message_callback(json);
}
}
private clearPartialMessageTimer(): void {
if (this.partialMessageTimer) {
clearTimeout(this.partialMessageTimer);
this.partialMessageTimer = undefined;
}
}
private setPartialMessageTimer(): void {
this.clearPartialMessageTimer();
if (this._partialMessageTimeout <= 0) {
return;
}
this.partialMessageTimer = setTimeout(
(token, timeout) => {
this.partialMessageTimer = undefined;
if (token === this.messageToken) {
this.firePartialMessage({ messageToken: token, waitingTime: timeout });
this.setPartialMessageTimer();
}
},
this._partialMessageTimeout,
this.messageToken,
this._partialMessageTimeout,
);
}
}
const ContentLength: string = "Content-Length: ";
const CRLF = "\r\n";
export class MessageIOWriter extends AbstractMessageWriter implements MessageWriter {
private io: MessageIO;
private encoding: BufferEncoding;
private errorCount: number;
public constructor(io: MessageIO, encoding: BufferEncoding = "utf8") {
constructor(public io: MessageIO) {
super();
this.io = io;
this.io.writer = this;
this.encoding = encoding as BufferEncoding;
this.errorCount = 0;
this.io.on("error", (error: any) => this.fireError(error));
this.io.on("close", () => this.fireClose());
}
public end(): void {}
public write(msg: Message): Promise<void> {
// discard outgoing messages that we know aren't supported
if ((msg as RequestMessage).method === "didChangeWatchedFiles") {
async write(msg: RequestMessage) {
const modified = this.io.requestFilter(msg);
if (!modified) {
log.debug("tx [discarded]:", msg);
return;
}
if ((msg as RequestMessage).method === "workspace/symbol") {
return;
}
const json = JSON.stringify(msg);
const contentLength = Buffer.byteLength(json, this.encoding);
log.debug("tx:", modified);
const json = JSON.stringify(modified);
const headers: string[] = [ContentLength, contentLength.toString(), CRLF, CRLF];
const contentLength = Buffer.byteLength(json, "utf-8").toString();
const message = `Content-Length: ${contentLength}\r\n\r\n${json}`;
try {
// callback
this.io.on_send_message(msg);
// Header must be written in ASCII encoding
this.io.send_message(headers.join(""));
// Now write the content. This can be written in any encoding
log.debug("tx:", msg);
this.io.send_message(json);
this.io.write(message);
this.errorCount = 0;
} catch (error) {
this.errorCount++;
this.fireError(error, msg, this.errorCount);
this.fireError(error, modified, this.errorCount);
}
return;
}
end(): void {}
}

View File

@@ -14,7 +14,7 @@ import type {
GodotNativeSymbol,
GodotNativeClassInfo,
GodotCapabilities,
} from "../lsp/gdscript.capabilities";
} from "./documentation_types";
import { make_html_content } from "./documentation_builder";
import { createLogger, get_configuration, get_extension_uri, make_docs_uri } from "../utils";
import { globals } from "../extension";

View File

@@ -3,7 +3,7 @@ import { SymbolKind } from "vscode-languageclient";
import * as Prism from "prismjs";
import * as csharp from "prismjs/components/prism-csharp";
import { marked } from "marked";
import type { GodotNativeSymbol } from "../lsp/gdscript.capabilities";
import type { GodotNativeSymbol } from "./documentation_types";
import { get_extension_uri } from "../utils";
import yabbcode = require("ya-bbcode");