Improve LSP connection behavior (fixes Godot3/4 port issue) (#511)

* Add port auto-fallback when attempting to connect to open editor's LSP

* Improve status widget tooltips

* Fix issue with configuration changes requiring a reload

* Upgraded logger utility
This commit is contained in:
Daelon Suzuka
2023-10-28 20:20:19 -04:00
committed by GitHub
parent 55fd8964a6
commit 55617fdd39
5 changed files with 172 additions and 88 deletions

View File

@@ -44,64 +44,89 @@ export class Logger {
}
}
export class Logger2 {
protected tag: string = "";
protected level: string = "";
protected time: boolean = false;
export enum LOG_LEVEL {
SILENT,
ERROR,
WARNING,
INFO,
DEBUG,
}
constructor(tag: string) {
this.tag = tag;
const LOG_LEVEL_NAMES = [
"SILENT",
"ERROR",
"WARN ",
"INFO ",
"DEBUG",
]
const RESET = "\u001b[0m"
const LOG_COLORS = [
RESET, // SILENT, normal
"\u001b[1;31m", // ERROR, red
"\u001b[1;33m", // WARNING, yellow
"\u001b[1;36m", // INFO, cyan
"\u001b[1;32m", // DEBUG, green
]
export class Logger2 {
private show_tag: boolean = true;
private show_time: boolean;
private show_label: boolean;
private show_level: boolean = false;
constructor(
private tag: string,
private level: LOG_LEVEL = LOG_LEVEL.DEBUG,
{ time = false, label = false }: { time?: boolean, label?: boolean } = {},
) {
this.show_time = time;
this.show_label = label;
}
log(...messages) {
let line = "[godotTools]";
if (this.time) {
line += `[${new Date().toISOString()}]`;
private log(level: LOG_LEVEL, ...messages) {
let prefix = "";
if (this.show_label) {
prefix += "[godotTools]";
}
if (this.level) {
line += `[${this.level}]`;
this.level = "";
if (this.show_time) {
prefix += `[${new Date().toISOString()}]`;
}
if (this.tag) {
line += `[${this.tag}]`;
if (this.show_level) {
prefix += "[" + LOG_COLORS[level] + LOG_LEVEL_NAMES[level] + RESET + "]";
}
if (line) {
line += " ";
if (this.show_tag) {
prefix += "[" + LOG_COLORS[level] + this.tag + RESET + "]";
}
for (let index = 0; index < messages.length; index++) {
line += messages[index];
if (index < messages.length) {
line += " ";
} else {
line += "\n";
}
}
console.log(line);
console.log(prefix, ...messages);
}
info(...messages) {
this.level = "INFO";
this.log(messages);
if (LOG_LEVEL.INFO <= this.level) {
this.log(LOG_LEVEL.INFO, ...messages);
}
}
debug(...messages) {
this.level = "DEBUG";
this.log(messages);
if (LOG_LEVEL.DEBUG <= this.level) {
this.log(LOG_LEVEL.DEBUG, ...messages);
}
}
warn(...messages) {
this.level = "WARNING";
this.log(messages);
if (LOG_LEVEL.WARNING <= this.level) {
this.log(LOG_LEVEL.WARNING, ...messages);
}
}
error(...messages) {
this.level = "ERROR";
this.log(messages);
if (LOG_LEVEL.ERROR <= this.level) {
this.log(LOG_LEVEL.ERROR, ...messages);
}
}
}
export function createLogger(tag) {
return new Logger2(tag);
export function createLogger(tag, level: LOG_LEVEL = LOG_LEVEL.DEBUG) {
return new Logger2(tag, level);
}
const logger = new Logger("godot-tools", true);

View File

@@ -1,6 +1,6 @@
import * as vscode from "vscode";
import * as fs from "fs";
import GDScriptLanguageClient, { ClientStatus } from "./GDScriptLanguageClient";
import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient";
import {
get_configuration,
get_free_port,
@@ -30,11 +30,14 @@ export class ClientConnectionManager {
private context: vscode.ExtensionContext;
public client: GDScriptLanguageClient = null;
private reconnection_attempts = 0;
private reconnectionAttempts = 0;
private target: TargetLSP = TargetLSP.EDITOR;
private status: ManagerStatus = ManagerStatus.INITIALIZING;
private statusWidget: vscode.StatusBarItem = null;
private connectedVersion: string = "";
constructor(p_context: vscode.ExtensionContext) {
this.context = p_context;
@@ -46,9 +49,11 @@ export class ClientConnectionManager {
}, get_configuration("lsp.autoReconnect.cooldown"));
register_command("startLanguageServer", () => {
// TODO: this might leave the manager in a wierd state
this.start_language_server();
this.reconnection_attempts = 0;
this.client.connect_to_server();
this.reconnectionAttempts = 0;
this.target = TargetLSP.HEADLESS;
this.client.connect_to_server(this.target);
});
register_command("stopLanguageServer", this.stop_language_server.bind(this));
register_command("checkStatus", this.on_status_item_click.bind(this));
@@ -65,13 +70,16 @@ export class ClientConnectionManager {
private async connect_to_language_server() {
this.client.port = -1;
this.target = TargetLSP.EDITOR;
this.connectedVersion = undefined;
if (get_configuration("lsp.headless")) {
this.target = TargetLSP.HEADLESS;
await this.start_language_server();
}
this.reconnection_attempts = 0;
this.client.connect_to_server();
this.reconnectionAttempts = 0;
this.client.connect_to_server(this.target);
}
private stop_language_server() {
@@ -112,7 +120,7 @@ export class ClientConnectionManager {
});
return;
}
this.connectedVersion = output;
if (match[1] !== projectVersion[0]) {
const message = `Cannot launch headless LSP: The current project uses Godot v${projectVersion}, but the specified Godot executable is version ${match[0]}`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
@@ -207,7 +215,7 @@ export class ClientConnectionManager {
}
private on_status_item_click() {
const lsp_target = this.get_lsp_connection_string();
const lspTarget = this.get_lsp_connection_string();
// TODO: fill these out with the ACTIONS a user could perform in each state
switch (this.status) {
case ManagerStatus.INITIALIZING:
@@ -217,11 +225,21 @@ export class ClientConnectionManager {
// vscode.window.showInformationMessage("Initializing LSP");
break;
case ManagerStatus.PENDING:
// vscode.window.showInformationMessage(`Connecting to the GDScript language server at ${lsp_target}`);
// vscode.window.showInformationMessage(`Connecting to the GDScript language server at ${lspTarget}`);
break;
case ManagerStatus.CONNECTED:
// vscode.window.showInformationMessage("Connected to the GDScript language server.");
case ManagerStatus.CONNECTED: {
const message = `Connected to the GDScript language server at ${lspTarget}.`;
vscode.window.showInformationMessage(
message,
"Restart LSP",
"Ok"
).then(item => {
if (item === "Restart LSP") {
this.connect_to_language_server();
}
});
break;
}
case ManagerStatus.DISCONNECTED:
this.retry_connect_client();
break;
@@ -231,39 +249,47 @@ export class ClientConnectionManager {
}
private update_status_widget() {
const lsp_target = this.get_lsp_connection_string();
const lspTarget = this.get_lsp_connection_string();
const maxAttempts = get_configuration("lsp.autoReconnect.attempts")
let text = "";
let tooltip = "";
switch (this.status) {
case ManagerStatus.INITIALIZING:
// this.statusWidget.text = `INITIALIZING`;
this.statusWidget.text = `$(sync~spin) Initializing`;
this.statusWidget.tooltip = `Initializing extension...`;
text = `$(sync~spin) Initializing`;
tooltip = `Initializing extension...`;
break;
case ManagerStatus.INITIALIZING_LSP:
// this.statusWidget.text = `INITIALIZING_LSP ` + this.reconnection_attempts;
this.statusWidget.text = `$(sync~spin) Initializing LSP`;
this.statusWidget.tooltip = `Connecting to headless GDScript language server at ${lsp_target}`;
text = `$(sync~spin) Initializing LSP ${this.reconnectionAttempts}/${maxAttempts}`;
tooltip = `Connecting to headless GDScript language server.\n${lspTarget}`;
if (this.connectedVersion) {
tooltip += `\n${this.connectedVersion}`;
}
break;
case ManagerStatus.PENDING:
// this.statusWidget.text = `PENDING`;
this.statusWidget.text = `$(sync~spin) Connecting`;
this.statusWidget.tooltip = `Connecting to the GDScript language server at ${lsp_target}`;
text = `$(sync~spin) Connecting`;
tooltip = `Connecting to the GDScript language server at ${lspTarget}`;
break;
case ManagerStatus.CONNECTED:
// this.statusWidget.text = `CONNECTED`;
this.statusWidget.text = `$(check) Connected`;
this.statusWidget.tooltip = `Connected to the GDScript language server.`;
text = `$(check) Connected`;
tooltip = `Connected to the GDScript language server.\n${lspTarget}`;
if (this.connectedVersion) {
tooltip += `\n${this.connectedVersion}`;
}
break;
case ManagerStatus.DISCONNECTED:
// this.statusWidget.text = `DISCONNECTED`;
this.statusWidget.text = `$(x) Disconnected`;
this.statusWidget.tooltip = `Disconnected from the GDScript language server.`;
text = `$(x) Disconnected`;
tooltip = `Disconnected from the GDScript language server.`;
break;
case ManagerStatus.RETRYING:
// this.statusWidget.text = `RETRYING ` + this.reconnection_attempts;
this.statusWidget.text = `$(sync~spin) Connecting ` + this.reconnection_attempts;
this.statusWidget.tooltip = `Connecting to the GDScript language server at ${lsp_target}`;
text = `$(sync~spin) Connecting ${this.reconnectionAttempts}/${maxAttempts}`;
tooltip = `Connecting to the GDScript language server.\n${lspTarget}`;
if (this.connectedVersion) {
tooltip += `\n${this.connectedVersion}`;
}
break;
}
this.statusWidget.text = text;
this.statusWidget.tooltip = tooltip;
}
private on_client_status_changed(status: ClientStatus) {
@@ -307,11 +333,11 @@ export class ClientConnectionManager {
}
private retry_connect_client() {
const auto_retry = get_configuration("lsp.autoReconnect.enabled");
const max_attempts = get_configuration("lsp.autoReconnect.attempts");
if (auto_retry && this.reconnection_attempts <= max_attempts - 1) {
this.reconnection_attempts++;
this.client.connect_to_server();
const autoRetry = get_configuration("lsp.autoReconnect.enabled");
const maxAttempts = get_configuration("lsp.autoReconnect.attempts");
if (autoRetry && this.reconnectionAttempts <= maxAttempts - 1) {
this.reconnectionAttempts++;
this.client.connect_to_server(this.target);
this.retry = true;
return;
}
@@ -320,8 +346,8 @@ export class ClientConnectionManager {
this.status = ManagerStatus.DISCONNECTED;
this.update_status_widget();
const lsp_target = this.get_lsp_connection_string();
let message = `Couldn't connect to the GDScript language server at ${lsp_target}. Is the Godot editor or language server running?`;
const lspTarget = this.get_lsp_connection_string();
let message = `Couldn't connect to the GDScript language server at ${lspTarget}. Is the Godot editor or language server running?`;
vscode.window.showErrorMessage(message, "Retry", "Ignore").then(item => {
if (item == "Retry") {
this.connect_to_language_server();

View File

@@ -1,10 +1,10 @@
import { EventEmitter } from "events";
import * as vscode from 'vscode';
import { LanguageClient, RequestMessage, ResponseMessage } from "vscode-languageclient/node";
import { LanguageClient, RequestMessage, ResponseMessage, integer } from "vscode-languageclient/node";
import { createLogger } from "../logger";
import { get_configuration, set_context } from "../utils";
import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
import NativeDocumentManager from './NativeDocumentManager';
import { NativeDocumentManager } from './NativeDocumentManager';
const log = createLogger("lsp.client");
@@ -13,10 +13,15 @@ export enum ClientStatus {
DISCONNECTED,
CONNECTED,
}
export enum TargetLSP {
HEADLESS,
EDITOR,
}
const CUSTOM_MESSAGE = "gdscrip_client/";
export default class GDScriptLanguageClient extends LanguageClient {
public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") == "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
private context: vscode.ExtensionContext;
@@ -27,7 +32,10 @@ export default class GDScriptLanguageClient extends LanguageClient {
private message_handler: MessageHandler = null;
private native_doc_manager: NativeDocumentManager = null;
public target: TargetLSP = TargetLSP.EDITOR;
public port: number = -1;
public lastPortTried: number = -1;
public sentMessages = new Map();
public lastSymbolHovered: string = "";
@@ -83,14 +91,26 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.native_doc_manager = new NativeDocumentManager(this.io);
}
connect_to_server() {
connect_to_server(target: TargetLSP = TargetLSP.EDITOR) {
this.target = target;
this.status = ClientStatus.PENDING;
const host = get_configuration("lsp.serverHost");
let port = get_configuration("lsp.serverPort");
if (this.port !== -1) {
port = this.port;
}
log.info(`attempting to connect to LSP at port ${port}`);
if (this.target == TargetLSP.EDITOR) {
if (port === 6005 || port === 6008) {
port = 6005;
}
}
this.lastPortTried = port;
const host = get_configuration("lsp.serverHost");
log.info(`attempting to connect to LSP at ${host}:${port}`);
this.io.connect_to_language_server(host, port);
}
@@ -100,7 +120,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
private on_send_message(message: RequestMessage) {
log.debug("tx: " + JSON.stringify(message));
log.debug("tx:", message);
this.sentMessages.set(message.id, message.method);
@@ -111,7 +131,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
private on_message(message: ResponseMessage) {
const msgString = JSON.stringify(message);
log.debug("rx: " + msgString);
log.debug("rx:", message);
// This is a dirty hack to fix the language server sending us
// invalid file URIs
@@ -178,6 +198,21 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
private on_disconnected() {
if (this.target == TargetLSP.EDITOR) {
const host = get_configuration("lsp.serverHost");
let port = get_configuration("lsp.serverPort");
if (port === 6005 || port === 6008) {
if (this.lastPortTried === 6005) {
port = 6008;
log.info(`attempting to connect to LSP at ${host}:${port}`);
this.lastPortTried = port;
this.io.connect_to_language_server(host, port);
return;
}
}
}
this.status = ClientStatus.DISCONNECTED;
}
}

View File

@@ -24,7 +24,7 @@ const enum WebViewMessageType {
INSPECT_NATIVE_SYMBOL = "INSPECT_NATIVE_SYMBOL",
}
export default class NativeDocumentManager extends EventEmitter {
export class NativeDocumentManager extends EventEmitter {
private io: MessageIO = null;
private native_classes: { [key: string]: GodotNativeClassInfo } = {};

View File

@@ -5,10 +5,8 @@ import { AddressInfo, createServer } from "net";
const EXTENSION_PREFIX = "godotTools";
const config = vscode.workspace.getConfiguration(EXTENSION_PREFIX);
export function get_configuration(name: string, default_value?: any) {
let config_value = config.get(name, null);
let config_value = vscode.workspace.getConfiguration(EXTENSION_PREFIX).get(name, null);
if (default_value && config_value === null) {
return default_value;
}
@@ -16,7 +14,7 @@ export function get_configuration(name: string, default_value?: any) {
}
export function set_configuration(name: string, value: any) {
return config.update(name, value);
return vscode.workspace.getConfiguration(EXTENSION_PREFIX).update(name, value);
}
export function is_debug_mode(): boolean {