From 55617fdd398100991cc61f489646b71b15d0fbd5 Mon Sep 17 00:00:00 2001 From: Daelon Suzuka Date: Sat, 28 Oct 2023 20:20:19 -0400 Subject: [PATCH] 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 --- src/logger.ts | 101 ++++++++++++++++++----------- src/lsp/ClientConnectionManager.ts | 100 +++++++++++++++++----------- src/lsp/GDScriptLanguageClient.ts | 51 ++++++++++++--- src/lsp/NativeDocumentManager.ts | 2 +- src/utils.ts | 6 +- 5 files changed, 172 insertions(+), 88 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 25bfe6d..7d2f457 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -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); diff --git a/src/lsp/ClientConnectionManager.ts b/src/lsp/ClientConnectionManager.ts index 95d9c32..ed97062 100644 --- a/src/lsp/ClientConnectionManager.ts +++ b/src/lsp/ClientConnectionManager.ts @@ -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(); diff --git a/src/lsp/GDScriptLanguageClient.ts b/src/lsp/GDScriptLanguageClient.ts index 40ee2b1..cc5e38e 100644 --- a/src/lsp/GDScriptLanguageClient.ts +++ b/src/lsp/GDScriptLanguageClient.ts @@ -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; } } diff --git a/src/lsp/NativeDocumentManager.ts b/src/lsp/NativeDocumentManager.ts index dfd12f6..a37a9c9 100644 --- a/src/lsp/NativeDocumentManager.ts +++ b/src/lsp/NativeDocumentManager.ts @@ -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 } = {}; diff --git a/src/utils.ts b/src/utils.ts index d0b68a3..35aa71c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 {