Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement headless LSP mode #488

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
ce923c4
Add ability to automatically run editor/lsp at startup
ryanabx Aug 25, 2023
45393b3
one small async for man, one giant leap for linter
ryanabx Aug 25, 2023
65e0474
Simplify user-facing-API and refactor internals
DaelonSuzuka Sep 18, 2023
b59cd24
Copy logger improvements from debugger branch
DaelonSuzuka Sep 21, 2023
89cf6fd
Fix mistake unpacking regex results
DaelonSuzuka Sep 21, 2023
fca070a
Move LSP client management code to its own class
DaelonSuzuka Sep 21, 2023
29736c5
Move get_free_port() to utils
DaelonSuzuka Sep 21, 2023
9024ec1
Split editorPath setting into godot3 and godot4
DaelonSuzuka Sep 21, 2023
dfc8de1
Add default for editorPath retrieval
DaelonSuzuka Sep 21, 2023
ddfef95
Random port LSP is working
DaelonSuzuka Sep 21, 2023
a977437
Rename editor context
DaelonSuzuka Sep 28, 2023
07f0c2f
Convert spaces to tabs
DaelonSuzuka Sep 28, 2023
a00084b
Add missing semicolons
DaelonSuzuka Sep 28, 2023
ca78e46
Refactor get_configuration() to make second parameter optional
DaelonSuzuka Sep 28, 2023
4cdc0e6
Add logging
DaelonSuzuka Sep 28, 2023
b0557cb
Add subspawn library
DaelonSuzuka Sep 28, 2023
146b7cd
Rebuild child process handling
DaelonSuzuka Sep 28, 2023
e8c2366
Remove logging and fix error
DaelonSuzuka Sep 28, 2023
93d315d
Fix function definitions and copyright statement
DaelonSuzuka Sep 28, 2023
7bba029
Move stopping the LSP to beginning of start function
DaelonSuzuka Sep 28, 2023
6e31c9f
Clean up initialization and add icon animation
DaelonSuzuka Sep 28, 2023
07cb7e6
Fix type error
DaelonSuzuka Sep 29, 2023
766d846
Remove redundant code paths
DaelonSuzuka Sep 29, 2023
f48ba74
Remove unused import
DaelonSuzuka Sep 29, 2023
2098bbe
Path aliases finally work
DaelonSuzuka Sep 29, 2023
bde8e3f
Clean up imports
DaelonSuzuka Sep 29, 2023
d18a03e
Fix linter errors
DaelonSuzuka Sep 29, 2023
a05c7c7
Add utils.register_command() helper
DaelonSuzuka Oct 3, 2023
42eeeaa
Begin rebuilding user workflow
DaelonSuzuka Oct 3, 2023
f009ec1
Update dependencies and fix type errors
DaelonSuzuka Oct 4, 2023
3f434d7
Start adding error messages
DaelonSuzuka Oct 4, 2023
8606721
Fixed dependencies having vulnerabilities according to npm
ryanabx Oct 4, 2023
41bde51
Revert vscode-languageclient version
DaelonSuzuka Oct 4, 2023
a9490f6
Switch to new logger style
DaelonSuzuka Oct 4, 2023
d375d7b
Disable section that doesn't work on windows
DaelonSuzuka Oct 4, 2023
16d59f5
Add todo
DaelonSuzuka Oct 6, 2023
c092103
Improve variable type detection for opening docs
DaelonSuzuka Oct 9, 2023
7a39d71
Add "get_word_under_cursor()" to utils
DaelonSuzuka Oct 9, 2023
1e07aa3
"Open documentation" context menu now works on most type-annotated va…
DaelonSuzuka Oct 9, 2023
71fe6cc
Add temp fix for bad formatting in hovers
DaelonSuzuka Oct 9, 2023
f4ef52d
Turn on lsp debugging logging
DaelonSuzuka Oct 9, 2023
831f478
Adapted hover formatting fix to godot 3
DaelonSuzuka Oct 9, 2023
5022699
Modify regex rule
DaelonSuzuka Oct 9, 2023
d7a07fe
Replace let with const
DaelonSuzuka Oct 10, 2023
5fefeb8
Fix lint errors
DaelonSuzuka Oct 10, 2023
3c0b1ab
Remove warning dialog option
DaelonSuzuka Oct 10, 2023
4983504
Remove unused setting
DaelonSuzuka Oct 10, 2023
d229cea
Rewrote headless LSP startup sequence
DaelonSuzuka Oct 10, 2023
e0469c8
Remove unused import
DaelonSuzuka Oct 10, 2023
1985069
Wrap the version check in a try/catch
DaelonSuzuka Oct 10, 2023
a7ed5c2
Remove import @alias feature because it broke packaging
DaelonSuzuka Oct 10, 2023
1a80e05
Fix linter complaint
DaelonSuzuka Oct 10, 2023
94c86ec
Change some log levels
DaelonSuzuka Oct 10, 2023
210d6c0
Improve headless LSP error handling
DaelonSuzuka Oct 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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