Suppress "workspace/symbol" not found error (#723)

* Discard outgoing "workspace/symbol" LSP messages
This commit is contained in:
David Kincaid
2024-09-23 13:56:45 -04:00
committed by GitHub
parent 1a84a57647
commit 170d3d4819
5 changed files with 123 additions and 90 deletions

View File

@@ -17,13 +17,13 @@ import { subProcess, killSubProcesses } from "../utils/subspawn";
const log = createLogger("lsp.manager", { output: "Godot LSP" }); const log = createLogger("lsp.manager", { output: "Godot LSP" });
enum ManagerStatus { enum ManagerStatus {
INITIALIZING, INITIALIZING = 0,
INITIALIZING_LSP, INITIALIZING_LSP = 1,
PENDING, PENDING = 2,
PENDING_LSP, PENDING_LSP = 3,
DISCONNECTED, DISCONNECTED = 4,
CONNECTED, CONNECTED = 5,
RETRYING, RETRYING = 6,
} }
export class ClientConnectionManager { export class ClientConnectionManager {
@@ -126,16 +126,18 @@ export class ClientConnectionManager {
if (result.version[2] < minimumVersion) { if (result.version[2] < minimumVersion) {
const message = `Cannot launch headless LSP: Headless LSP mode is only available on v${targetVersion} or newer, but the specified Godot executable is v${result.version}.`; const message = `Cannot launch headless LSP: Headless LSP mode is only available on v${targetVersion} or newer, but the specified Godot executable is v${result.version}.`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore").then(item => { vscode.window
if (item === "Select Godot executable") { .showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore")
select_godot_executable(settingName); .then((item) => {
} else if (item === "Open Settings") { if (item === "Select Godot executable") {
vscode.commands.executeCommand("workbench.action.openSettings", settingName); select_godot_executable(settingName);
} else if (item === "Disable Headless LSP") { } else if (item === "Open Settings") {
set_configuration("lsp.headless", false); vscode.commands.executeCommand("workbench.action.openSettings", settingName);
prompt_for_reload(); } else if (item === "Disable Headless LSP") {
} set_configuration("lsp.headless", false);
}); prompt_for_reload();
}
});
return; return;
} }
@@ -197,7 +199,7 @@ export class ClientConnectionManager {
if (this.target === TargetLSP.HEADLESS) { if (this.target === TargetLSP.HEADLESS) {
options = ["Restart LSP", ...options]; options = ["Restart LSP", ...options];
} }
vscode.window.showInformationMessage(message, ...options).then(item => { vscode.window.showInformationMessage(message, ...options).then((item) => {
if (item === "Restart LSP") { if (item === "Restart LSP") {
this.connect_to_language_server(); this.connect_to_language_server();
} }
@@ -324,7 +326,7 @@ export class ClientConnectionManager {
options = ["Open workspace with Godot Editor", ...options]; options = ["Open workspace with Godot Editor", ...options];
} }
vscode.window.showErrorMessage(message, ...options).then(item => { vscode.window.showErrorMessage(message, ...options).then((item) => {
if (item === "Retry") { if (item === "Retry") {
this.connect_to_language_server(); this.connect_to_language_server();
} }

View File

@@ -1,27 +1,40 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { LanguageClient, NotificationMessage, RequestMessage, ResponseMessage } from "vscode-languageclient/node"; import {
import { EventEmitter } from "events"; LanguageClient,
type NotificationMessage,
type RequestMessage,
type ResponseMessage,
} from "vscode-languageclient/node";
import { EventEmitter } from "node:events";
import { get_configuration, createLogger } from "../utils"; import { get_configuration, createLogger } from "../utils";
import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO"; import {
type Message,
type MessageIO,
MessageIOReader,
MessageIOWriter,
TCPMessageIO,
WebSocketMessageIO,
} from "./MessageIO";
import { globals } from "../extension"; import { globals } from "../extension";
const log = createLogger("lsp.client", { output: "Godot LSP" }); const log = createLogger("lsp.client", { output: "Godot LSP" });
export enum ClientStatus { export enum ClientStatus {
PENDING, PENDING = 0,
DISCONNECTED, DISCONNECTED = 1,
CONNECTED, CONNECTED = 2,
} }
export enum TargetLSP { export enum TargetLSP {
HEADLESS, HEADLESS = 0,
EDITOR, EDITOR = 1,
} }
const CUSTOM_MESSAGE = "gdscript_client/"; const CUSTOM_MESSAGE = "gdscript_client/";
export default class GDScriptLanguageClient extends LanguageClient { export default class GDScriptLanguageClient extends LanguageClient {
public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") === "ws") ? new WebSocketMessageIO() : new TCPMessageIO(); public readonly io: MessageIO =
get_configuration("lsp.serverProtocol") === "ws" ? new WebSocketMessageIO() : new TCPMessageIO();
private _status_changed_callbacks: ((v: ClientStatus) => void)[] = []; private _status_changed_callbacks: ((v: ClientStatus) => void)[] = [];
private _initialize_request: Message = null; private _initialize_request: Message = null;
@@ -35,10 +48,14 @@ export default class GDScriptLanguageClient extends LanguageClient {
public lastSymbolHovered = ""; public lastSymbolHovered = "";
private _started = false; private _started = false;
public get started(): boolean { return this._started; } public get started(): boolean {
return this._started;
}
private _status: ClientStatus; private _status: ClientStatus;
public get status(): ClientStatus { return this._status; } public get status(): ClientStatus {
return this._status;
}
public set status(v: ClientStatus) { public set status(v: ClientStatus) {
if (this._status !== v) { if (this._status !== v) {
this._status = v; this._status = v;
@@ -72,7 +89,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
// Notify the server about file changes to '.gd files contain in the workspace // Notify the server about file changes to '.gd files contain in the workspace
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"), fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
}, },
} },
); );
this.status = ClientStatus.PENDING; this.status = ClientStatus.PENDING;
this.io.on("disconnected", this.on_disconnected.bind(this)); this.io.on("disconnected", this.on_disconnected.bind(this));
@@ -149,7 +166,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
let value: string = message.result["contents"]?.value; let value: string = message.result["contents"]?.value;
if (value) { if (value) {
// this is a dirty hack to fix language server sending us prerendered // this is a dirty hack to fix language server sending us prerendered
// markdown but not correctly stripping leading #'s, leading to // markdown but not correctly stripping leading #'s, leading to
// docstrings being displayed as titles // docstrings being displayed as titles
value = value.replace(/\n[#]+/g, "\n"); value = value.replace(/\n[#]+/g, "\n");
@@ -216,7 +233,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.io.writer.write(this._initialize_request); this.io.writer.write(this._initialize_request);
} }
this.status = ClientStatus.CONNECTED; this.status = ClientStatus.CONNECTED;
const host = get_configuration("lsp.serverHost"); const host = get_configuration("lsp.serverHost");
log.info(`connected to LSP at ${host}:${this.lastPortTried}`); log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
} }
@@ -260,12 +277,12 @@ class MessageHandler extends EventEmitter {
on_message(message: any) { on_message(message: any) {
// FIXME: Hot fix VSCode 1.42 hover position // FIXME: Hot fix VSCode 1.42 hover position
if (message && message.result && message.result.range && message.result.contents) { if (message?.result?.range && message.result.contents) {
message.result.range = undefined; message.result.range = undefined;
} }
// What does this do? // What does this do?
if (message && message.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) { if (message?.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) {
const method = (message.method as string).substring(CUSTOM_MESSAGE.length, message.method.length); const method = (message.method as string).substring(CUSTOM_MESSAGE.length, message.method.length);
if (this[method]) { if (this[method]) {
const ret = this[method](message.params); const ret = this[method](message.params);

View File

@@ -4,34 +4,33 @@
* ------------------------------------------------------------------------------------------ */ * ------------------------------------------------------------------------------------------ */
const DefaultSize: number = 8192; const DefaultSize: number = 8192;
const CR: number = Buffer.from('\r', 'ascii')[0]; const CR: number = Buffer.from("\r", "ascii")[0];
const LF: number = Buffer.from('\n', 'ascii')[0]; const LF: number = Buffer.from("\n", "ascii")[0];
const CRLF: string = '\r\n'; const CRLF: string = "\r\n";
export default class MessageBuffer { export default class MessageBuffer {
private encoding: BufferEncoding; private encoding: BufferEncoding;
private index: number; private index: number;
private buffer: Buffer; private buffer: Buffer;
constructor(encoding: string = 'utf8') { constructor(encoding = "utf8") {
this.encoding = encoding as BufferEncoding; this.encoding = encoding as BufferEncoding;
this.index = 0; this.index = 0;
this.buffer = Buffer.allocUnsafe(DefaultSize); this.buffer = Buffer.allocUnsafe(DefaultSize);
} }
public append(chunk: Buffer | String): void { public append(chunk: Buffer | string): void {
var toAppend: Buffer = <Buffer>chunk; let toAppend: Buffer = <Buffer>chunk;
if (typeof (chunk) === 'string') { if (typeof chunk === "string") {
var str = <string>chunk; const str = <string>chunk;
var bufferLen = Buffer.byteLength(str, this.encoding); const bufferLen = Buffer.byteLength(str, this.encoding);
toAppend = Buffer.allocUnsafe(bufferLen); toAppend = Buffer.allocUnsafe(bufferLen);
toAppend.write(str, 0, bufferLen, this.encoding); toAppend.write(str, 0, bufferLen, this.encoding);
} }
if (this.buffer.length - this.index >= toAppend.length) { if (this.buffer.length - this.index >= toAppend.length) {
toAppend.copy(this.buffer, this.index, 0, toAppend.length); toAppend.copy(this.buffer, this.index, 0, toAppend.length);
} else { } else {
var newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize; const newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
if (this.index === 0) { if (this.index === 0) {
this.buffer = Buffer.allocUnsafe(newSize); this.buffer = Buffer.allocUnsafe(newSize);
toAppend.copy(this.buffer, 0, 0, toAppend.length); toAppend.copy(this.buffer, 0, 0, toAppend.length);
@@ -42,10 +41,16 @@ export default class MessageBuffer {
this.index += toAppend.length; this.index += toAppend.length;
} }
public tryReadHeaders(): { [key: string]: string; } | undefined { public tryReadHeaders(): { [key: string]: string } | undefined {
let result: { [key: string]: string; } | undefined = undefined; let result: { [key: string]: string } | undefined = undefined;
let current = 0; let current = 0;
while (current + 3 < this.index && (this.buffer[current] !== CR || this.buffer[current + 1] !== LF || this.buffer[current + 2] !== CR || this.buffer[current + 3] !== LF)) { while (
current + 3 < this.index &&
(this.buffer[current] !== CR ||
this.buffer[current + 1] !== LF ||
this.buffer[current + 2] !== CR ||
this.buffer[current + 3] !== LF)
) {
current++; current++;
} }
// No header / body separator found (e.g CRLFCRLF) // No header / body separator found (e.g CRLFCRLF)
@@ -53,18 +58,18 @@ export default class MessageBuffer {
return result; return result;
} }
result = Object.create(null); result = Object.create(null);
let headers = this.buffer.toString('ascii', 0, current).split(CRLF); const headers = this.buffer.toString("ascii", 0, current).split(CRLF);
headers.forEach((header) => { for (const header of headers) {
let index: number = header.indexOf(':'); const index: number = header.indexOf(":");
if (index === -1) { if (index === -1) {
throw new Error('Message header must separate key and value using :'); throw new Error("Message header must separate key and value using :");
} }
let key = header.substr(0, index); const key = header.substr(0, index);
let value = header.substr(index + 1).trim(); const value = header.substr(index + 1).trim();
result![key] = value; result![key] = value;
}); }
let nextStart = current + 4; const nextStart = current + 4;
this.buffer = this.buffer.slice(nextStart); this.buffer = this.buffer.slice(nextStart);
this.index = this.index - nextStart; this.index = this.index - nextStart;
return result; return result;
@@ -74,8 +79,8 @@ export default class MessageBuffer {
if (this.index < length) { if (this.index < length) {
return null; return null;
} }
let result = this.buffer.toString(this.encoding, 0, length); const result = this.buffer.toString(this.encoding, 0, length);
let nextStart = length; const nextStart = length;
this.buffer.copy(this.buffer, 0, nextStart); this.buffer.copy(this.buffer, 0, nextStart);
this.index = this.index - nextStart; this.index = this.index - nextStart;
return result; return result;

View File

@@ -1,16 +1,16 @@
import { import {
AbstractMessageReader, AbstractMessageReader,
MessageReader, type MessageReader,
DataCallback, type DataCallback,
Disposable, type Disposable,
RequestMessage, type RequestMessage,
ResponseMessage, type ResponseMessage,
NotificationMessage, type NotificationMessage,
AbstractMessageWriter, AbstractMessageWriter,
MessageWriter type MessageWriter,
} from "vscode-jsonrpc"; } from "vscode-jsonrpc";
import { EventEmitter } from "events"; import { EventEmitter } from "node:events";
import { WebSocket, Data } from "ws"; import { WebSocket, type Data } from "ws";
import { Socket } from "net"; import { Socket } from "net";
import MessageBuffer from "./MessageBuffer"; import MessageBuffer from "./MessageBuffer";
import { createLogger } from "../utils"; import { createLogger } from "../utils";
@@ -45,7 +45,6 @@ export class MessageIO extends EventEmitter {
} }
} }
export class WebSocketMessageIO extends MessageIO { export class WebSocketMessageIO extends MessageIO {
private socket: WebSocket = null; private socket: WebSocket = null;
@@ -59,7 +58,10 @@ export class WebSocketMessageIO extends MessageIO {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.socket = null; this.socket = null;
const ws = new WebSocket(`ws://${host}:${port}`); const ws = new WebSocket(`ws://${host}:${port}`);
ws.on("open", () => { this.on_connected(ws); resolve(); }); ws.on("open", () => {
this.on_connected(ws);
resolve();
});
ws.on("message", this.on_message.bind(this)); ws.on("message", this.on_message.bind(this));
ws.on("error", this.on_disconnected.bind(this)); ws.on("error", this.on_disconnected.bind(this));
ws.on("close", this.on_disconnected.bind(this)); ws.on("close", this.on_disconnected.bind(this));
@@ -91,7 +93,10 @@ export class TCPMessageIO extends MessageIO {
this.socket = null; this.socket = null;
const socket = new Socket(); const socket = new Socket();
socket.connect(port, host); socket.connect(port, host);
socket.on("connect", () => { this.on_connected(socket); resolve(); }); socket.on("connect", () => {
this.on_connected(socket);
resolve();
});
socket.on("data", this.on_message.bind(this)); socket.on("data", this.on_message.bind(this));
socket.on("end", this.on_disconnected.bind(this)); socket.on("end", this.on_disconnected.bind(this));
socket.on("close", this.on_disconnected.bind(this)); socket.on("close", this.on_disconnected.bind(this));
@@ -162,15 +167,15 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
if (!contentLength) { if (!contentLength) {
throw new Error("Header must provide a Content-Length property."); throw new Error("Header must provide a Content-Length property.");
} }
const length = parseInt(contentLength); const length = Number.parseInt(contentLength);
if (isNaN(length)) { if (Number.isNaN(length)) {
throw new Error("Content-Length value must be a number."); throw new Error("Content-Length value must be a number.");
} }
this.nextMessageLength = length; this.nextMessageLength = length;
// Take the encoding form the header. For compatibility // Take the encoding form the header. For compatibility
// treat both utf-8 and utf8 as node utf8 // treat both utf-8 and utf8 as node utf8
} }
var msg = this.buffer.tryReadContent(this.nextMessageLength); const msg = this.buffer.tryReadContent(this.nextMessageLength);
if (msg === null) { if (msg === null) {
/** We haven't received the full message yet. */ /** We haven't received the full message yet. */
this.setPartialMessageTimer(); this.setPartialMessageTimer();
@@ -179,7 +184,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
this.clearPartialMessageTimer(); this.clearPartialMessageTimer();
this.nextMessageLength = -1; this.nextMessageLength = -1;
this.messageToken++; this.messageToken++;
var json = JSON.parse(msg); const json = JSON.parse(msg);
log.debug("rx:", json); log.debug("rx:", json);
this.callback(json); this.callback(json);
@@ -200,13 +205,18 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
if (this._partialMessageTimeout <= 0) { if (this._partialMessageTimeout <= 0) {
return; return;
} }
this.partialMessageTimer = setTimeout((token, timeout) => { this.partialMessageTimer = setTimeout(
this.partialMessageTimer = undefined; (token, timeout) => {
if (token === this.messageToken) { this.partialMessageTimer = undefined;
this.firePartialMessage({ messageToken: token, waitingTime: timeout }); if (token === this.messageToken) {
this.setPartialMessageTimer(); this.firePartialMessage({ messageToken: token, waitingTime: timeout });
} this.setPartialMessageTimer();
}, this._partialMessageTimeout, this.messageToken, this._partialMessageTimeout); }
},
this._partialMessageTimeout,
this.messageToken,
this._partialMessageTimeout,
);
} }
} }
@@ -227,21 +237,20 @@ export class MessageIOWriter extends AbstractMessageWriter implements MessageWri
this.io.on("close", () => this.fireClose()); this.io.on("close", () => this.fireClose());
} }
public end(): void { public end(): void {}
}
public write(msg: Message): Promise<void> { public write(msg: Message): Promise<void> {
// discard outgoing messages that we know aren't supported
if ((msg as RequestMessage).method === "didChangeWatchedFiles") { if ((msg as RequestMessage).method === "didChangeWatchedFiles") {
return; return;
} }
if ((msg as RequestMessage).method === "workspace/symbol") {
return;
}
const json = JSON.stringify(msg); const json = JSON.stringify(msg);
const contentLength = Buffer.byteLength(json, this.encoding); const contentLength = Buffer.byteLength(json, this.encoding);
const headers: string[] = [ const headers: string[] = [ContentLength, contentLength.toString(), CRLF, CRLF];
ContentLength, contentLength.toString(), CRLF,
CRLF
];
try { try {
// callback // callback
this.io.on_send_message(msg); this.io.on_send_message(msg);

View File

@@ -1,4 +1,4 @@
import { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient"; import type { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient";
export interface NativeSymbolInspectParams { export interface NativeSymbolInspectParams {
native_class: string; native_class: string;