Skip to content

Commit

Permalink
Implement headless LSP mode (#488)
Browse files Browse the repository at this point in the history
* adds new Headless LSP mode
* refactor and simplify LSP client control flow into new `ClientConnectionManager` class
* adds new setting: `godotTools.lsp.headless`, disabled by default
* split `godotTools.editorPath` into `godotTools.editorPath.godot3` and `.godot4`
* fix #373, broken formatting in hovers
* improve right click -> open docs to work on type-annotated variables

---------

Co-authored-by: David Kincaid <daelonsuzuka@gmail.com>
  • Loading branch information
ryanabx and DaelonSuzuka authored Oct 11, 2023
1 parent 6a9f408 commit f4e4b9c
Show file tree
Hide file tree
Showing 14 changed files with 1,075 additions and 461 deletions.
538 changes: 343 additions & 195 deletions package-lock.json

Large diffs are not rendered by default.

39 changes: 27 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"author": "The Godot Engine community",
"publisher": "geequlim",
"engines": {
"vscode": "^1.68.0"
"vscode": "^1.80.0"
},
"categories": [
"Programming Languages",
Expand All @@ -26,9 +26,6 @@
],
"activationEvents": [
"workspaceContains:project.godot",
"onLanguage:gdscript",
"onLanguage:gdshader",
"onLanguage:gdresource",
"onDebugResolve:godot"
],
"main": "./out/extension.js",
Expand All @@ -49,6 +46,14 @@
"command": "godotTools.openEditor",
"title": "Godot Tools: Open workspace with Godot editor"
},
{
"command": "godotTools.startLanguageServer",
"title": "Godot Tools: Start the GDScript Language Server for this workspace"
},
{
"command": "godotTools.stopLanguageServer",
"title": "Godot Tools: Stop the GDScript Language Server for this workspace"
},
{
"command": "godotTools.runProject",
"title": "Godot Tools: Run workspace as Godot project"
Expand Down Expand Up @@ -184,10 +189,20 @@
"default": 6008,
"description": "The server port of the GDScript language server"
},
"godotTools.editorPath": {
"godotTools.lsp.headless": {
"type": "boolean",
"default": false,
"description": "Whether to launch the LSP as a headless child process"
},
"godotTools.editorPath.godot3": {
"type": "string",
"default": "",
"description": "The absolute path to the Godot editor executable"
"default": "godot3",
"description": "The absolute path to the Godot 3 editor executable"
},
"godotTools.editorPath.godot4": {
"type": "string",
"default": "godot4",
"description": "The absolute path to the Godot 4 editor executable"
},
"godotTools.sceneFileConfig": {
"type": "string",
Expand Down Expand Up @@ -546,7 +561,7 @@
"editor/context": [
{
"command": "godotTools.openTypeDocumentation",
"when": "godotTools.context.connectedToEditor",
"when": "godotTools.context.connectedToLSP && godotTools.context.typeFound",
"group": "navigation@9"
},
{
Expand All @@ -560,15 +575,15 @@
"devDependencies": {
"@types/marked": "^0.6.5",
"@types/mocha": "^9.1.0",
"@types/node": "^10.12.21",
"@types/node": "^18.15.0",
"@types/prismjs": "^1.16.8",
"@types/vscode": "^1.68.0",
"@types/vscode": "^1.80.0",
"@types/ws": "^8.2.2",
"@vscode/vsce": "^2.21.0",
"esbuild": "^0.15.2",
"ts-node": "^10.9.1",
"tslint": "^5.20.1",
"typescript": "^3.5.1",
"vsce": "^2.10.0"
"typescript": "^5.2.2"
},
"dependencies": {
"await-notify": "^1.0.1",
Expand Down
2 changes: 1 addition & 1 deletion src/document_link_provider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vscode from "vscode";
import { Uri, Position, Range, TextDocument } from "vscode";
import { Uri, Position, Range } from "vscode";
import { convert_resource_path_to_uri } from "./utils";

export class GDDocumentLinkProvider implements vscode.DocumentLinkProvider {
Expand Down
193 changes: 42 additions & 151 deletions src/godot-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,70 @@ import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
import { GDDocumentLinkProvider } from "./document_link_provider";
import GDScriptLanguageClient, { ClientStatus } from "./lsp/GDScriptLanguageClient";
import { ClientConnectionManager } from "./lsp/ClientConnectionManager";
import { ScenePreviewProvider } from "./scene_preview_provider";
import { get_configuration, set_configuration, find_file, set_context, find_project_file } from "./utils";
import {
get_configuration,
set_configuration,
find_file,
find_project_file,
register_command
} from "./utils";

const TOOL_NAME = "GodotTools";

export class GodotTools {
private reconnection_attempts = 0;
private context: vscode.ExtensionContext;
private client: GDScriptLanguageClient = null;

private lspClientManager: ClientConnectionManager = null;
private linkProvider: GDDocumentLinkProvider = null;
private scenePreviewManager: ScenePreviewProvider = null;

private connection_status: vscode.StatusBarItem = null;

constructor(p_context: vscode.ExtensionContext) {
this.context = p_context;
this.client = new GDScriptLanguageClient(p_context);
this.client.watch_status(this.on_client_status_changed.bind(this));
this.connection_status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);

this.lspClientManager = new ClientConnectionManager(p_context);
this.linkProvider = new GDDocumentLinkProvider(p_context);

setInterval(() => {
this.retry_callback();
}, get_configuration("lsp.autoReconnect.cooldown", 3000));
}

public activate() {
vscode.commands.registerCommand("godotTools.openEditor", () => {
register_command("openEditor", () => {
this.open_workspace_with_editor("-e").catch(err => vscode.window.showErrorMessage(err));
});
vscode.commands.registerCommand("godotTools.runProject", () => {
register_command("runProject", () => {
this.open_workspace_with_editor().catch(err => vscode.window.showErrorMessage(err));
});
vscode.commands.registerCommand("godotTools.runProjectDebug", () => {
register_command("runProjectDebug", () => {
this.open_workspace_with_editor("--debug-collisions --debug-navigation").catch(err => vscode.window.showErrorMessage(err));
});
vscode.commands.registerCommand("godotTools.checkStatus", this.check_client_status.bind(this));
vscode.commands.registerCommand("godotTools.setSceneFile", this.set_scene_file.bind(this));
vscode.commands.registerCommand("godotTools.copyResourcePathContext", this.copy_resource_path.bind(this));
vscode.commands.registerCommand("godotTools.copyResourcePath", this.copy_resource_path.bind(this));
vscode.commands.registerCommand("godotTools.openTypeDocumentation", this.open_type_documentation.bind(this));
vscode.commands.registerCommand("godotTools.switchSceneScript", this.switch_scene_script.bind(this));

set_context("godotTools.context.connectedToEditor", false);
register_command("setSceneFile", this.set_scene_file.bind(this));
register_command("copyResourcePathContext", this.copy_resource_path.bind(this));
register_command("copyResourcePath", this.copy_resource_path.bind(this));
register_command("openTypeDocumentation", this.open_type_documentation.bind(this));
register_command("switchSceneScript", this.switch_scene_script.bind(this));

this.scenePreviewManager = new ScenePreviewProvider();

this.connection_status.text = "$(sync) Initializing";
this.connection_status.command = "godotTools.checkStatus";
this.connection_status.show();

this.reconnection_attempts = 0;
this.client.connect_to_server();
}

public deactivate() {
this.client.stop();
this.lspClientManager.client.stop();
}

private open_workspace_with_editor(params = "") {
return new Promise<void>(async (resolve, reject) => {
let valid = false;
let project_dir = '';
let project_file = '';
if (vscode.workspace.workspaceFolders != undefined) {
const files = await vscode.workspace.findFiles("**/project.godot");
if (files) {
project_file = files[0].fsPath;
project_dir = path.dirname(project_file);
let cfg = project_file;
valid = (fs.existsSync(cfg) && fs.statSync(cfg).isFile());
}
}
let project_dir = '';
let project_file = '';

if (vscode.workspace.workspaceFolders != undefined) {
const files = await vscode.workspace.findFiles("**/project.godot");
if (files) {
project_file = files[0].fsPath;
project_dir = path.dirname(project_file);
let cfg = project_file;
valid = (fs.existsSync(cfg) && fs.statSync(cfg).isFile());
}
}
if (valid) {
this.run_editor(`--path "${project_dir}" ${params}`).then(() => resolve()).catch(err => {
reject(err);
Expand All @@ -93,27 +81,20 @@ export class GodotTools {
uri = vscode.window.activeTextEditor.document.uri;
}

const project_dir = path.dirname(find_project_file(uri.fsPath));
if (project_dir === null) {
return
}
const project_dir = path.dirname(find_project_file(uri.fsPath));
if (project_dir === null) {
return;
}

let relative_path = path.normalize(path.relative(project_dir, uri.fsPath));
relative_path = relative_path.split(path.sep).join(path.posix.sep);
relative_path = "res://" + relative_path;

vscode.env.clipboard.writeText(relative_path);
}

private open_type_documentation(uri: vscode.Uri) {
// get word under cursor
const activeEditor = vscode.window.activeTextEditor;
const document = activeEditor.document;
const curPos = activeEditor.selection.active;
const wordRange = document.getWordRangeAtPosition(curPos);
const symbolName = document.getText(wordRange);

this.client.open_documentation(symbolName);
private open_type_documentation() {
this.lspClientManager.client.open_documentation();
}

private async switch_scene_script() {
Expand Down Expand Up @@ -145,7 +126,7 @@ export class GodotTools {
}

private run_editor(params = "") {

// TODO: rewrite this entire function
return new Promise<void>((resolve, reject) => {
const run_godot = (path: string, params: string) => {
const is_powershell_path = (path?: string) => {
Expand Down Expand Up @@ -206,7 +187,8 @@ export class GodotTools {
resolve();
};

let editorPath = get_configuration("editorPath", "");
// TODO: This config doesn't exist anymore
let editorPath = get_configuration("editorPath");
if (!fs.existsSync(editorPath) || !fs.statSync(editorPath).isFile()) {
vscode.window.showOpenDialog({
openLabel: "Run",
Expand All @@ -228,95 +210,4 @@ export class GodotTools {
}
});
}

private check_client_status() {
let host = get_configuration("lsp.serverPort", "localhost");
let port = get_configuration("lsp.serverHost", 6008);
switch (this.client.status) {
case ClientStatus.PENDING:
vscode.window.showInformationMessage(`Connecting to the GDScript language server at ${host}:${port}`);
break;
case ClientStatus.CONNECTED:
vscode.window.showInformationMessage("Connected to the GDScript language server.");
break;
case ClientStatus.DISCONNECTED:
this.retry_connect_client();
break;
}
}

private on_client_status_changed(status: ClientStatus) {
let host = get_configuration("lsp.serverHost", "localhost");
let port = get_configuration("lsp.serverPort", 6008);
switch (status) {
case ClientStatus.PENDING:
this.connection_status.text = `$(sync) Connecting`;
this.connection_status.tooltip = `Connecting to the GDScript language server at ${host}:${port}`;
break;
case ClientStatus.CONNECTED:
this.retry = false;
set_context("godotTools.context.connectedToEditor", true);
this.connection_status.text = `$(check) Connected`;
this.connection_status.tooltip = `Connected to the GDScript language server.`;
if (!this.client.started) {
this.context.subscriptions.push(this.client.start());
}
break;
case ClientStatus.DISCONNECTED:
if (this.retry) {
this.connection_status.text = `$(sync) Connecting ` + this.reconnection_attempts;
this.connection_status.tooltip = `Connecting to the GDScript language server...`;
} else {
set_context("godotTools.context.connectedToEditor", false);
this.connection_status.text = `$(x) Disconnected`;
this.connection_status.tooltip = `Disconnected from the GDScript language server.`;
}
this.retry = true;
break;
default:
break;
}
}

private retry = false;

private retry_callback() {
if (this.retry) {
this.retry_connect_client();
}
}

private retry_connect_client() {
const auto_retry = get_configuration("lsp.autoReconnect.enabled", true);
const max_attempts = get_configuration("lsp.autoReconnect.attempts", 10);
if (auto_retry && this.reconnection_attempts <= max_attempts) {
this.reconnection_attempts++;
this.client.connect_to_server();
this.connection_status.text = `Connecting ` + this.reconnection_attempts;
this.retry = true;
return;
}

this.retry = false;
this.connection_status.text = `$(x) Disconnected`;
this.connection_status.tooltip = `Disconnected from the GDScript language server.`;

let host = get_configuration("lsp.serverHost", "localhost");
let port = get_configuration("lsp.serverPort", 6008);
let message = `Couldn't connect to the GDScript language server at ${host}:${port}. Is the Godot editor running?`;
vscode.window.showErrorMessage(message, "Open Godot Editor", "Retry", "Ignore").then(item => {
if (item == "Retry") {
this.reconnection_attempts = 0;
this.client.connect_to_server();
} else if (item == "Open Godot Editor") {
this.client.status = ClientStatus.PENDING;
this.open_workspace_with_editor("-e").then(() => {
setTimeout(() => {
this.reconnection_attempts = 0;
this.client.connect_to_server();
}, 10 * 1000);
});
}
});
}
}
Loading

0 comments on commit f4e4b9c

Please sign in to comment.