فهرست منبع

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
Daelon Suzuka 1 سال پیش
والد
کامیت
55617fdd39
5فایلهای تغییر یافته به همراه172 افزوده شده و 88 حذف شده
  1. 63 38
      src/logger.ts
  2. 63 37
      src/lsp/ClientConnectionManager.ts
  3. 43 8
      src/lsp/GDScriptLanguageClient.ts
  4. 1 1
      src/lsp/NativeDocumentManager.ts
  5. 2 4
      src/utils.ts

+ 63 - 38
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) {
 	info(...messages) {
-		this.level = "INFO";
-		this.log(messages);
+		if (LOG_LEVEL.INFO <= this.level) {
+			this.log(LOG_LEVEL.INFO, ...messages);
+		}
 	}
 	}
 	debug(...messages) {
 	debug(...messages) {
-		this.level = "DEBUG";
-		this.log(messages);
+		if (LOG_LEVEL.DEBUG <= this.level) {
+			this.log(LOG_LEVEL.DEBUG, ...messages);
+		}
 	}
 	}
 	warn(...messages) {
 	warn(...messages) {
-		this.level = "WARNING";
-		this.log(messages);
+		if (LOG_LEVEL.WARNING <= this.level) {
+			this.log(LOG_LEVEL.WARNING, ...messages);
+		}
 	}
 	}
 	error(...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);
 const logger = new Logger("godot-tools", true);

+ 63 - 37
src/lsp/ClientConnectionManager.ts

@@ -1,6 +1,6 @@
 import * as vscode from "vscode";
 import * as vscode from "vscode";
 import * as fs from "fs";
 import * as fs from "fs";
-import GDScriptLanguageClient, { ClientStatus } from "./GDScriptLanguageClient";
+import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient";
 import {
 import {
 	get_configuration,
 	get_configuration,
 	get_free_port,
 	get_free_port,
@@ -30,11 +30,14 @@ export class ClientConnectionManager {
 	private context: vscode.ExtensionContext;
 	private context: vscode.ExtensionContext;
 	public client: GDScriptLanguageClient = null;
 	public client: GDScriptLanguageClient = null;
 
 
-	private reconnection_attempts = 0;
+	private reconnectionAttempts = 0;
 
 
+	private target: TargetLSP = TargetLSP.EDITOR;
 	private status: ManagerStatus = ManagerStatus.INITIALIZING;
 	private status: ManagerStatus = ManagerStatus.INITIALIZING;
 	private statusWidget: vscode.StatusBarItem = null;
 	private statusWidget: vscode.StatusBarItem = null;
 
 
+	private connectedVersion: string = "";
+
 	constructor(p_context: vscode.ExtensionContext) {
 	constructor(p_context: vscode.ExtensionContext) {
 		this.context = p_context;
 		this.context = p_context;
 
 
@@ -46,9 +49,11 @@ export class ClientConnectionManager {
 		}, get_configuration("lsp.autoReconnect.cooldown"));
 		}, get_configuration("lsp.autoReconnect.cooldown"));
 
 
 		register_command("startLanguageServer", () => {
 		register_command("startLanguageServer", () => {
+			// TODO: this might leave the manager in a wierd state
 			this.start_language_server();
 			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("stopLanguageServer", this.stop_language_server.bind(this));
 		register_command("checkStatus", this.on_status_item_click.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() {
 	private async connect_to_language_server() {
 		this.client.port = -1;
 		this.client.port = -1;
+		this.target = TargetLSP.EDITOR;
+		this.connectedVersion = undefined;
 
 
 		if (get_configuration("lsp.headless")) {
 		if (get_configuration("lsp.headless")) {
+			this.target = TargetLSP.HEADLESS;
 			await this.start_language_server();
 			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() {
 	private stop_language_server() {
@@ -112,7 +120,7 @@ export class ClientConnectionManager {
 				});
 				});
 				return;
 				return;
 			}
 			}
-
+			this.connectedVersion = output;
 			if (match[1] !== projectVersion[0]) {
 			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]}`;
 				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 => {
 				vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
@@ -207,7 +215,7 @@ export class ClientConnectionManager {
 	}
 	}
 
 
 	private on_status_item_click() {
 	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
 		// TODO: fill these out with the ACTIONS a user could perform in each state
 		switch (this.status) {
 		switch (this.status) {
 			case ManagerStatus.INITIALIZING:
 			case ManagerStatus.INITIALIZING:
@@ -217,11 +225,21 @@ export class ClientConnectionManager {
 				// vscode.window.showInformationMessage("Initializing LSP");
 				// vscode.window.showInformationMessage("Initializing LSP");
 				break;
 				break;
 			case ManagerStatus.PENDING:
 			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;
 				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;
 				break;
+			}
 			case ManagerStatus.DISCONNECTED:
 			case ManagerStatus.DISCONNECTED:
 				this.retry_connect_client();
 				this.retry_connect_client();
 				break;
 				break;
@@ -231,39 +249,47 @@ export class ClientConnectionManager {
 	}
 	}
 
 
 	private update_status_widget() {
 	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) {
 		switch (this.status) {
 			case ManagerStatus.INITIALIZING:
 			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;
 				break;
 			case ManagerStatus.INITIALIZING_LSP:
 			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;
 				break;
 			case ManagerStatus.PENDING:
 			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;
 				break;
 			case ManagerStatus.CONNECTED:
 			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;
 				break;
 			case ManagerStatus.DISCONNECTED:
 			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;
 				break;
 			case ManagerStatus.RETRYING:
 			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;
 				break;
 		}
 		}
+		this.statusWidget.text = text;
+		this.statusWidget.tooltip = tooltip;
 	}
 	}
 
 
 	private on_client_status_changed(status: ClientStatus) {
 	private on_client_status_changed(status: ClientStatus) {
@@ -307,11 +333,11 @@ export class ClientConnectionManager {
 	}
 	}
 
 
 	private retry_connect_client() {
 	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;
 			this.retry = true;
 			return;
 			return;
 		}
 		}
@@ -320,8 +346,8 @@ export class ClientConnectionManager {
 		this.status = ManagerStatus.DISCONNECTED;
 		this.status = ManagerStatus.DISCONNECTED;
 		this.update_status_widget();
 		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 => {
 		vscode.window.showErrorMessage(message, "Retry", "Ignore").then(item => {
 			if (item == "Retry") {
 			if (item == "Retry") {
 				this.connect_to_language_server();
 				this.connect_to_language_server();

+ 43 - 8
src/lsp/GDScriptLanguageClient.ts

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

+ 1 - 1
src/lsp/NativeDocumentManager.ts

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

+ 2 - 4
src/utils.ts

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