Implement headless LSP mode (#488)

* 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>
This commit is contained in:
Ryan Brue
2023-10-10 22:05:22 -05:00
committed by GitHub
parent 6a9f408d4e
commit f4e4b9c422
14 changed files with 1075 additions and 461 deletions

538
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
"author": "The Godot Engine community", "author": "The Godot Engine community",
"publisher": "geequlim", "publisher": "geequlim",
"engines": { "engines": {
"vscode": "^1.68.0" "vscode": "^1.80.0"
}, },
"categories": [ "categories": [
"Programming Languages", "Programming Languages",
@@ -26,9 +26,6 @@
], ],
"activationEvents": [ "activationEvents": [
"workspaceContains:project.godot", "workspaceContains:project.godot",
"onLanguage:gdscript",
"onLanguage:gdshader",
"onLanguage:gdresource",
"onDebugResolve:godot" "onDebugResolve:godot"
], ],
"main": "./out/extension.js", "main": "./out/extension.js",
@@ -49,6 +46,14 @@
"command": "godotTools.openEditor", "command": "godotTools.openEditor",
"title": "Godot Tools: Open workspace with Godot editor" "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", "command": "godotTools.runProject",
"title": "Godot Tools: Run workspace as Godot project" "title": "Godot Tools: Run workspace as Godot project"
@@ -184,10 +189,20 @@
"default": 6008, "default": 6008,
"description": "The server port of the GDScript language server" "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", "type": "string",
"default": "", "default": "godot3",
"description": "The absolute path to the Godot editor executable" "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": { "godotTools.sceneFileConfig": {
"type": "string", "type": "string",
@@ -546,7 +561,7 @@
"editor/context": [ "editor/context": [
{ {
"command": "godotTools.openTypeDocumentation", "command": "godotTools.openTypeDocumentation",
"when": "godotTools.context.connectedToEditor", "when": "godotTools.context.connectedToLSP && godotTools.context.typeFound",
"group": "navigation@9" "group": "navigation@9"
}, },
{ {
@@ -560,15 +575,15 @@
"devDependencies": { "devDependencies": {
"@types/marked": "^0.6.5", "@types/marked": "^0.6.5",
"@types/mocha": "^9.1.0", "@types/mocha": "^9.1.0",
"@types/node": "^10.12.21", "@types/node": "^18.15.0",
"@types/prismjs": "^1.16.8", "@types/prismjs": "^1.16.8",
"@types/vscode": "^1.68.0", "@types/vscode": "^1.80.0",
"@types/ws": "^8.2.2", "@types/ws": "^8.2.2",
"@vscode/vsce": "^2.21.0",
"esbuild": "^0.15.2", "esbuild": "^0.15.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tslint": "^5.20.1", "tslint": "^5.20.1",
"typescript": "^3.5.1", "typescript": "^5.2.2"
"vsce": "^2.10.0"
}, },
"dependencies": { "dependencies": {
"await-notify": "^1.0.1", "await-notify": "^1.0.1",

View File

@@ -1,5 +1,5 @@
import * as vscode from "vscode"; 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"; import { convert_resource_path_to_uri } from "./utils";
export class GDDocumentLinkProvider implements vscode.DocumentLinkProvider { export class GDDocumentLinkProvider implements vscode.DocumentLinkProvider {

View File

@@ -2,82 +2,70 @@ import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import * as vscode from "vscode"; import * as vscode from "vscode";
import { GDDocumentLinkProvider } from "./document_link_provider"; import { GDDocumentLinkProvider } from "./document_link_provider";
import GDScriptLanguageClient, { ClientStatus } from "./lsp/GDScriptLanguageClient"; import { ClientConnectionManager } from "./lsp/ClientConnectionManager";
import { ScenePreviewProvider } from "./scene_preview_provider"; 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"; const TOOL_NAME = "GodotTools";
export class GodotTools { export class GodotTools {
private reconnection_attempts = 0;
private context: vscode.ExtensionContext; private context: vscode.ExtensionContext;
private client: GDScriptLanguageClient = null;
private lspClientManager: ClientConnectionManager = null;
private linkProvider: GDDocumentLinkProvider = null; private linkProvider: GDDocumentLinkProvider = null;
private scenePreviewManager: ScenePreviewProvider = null; private scenePreviewManager: ScenePreviewProvider = null;
private connection_status: vscode.StatusBarItem = null;
constructor(p_context: vscode.ExtensionContext) { constructor(p_context: vscode.ExtensionContext) {
this.context = p_context; 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); this.linkProvider = new GDDocumentLinkProvider(p_context);
setInterval(() => {
this.retry_callback();
}, get_configuration("lsp.autoReconnect.cooldown", 3000));
} }
public activate() { public activate() {
vscode.commands.registerCommand("godotTools.openEditor", () => { register_command("openEditor", () => {
this.open_workspace_with_editor("-e").catch(err => vscode.window.showErrorMessage(err)); 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)); 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)); 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)); register_command("setSceneFile", this.set_scene_file.bind(this));
vscode.commands.registerCommand("godotTools.setSceneFile", this.set_scene_file.bind(this)); register_command("copyResourcePathContext", this.copy_resource_path.bind(this));
vscode.commands.registerCommand("godotTools.copyResourcePathContext", this.copy_resource_path.bind(this)); register_command("copyResourcePath", this.copy_resource_path.bind(this));
vscode.commands.registerCommand("godotTools.copyResourcePath", this.copy_resource_path.bind(this)); register_command("openTypeDocumentation", this.open_type_documentation.bind(this));
vscode.commands.registerCommand("godotTools.openTypeDocumentation", this.open_type_documentation.bind(this)); register_command("switchSceneScript", this.switch_scene_script.bind(this));
vscode.commands.registerCommand("godotTools.switchSceneScript", this.switch_scene_script.bind(this));
set_context("godotTools.context.connectedToEditor", false);
this.scenePreviewManager = new ScenePreviewProvider(); 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() { public deactivate() {
this.client.stop(); this.lspClientManager.client.stop();
} }
private open_workspace_with_editor(params = "") { private open_workspace_with_editor(params = "") {
return new Promise<void>(async (resolve, reject) => { return new Promise<void>(async (resolve, reject) => {
let valid = false; let valid = false;
let project_dir = ''; let project_dir = '';
let project_file = ''; let project_file = '';
if (vscode.workspace.workspaceFolders != undefined) { if (vscode.workspace.workspaceFolders != undefined) {
const files = await vscode.workspace.findFiles("**/project.godot"); const files = await vscode.workspace.findFiles("**/project.godot");
if (files) { if (files) {
project_file = files[0].fsPath; project_file = files[0].fsPath;
project_dir = path.dirname(project_file); project_dir = path.dirname(project_file);
let cfg = project_file; let cfg = project_file;
valid = (fs.existsSync(cfg) && fs.statSync(cfg).isFile()); valid = (fs.existsSync(cfg) && fs.statSync(cfg).isFile());
} }
} }
if (valid) { if (valid) {
this.run_editor(`--path "${project_dir}" ${params}`).then(() => resolve()).catch(err => { this.run_editor(`--path "${project_dir}" ${params}`).then(() => resolve()).catch(err => {
reject(err); reject(err);
@@ -93,11 +81,11 @@ export class GodotTools {
uri = vscode.window.activeTextEditor.document.uri; uri = vscode.window.activeTextEditor.document.uri;
} }
const project_dir = path.dirname(find_project_file(uri.fsPath)); const project_dir = path.dirname(find_project_file(uri.fsPath));
if (project_dir === null) { if (project_dir === null) {
return return;
} }
let relative_path = path.normalize(path.relative(project_dir, uri.fsPath)); let relative_path = path.normalize(path.relative(project_dir, uri.fsPath));
relative_path = relative_path.split(path.sep).join(path.posix.sep); relative_path = relative_path.split(path.sep).join(path.posix.sep);
relative_path = "res://" + relative_path; relative_path = "res://" + relative_path;
@@ -105,15 +93,8 @@ export class GodotTools {
vscode.env.clipboard.writeText(relative_path); vscode.env.clipboard.writeText(relative_path);
} }
private open_type_documentation(uri: vscode.Uri) { private open_type_documentation() {
// get word under cursor this.lspClientManager.client.open_documentation();
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 async switch_scene_script() { private async switch_scene_script() {
@@ -145,7 +126,7 @@ export class GodotTools {
} }
private run_editor(params = "") { private run_editor(params = "") {
// TODO: rewrite this entire function
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const run_godot = (path: string, params: string) => { const run_godot = (path: string, params: string) => {
const is_powershell_path = (path?: string) => { const is_powershell_path = (path?: string) => {
@@ -206,7 +187,8 @@ export class GodotTools {
resolve(); 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()) { if (!fs.existsSync(editorPath) || !fs.statSync(editorPath).isFile()) {
vscode.window.showOpenDialog({ vscode.window.showOpenDialog({
openLabel: "Run", openLabel: "Run",
@@ -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);
});
}
});
}
} }

View File

@@ -1,20 +1,21 @@
export class Logger { export class Logger {
protected buffer: string = ""; protected buffer: string = "";
protected tag: string = ''; protected tag: string = "";
protected time: boolean = false; protected time: boolean = false;
constructor(tag: string, time: boolean) { constructor(tag: string, time: boolean) {
this.tag = tag; this.tag = tag;
this.time = time; this.time = time;
} }
clear() { clear() {
this.buffer = ""; this.buffer = "";
} }
log(...messages) { log(...messages) {
let line = ''; let line = "";
if (this.tag) { if (this.tag) {
line += `[${this.tag}]`; line += `[${this.tag}]`;
} }
@@ -22,9 +23,9 @@ export class Logger {
line += `[${new Date().toISOString()}]`; line += `[${new Date().toISOString()}]`;
} }
if (line) { if (line) {
line += ' '; line += " ";
} }
for (let index = 0; index < messages.length; index++) { for (let index = 0; index < messages.length; index++) {
line += messages[index]; line += messages[index];
if (index < messages.length) { if (index < messages.length) {
@@ -33,15 +34,75 @@ export class Logger {
line += "\n"; line += "\n";
} }
} }
this.buffer += line; this.buffer += line;
console.log(line); console.log(line);
} }
get_buffer(): string { get_buffer(): string {
return this.buffer; return this.buffer;
} }
} }
const logger = new Logger('godot-tools', true); export class Logger2 {
protected tag: string = "";
protected level: string = "";
protected time: boolean = false;
constructor(tag: string) {
this.tag = tag;
}
log(...messages) {
let line = "[godotTools]";
if (this.time) {
line += `[${new Date().toISOString()}]`;
}
if (this.level) {
line += `[${this.level}]`;
this.level = "";
}
if (this.tag) {
line += `[${this.tag}]`;
}
if (line) {
line += " ";
}
for (let index = 0; index < messages.length; index++) {
line += messages[index];
if (index < messages.length) {
line += " ";
} else {
line += "\n";
}
}
console.log(line);
}
info(...messages) {
this.level = "INFO";
this.log(messages);
}
debug(...messages) {
this.level = "DEBUG";
this.log(messages);
}
warn(...messages) {
this.level = "WARNING";
this.log(messages);
}
error(...messages) {
this.level = "ERROR";
this.log(messages);
}
}
export function createLogger(tag) {
return new Logger2(tag);
}
const logger = new Logger("godot-tools", true);
export default logger; export default logger;

View File

@@ -0,0 +1,331 @@
import * as vscode from "vscode";
import * as fs from "fs";
import GDScriptLanguageClient, { ClientStatus } from "./GDScriptLanguageClient";
import {
get_configuration,
get_free_port,
get_project_version,
get_project_dir,
set_context,
register_command,
set_configuration,
} from "../utils";
import { createLogger } from "../logger";
import { execSync } from "child_process";
import { subProcess, killSubProcesses } from '../utils/subspawn';
const log = createLogger("lsp.manager");
enum ManagerStatus {
INITIALIZING,
INITIALIZING_LSP,
PENDING,
PENDING_LSP,
DISCONNECTED,
CONNECTED,
RETRYING,
}
export class ClientConnectionManager {
private context: vscode.ExtensionContext;
public client: GDScriptLanguageClient = null;
private reconnection_attempts = 0;
private status: ManagerStatus = ManagerStatus.INITIALIZING;
private statusWidget: 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));
setInterval(() => {
this.retry_callback();
}, get_configuration("lsp.autoReconnect.cooldown"));
register_command("startLanguageServer", () => {
this.start_language_server();
this.reconnection_attempts = 0;
this.client.connect_to_server();
});
register_command("stopLanguageServer", this.stop_language_server.bind(this));
register_command("checkStatus", this.on_status_item_click.bind(this));
set_context("connectedToLSP", false);
this.statusWidget = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
this.statusWidget.command = "godotTools.checkStatus";
this.statusWidget.show();
this.update_status_widget();
this.connect_to_language_server();
}
private async connect_to_language_server() {
this.client.port = -1;
if (get_configuration("lsp.headless")) {
await this.start_language_server();
}
this.reconnection_attempts = 0;
this.client.connect_to_server();
}
private stop_language_server() {
killSubProcesses('LSP');
}
private async start_language_server() {
this.stop_language_server();
const projectDir = await get_project_dir();
if (!projectDir) {
vscode.window.showErrorMessage("Current workspace is not a Godot project");
return;
}
const projectVersion = await get_project_version();
let minimumVersion = '6';
let targetVersion = '3.6';
if (projectVersion.startsWith('4')) {
minimumVersion = '2';
targetVersion = '4.2';
}
const settingName = `editorPath.godot${projectVersion[0]}`;
const godotPath = get_configuration(settingName);
try {
const output = execSync(`${godotPath} --version`).toString().trim();
const pattern = /([34])\.([0-9]+)\.(?:[0-9]+\.)?\w+.\w+.[0-9a-f]{9}/;
const match = output.match(pattern);
if (!match) {
const message = `Cannot launch headless LSP: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
}
});
return;
}
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 => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
}
});
return;
}
if (match[2] < minimumVersion) {
const message = `Cannot launch headless LSP: Headless LSP mode is only available on version ${targetVersion} or newer, but the specified Godot executable is version ${match[0]}.`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Disable Headless LSP", "Ignore").then(item => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
} else if (item == "Disable Headless LSP") {
set_configuration("lsp.headless", false);
this.prompt_for_reload();
}
});
return;
}
} catch (e) {
const message = `Cannot launch headless LSP: ${settingName} of ${godotPath} is not a valid Godot executable`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
}
});
return;
}
this.client.port = await get_free_port();
log.info(`starting headless LSP on port ${this.client.port}`);
const headlessFlags = "--headless --no-window";
const command = `${godotPath} --path "${projectDir}" --editor ${headlessFlags} --lsp-port ${this.client.port}`;
const lspProcess = subProcess("LSP", command, { shell: true });
const lspStdout = createLogger("lsp.stdout");
lspProcess.stdout.on('data', (data) => {
const out = data.toString().trim();
if (out) {
lspStdout.debug(out);
}
});
// const lspStderr = createLogger("lsp.stderr");
// lspProcess.stderr.on('data', (data) => {
// const out = data.toString().trim();
// if (out) {
// lspStderr.debug(out);
// }
// });
lspProcess.on('close', (code) => {
log.info(`LSP process exited with code ${code}`);
});
}
private async select_godot_executable(settingName: string) {
vscode.window.showOpenDialog({
openLabel: "Select Godot executable",
filters: process.platform === "win32" ? { "Godot Editor Binary": ["exe", "EXE"] } : undefined
}).then(async (uris: vscode.Uri[]) => {
if (!uris) {
return;
}
const path = uris[0].fsPath;
set_configuration(settingName, path);
this.prompt_for_reload();
});
}
private async prompt_for_reload() {
const message = `Reload VSCode to apply settings`;
vscode.window.showErrorMessage(message, "Reload").then(item => {
if (item == "Reload") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
}
private get_lsp_connection_string() {
let host = get_configuration("lsp.serverHost");
let port = get_configuration("lsp.serverPort");
if (this.client.port !== -1) {
port = this.client.port;
}
return `${host}:${port}`;
}
private on_status_item_click() {
const lsp_target = 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:
// vscode.window.showInformationMessage("Initializing extension");
break;
case ManagerStatus.INITIALIZING_LSP:
// vscode.window.showInformationMessage("Initializing LSP");
break;
case ManagerStatus.PENDING:
// vscode.window.showInformationMessage(`Connecting to the GDScript language server at ${lsp_target}`);
break;
case ManagerStatus.CONNECTED:
// vscode.window.showInformationMessage("Connected to the GDScript language server.");
break;
case ManagerStatus.DISCONNECTED:
this.retry_connect_client();
break;
case ManagerStatus.RETRYING:
break;
}
}
private update_status_widget() {
const lsp_target = this.get_lsp_connection_string();
switch (this.status) {
case ManagerStatus.INITIALIZING:
// this.statusWidget.text = `INITIALIZING`;
this.statusWidget.text = `$(sync~spin) Initializing`;
this.statusWidget.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}`;
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}`;
break;
case ManagerStatus.CONNECTED:
// this.statusWidget.text = `CONNECTED`;
this.statusWidget.text = `$(check) Connected`;
this.statusWidget.tooltip = `Connected to the GDScript language server.`;
break;
case ManagerStatus.DISCONNECTED:
// this.statusWidget.text = `DISCONNECTED`;
this.statusWidget.text = `$(x) Disconnected`;
this.statusWidget.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}`;
break;
}
}
private on_client_status_changed(status: ClientStatus) {
switch (status) {
case ClientStatus.PENDING:
this.status = ManagerStatus.PENDING;
break;
case ClientStatus.CONNECTED:
this.retry = false;
set_context("connectedToLSP", true);
this.status = ManagerStatus.CONNECTED;
if (!this.client.started) {
this.context.subscriptions.push(this.client.start());
}
break;
case ClientStatus.DISCONNECTED:
set_context("connectedToLSP", false);
if (this.retry) {
if (this.client.port != -1) {
this.status = ManagerStatus.INITIALIZING_LSP;
} else {
this.status = ManagerStatus.RETRYING;
}
} else {
this.status = ManagerStatus.DISCONNECTED;
}
this.retry = true;
break;
default:
break;
}
this.update_status_widget();
}
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");
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();
this.retry = true;
return;
}
this.retry = false;
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?`;
vscode.window.showErrorMessage(message, "Retry", "Ignore").then(item => {
if (item == "Retry") {
this.connect_to_language_server();
}
});
}
}

View File

@@ -1,11 +1,13 @@
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { LanguageClient, RequestMessage } from "vscode-languageclient/node"; import { LanguageClient, RequestMessage, ResponseMessage } from "vscode-languageclient/node";
import logger from "../logger"; import { createLogger } from "../logger";
import { get_configuration, is_debug_mode } 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");
export enum ClientStatus { export enum ClientStatus {
PENDING, PENDING,
DISCONNECTED, DISCONNECTED,
@@ -15,19 +17,23 @@ 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", "tcp") == "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;
private _started : boolean = false; private _started: boolean = false;
private _status : ClientStatus; private _status: ClientStatus;
private _status_changed_callbacks: ((v : ClientStatus)=>void)[] = []; private _status_changed_callbacks: ((v: ClientStatus) => void)[] = [];
private _initialize_request: Message = null; private _initialize_request: Message = null;
private message_handler: MessageHandler = null; private message_handler: MessageHandler = null;
private native_doc_manager: NativeDocumentManager = null; private native_doc_manager: NativeDocumentManager = null;
public get started() : boolean { return this._started; } public port: number = -1;
public get status() : ClientStatus { return this._status; } public sentMessages = new Map();
public set status(v : ClientStatus) { public lastSymbolHovered: string = "";
public get started(): boolean { return this._started; }
public get status(): ClientStatus { return this._status; }
public set status(v: ClientStatus) {
if (this._status != v) { if (this._status != v) {
this._status = v; this._status = v;
for (const callback of this._status_changed_callbacks) { for (const callback of this._status_changed_callbacks) {
@@ -36,14 +42,15 @@ export default class GDScriptLanguageClient extends LanguageClient {
} }
} }
public watch_status(callback: (v : ClientStatus)=>void) { public watch_status(callback: (v: ClientStatus) => void) {
if (this._status_changed_callbacks.indexOf(callback) == -1) { if (this._status_changed_callbacks.indexOf(callback) == -1) {
this._status_changed_callbacks.push(callback); this._status_changed_callbacks.push(callback);
} }
} }
public open_documentation(symbolName: string) { public open_documentation() {
this.native_doc_manager.request_documentation(symbolName); const symbol = this.lastSymbolHovered;
this.native_doc_manager.request_documentation(symbol);
} }
constructor(context: vscode.ExtensionContext) { constructor(context: vscode.ExtensionContext) {
@@ -51,7 +58,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
`GDScriptLanguageClient`, `GDScriptLanguageClient`,
() => { () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
resolve({reader: new MessageIOReader(this.io), writer: new MessageIOWriter(this.io)}); resolve({ reader: new MessageIOReader(this.io), writer: new MessageIOWriter(this.io) });
}); });
}, },
{ {
@@ -78,45 +85,91 @@ export default class GDScriptLanguageClient extends LanguageClient {
connect_to_server() { connect_to_server() {
this.status = ClientStatus.PENDING; this.status = ClientStatus.PENDING;
let host = get_configuration("lsp.serverHost", "127.0.0.1"); const host = get_configuration("lsp.serverHost");
let port = get_configuration("lsp.serverPort", 6008); let port = get_configuration("lsp.serverPort");
if (this.port !== -1) {
port = this.port;
}
log.info(`attempting to connect to LSP at port ${port}`);
this.io.connect_to_language_server(host, port); this.io.connect_to_language_server(host, port);
} }
start(): vscode.Disposable { start() {
this._started = true; this._started = true;
return super.start(); return super.start();
} }
private on_send_message(message: Message) { private on_send_message(message: RequestMessage) {
if (is_debug_mode()) { log.debug("tx: " + JSON.stringify(message));
logger.log("[client]", JSON.stringify(message));
} this.sentMessages.set(message.id, message.method);
if ((message as RequestMessage).method == "initialize") {
if (message.method == "initialize") {
this._initialize_request = message; this._initialize_request = message;
} }
} }
private on_message(message: Message) { private on_message(message: ResponseMessage) {
if (is_debug_mode()) { const msgString = JSON.stringify(message);
logger.log("[server]", JSON.stringify(message)); log.debug("rx: " + msgString);
}
// 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
// This should be forward-compatible, meaning that it will work // This should be forward-compatible, meaning that it will work
// with the current broken version, AND the fixed future version. // with the current broken version, AND the fixed future version.
const match = JSON.stringify(message).match(/"target":"file:\/\/[^\/][^"]*"/); const match = msgString.match(/"target":"file:\/\/[^\/][^"]*"/);
if (match) { if (match) {
for (let i = 0; i < message["result"].length; i++) { const count = (message["result"] as Array<object>).length;
const x = message["result"][i]["target"]; for (let i = 0; i < count; i++) {
const x: string = message["result"][i]["target"];
message["result"][i]["target"] = x.replace('file://', 'file:///'); message["result"][i]["target"] = x.replace('file://', 'file:///');
} }
} }
const method = this.sentMessages.get(message.id);
if (method === "textDocument/hover") {
this.handle_hover_response(message);
// this is a dirty hack to fix language server sending us prerendered
// markdown but not correctly stripping leading #'s, leading to
// docstrings being displayed as titles
const value: string = message.result["contents"].value;
message.result["contents"].value = value.replace(/\n[#]+/g, '\n');
}
this.message_handler.on_message(message); this.message_handler.on_message(message);
} }
private handle_hover_response(message: ResponseMessage) {
this.lastSymbolHovered = "";
set_context("typeFound", false);
let decl: string = message.result["contents"].value;
decl = decl.split('\n')[0].trim();
// strip off the value
if (decl.includes("=")) {
decl = decl.split("=")[0];
}
if (decl.includes(":")) {
const parts = decl.split(":");
if (parts.length === 2) {
decl = parts[1].trim();
}
}
if (decl.includes("<Native>")) {
decl = decl.split(" ")[2];
}
if (decl.includes(" ")) {
return;
}
this.lastSymbolHovered = decl;
set_context("typeFound", true);
}
private on_connected() { private on_connected() {
if (this._initialize_request) { if (this._initialize_request) {
this.io.writer.write(this._initialize_request); this.io.writer.write(this._initialize_request);
@@ -129,10 +182,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
} }
} }
class MessageHandler extends EventEmitter { class MessageHandler extends EventEmitter {
private io: MessageIO = null; private io: MessageIO = null;
constructor(io: MessageIO) { constructor(io: MessageIO) {
@@ -140,8 +190,8 @@ class MessageHandler extends EventEmitter {
this.io = io; this.io = io;
} }
changeWorkspace(params: {path: string}) { changeWorkspace(params: { path: string }) {
vscode.window.showErrorMessage("The GDScript language server can't work properly!\nThe open workspace is different from the editor's.", 'Reload', 'Ignore').then(item=>{ vscode.window.showErrorMessage("The GDScript language server can't work properly!\nThe open workspace is different from the editor's.", 'Reload', 'Ignore').then(item => {
if (item == "Reload") { if (item == "Reload") {
let folderUrl = vscode.Uri.file(params.path); let folderUrl = vscode.Uri.file(params.path);
vscode.commands.executeCommand('vscode.openFolder', folderUrl, false); vscode.commands.executeCommand('vscode.openFolder', folderUrl, false);
@@ -150,7 +200,6 @@ class MessageHandler extends EventEmitter {
} }
on_message(message: any) { on_message(message: any) {
// FIXME: Hot fix VSCode 1.42 hover position // FIXME: Hot fix VSCode 1.42 hover position
if (message && message.result && message.result.range && message.result.contents) { if (message && message.result && message.result.range && message.result.contents) {
message.result.range = undefined; message.result.range = undefined;

View File

@@ -10,12 +10,12 @@ const CRLF: string = '\r\n';
export default class MessageBuffer { export default class MessageBuffer {
private encoding: string; private encoding: BufferEncoding;
private index: number; private index: number;
private buffer: Buffer; private buffer: Buffer;
constructor(encoding: string = 'utf8') { constructor(encoding: string = 'utf8') {
this.encoding = encoding; this.encoding = encoding as BufferEncoding;
this.index = 0; this.index = 0;
this.buffer = Buffer.allocUnsafe(DefaultSize); this.buffer = Buffer.allocUnsafe(DefaultSize);
} }

View File

@@ -108,7 +108,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
private buffer: MessageBuffer; private buffer: MessageBuffer;
private nextMessageLength: number; private nextMessageLength: number;
private messageToken: number; private messageToken: number;
private partialMessageTimer: NodeJS.Timer | undefined; private partialMessageTimer: NodeJS.Timeout | undefined;
private _partialMessageTimeout: number; private _partialMessageTimeout: number;
public constructor(io: MessageIO, encoding: string = 'utf8') { public constructor(io: MessageIO, encoding: string = 'utf8') {
@@ -204,14 +204,14 @@ const CRLF = '\r\n';
export class MessageIOWriter extends AbstractMessageWriter implements MessageWriter { export class MessageIOWriter extends AbstractMessageWriter implements MessageWriter {
private io: MessageIO; private io: MessageIO;
private encoding: string; private encoding: BufferEncoding;
private errorCount: number; private errorCount: number;
public constructor(io: MessageIO, encoding: string = 'utf8') { public constructor(io: MessageIO, encoding: string = 'utf8') {
super(); super();
this.io = io; this.io = io;
this.io.writer = this; this.io.writer = this;
this.encoding = encoding; this.encoding = encoding as BufferEncoding;
this.errorCount = 0; this.errorCount = 0;
this.io.on('error', (error: any) => this.fireError(error)); this.io.on('error', (error: any) => this.fireError(error));
this.io.on('close', () => this.fireClose()); this.io.on('close', () => this.fireClose());

View File

@@ -5,7 +5,7 @@ import { MessageIO } from "./MessageIO";
import { NotificationMessage } from "vscode-jsonrpc"; import { NotificationMessage } from "vscode-jsonrpc";
import * as Prism from "../deps/prism/prism"; import * as Prism from "../deps/prism/prism";
import * as marked from "marked"; import * as marked from "marked";
import { get_configuration } from "../utils"; import { get_configuration, register_command } from "../utils";
import { import {
Methods, Methods,
NativeSymbolInspectParams, NativeSymbolInspectParams,
@@ -13,6 +13,7 @@ import {
GodotNativeClassInfo, GodotNativeClassInfo,
GodotCapabilities, GodotCapabilities,
} from "./gdscript.capabilities"; } from "./gdscript.capabilities";
marked.setOptions({ marked.setOptions({
highlight: function (code, lang) { highlight: function (code, lang) {
return Prism.highlight(code, GDScriptGrammar, lang); return Prism.highlight(code, GDScriptGrammar, lang);
@@ -52,10 +53,7 @@ export default class NativeDocumentManager extends EventEmitter {
} }
}); });
vscode.commands.registerCommand( register_command("listNativeClasses", this.list_native_classes.bind(this));
"godotTools.listNativeClasses",
this.list_native_classes.bind(this)
);
} }
public request_documentation(symbolName: string) { public request_documentation(symbolName: string) {
@@ -85,7 +83,7 @@ export default class NativeDocumentManager extends EventEmitter {
private inspect_native_symbol(params: NativeSymbolInspectParams) { private inspect_native_symbol(params: NativeSymbolInspectParams) {
let json_data = ""; let json_data = "";
if (get_configuration("lsp.serverProtocol", "tcp") == "ws") { if (get_configuration("lsp.serverProtocol") == "ws") {
json_data = JSON.stringify({ json_data = JSON.stringify({
id: -1, id: -1,
jsonrpc: "2.0", jsonrpc: "2.0",
@@ -129,7 +127,7 @@ export default class NativeDocumentManager extends EventEmitter {
* configuration and previously opened native symbols. * configuration and previously opened native symbols.
*/ */
private get_new_native_symbol_column(): vscode.ViewColumn { private get_new_native_symbol_column(): vscode.ViewColumn {
const config_placement = get_configuration("nativeSymbolPlacement", "beside"); const config_placement = get_configuration("nativeSymbolPlacement");
if (config_placement == "active") { if (config_placement == "active") {
return vscode.ViewColumn.Active; return vscode.ViewColumn.Active;
@@ -297,8 +295,7 @@ export default class NativeDocumentManager extends EventEmitter {
); );
const title = element( const title = element(
"p", "p",
`${ `${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""
with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""
}${s.name}( ${args} )` }${s.name}( ${args} )`
); );
const doc = element( const doc = element(
@@ -439,9 +436,8 @@ function element<K extends keyof HTMLElementTagNameMap>(
props_str += ` ${key}="${props[key]}"`; props_str += ` ${key}="${props[key]}"`;
} }
} }
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${ return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""
new_line ? "\n" : "" }`;
}`;
} }
function make_link(classname: string, symbol: string) { function make_link(classname: string, symbol: string) {
if (!symbol || symbol == classname) { if (!symbol || symbol == classname) {

View File

@@ -10,12 +10,16 @@ import {
import path = require("path"); import path = require("path");
import fs = require("fs"); import fs = require("fs");
import * as vscode from "vscode"; import * as vscode from "vscode";
import { get_configuration, set_configuration, find_file, set_context, convert_resource_path_to_uri } from "./utils"; import {
import logger from "./logger"; get_configuration,
find_file,
set_context,
convert_resource_path_to_uri,
register_command,
} from "./utils";
import { createLogger } from "./logger";
function log(...messages) { const log = createLogger("scene preview");
logger.log("[scene preview]", messages);
}
export class ScenePreviewProvider implements TreeDataProvider<SceneNode> { export class ScenePreviewProvider implements TreeDataProvider<SceneNode> {
private root: SceneNode | undefined; private root: SceneNode | undefined;
@@ -84,15 +88,15 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode> {
this.tree.onDidChangeSelection(this.tree_selection_changed); this.tree.onDidChangeSelection(this.tree_selection_changed);
vscode.commands.registerCommand("godotTools.scenePreview.pin", this.pin_preview.bind(this)); register_command("scenePreview.pin", this.pin_preview.bind(this));
vscode.commands.registerCommand("godotTools.scenePreview.unpin", this.unpin_preview.bind(this)); register_command("scenePreview.unpin", this.unpin_preview.bind(this));
vscode.commands.registerCommand("godotTools.scenePreview.copyNodePath", this.copy_node_path.bind(this)); register_command("scenePreview.copyNodePath", this.copy_node_path.bind(this));
vscode.commands.registerCommand("godotTools.scenePreview.copyResourcePath", this.copy_resource_path.bind(this)); register_command("scenePreview.copyResourcePath", this.copy_resource_path.bind(this));
vscode.commands.registerCommand("godotTools.scenePreview.openScene", this.open_scene.bind(this)); register_command("scenePreview.openScene", this.open_scene.bind(this));
vscode.commands.registerCommand("godotTools.scenePreview.openScript", this.open_script.bind(this)); register_command("scenePreview.openScript", this.open_script.bind(this));
vscode.commands.registerCommand("godotTools.scenePreview.goToDefinition", this.go_to_definition.bind(this)); register_command("scenePreview.goToDefinition", this.go_to_definition.bind(this));
vscode.commands.registerCommand("godotTools.scenePreview.refresh", () => register_command("scenePreview.refresh", () =>
this.refresh() this.refresh()
); );

View File

@@ -1,49 +1,106 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import * as fs from "fs";
import { AddressInfo, createServer } from "net";
const CONFIG_CONTAINER = "godotTools"; const EXTENSION_PREFIX = "godotTools";
export function get_configuration(name: string, default_value: any = null) { const config = vscode.workspace.getConfiguration(EXTENSION_PREFIX);
let config_value = vscode.workspace.getConfiguration(CONFIG_CONTAINER).get(name, null);
if (config_value === null) { export function get_configuration(name: string, default_value?: any) {
let config_value = config.get(name, null);
if (default_value && config_value === null) {
return default_value; return default_value;
} }
return config_value; return config_value;
} }
export function set_configuration(name: string, value: any) { export function set_configuration(name: string, value: any) {
return vscode.workspace.getConfiguration(CONFIG_CONTAINER).update(name, value); return config.update(name, value);
} }
export function is_debug_mode(): boolean { export function is_debug_mode(): boolean {
return process.env.VSCODE_DEBUG_MODE === "true"; return process.env.VSCODE_DEBUG_MODE === "true";
} }
const CONTEXT_PREFIX = `${EXTENSION_PREFIX}.context.`;
export function set_context(name: string, value: any) { export function set_context(name: string, value: any) {
vscode.commands.executeCommand("setContext", name, value); return vscode.commands.executeCommand("setContext", CONTEXT_PREFIX + name, value);
} }
export function find_project_file(start: string, depth:number=20) { export function register_command(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
// This function appears to be fast enough, but if speed is ever an issue, return vscode.commands.registerCommand(`${EXTENSION_PREFIX}.${command}`, callback);
// memoizing the result should be straightforward
const folder = path.dirname(start);
if (start == folder) {
return null;
}
const project_file = path.join(folder, "project.godot");
if (fs.existsSync(project_file)) {
return project_file;
} else {
if (depth === 0) {
return null;
}
return find_project_file(folder, depth - 1);
}
} }
export async function find_file(file: string): Promise<vscode.Uri|null> { export function get_word_under_cursor(): string {
const activeEditor = vscode.window.activeTextEditor;
const document = activeEditor.document;
const curPos = activeEditor.selection.active;
const wordRange = document.getWordRangeAtPosition(curPos);
const symbolName = document.getText(wordRange);
return symbolName;
}
export async function get_project_version(): Promise<string | undefined> {
const project_dir = await get_project_dir();
if (!project_dir) {
return undefined;
}
let godot_version = '3.x';
const project_file = vscode.Uri.file(path.join(project_dir, 'project.godot'));
const document = await vscode.workspace.openTextDocument(project_file);
const text = document.getText();
const match = text.match(/config\/features=PackedStringArray\((.*)\)/);
if (match) {
const line = match[0];
const version = line.match(/\"(4.[0-9]+)\"/);
if (version) {
godot_version = version[1];
}
}
return godot_version;
}
export async function get_project_dir() {
let project_dir = undefined;
let project_file = '';
if (vscode.workspace.workspaceFolders != undefined) {
const files = await vscode.workspace.findFiles("**/project.godot");
if (files) {
project_file = files[0].fsPath;
if (fs.existsSync(project_file) && fs.statSync(project_file).isFile()) {
project_dir = path.dirname(project_file);
}
}
}
return project_dir;
}
export function find_project_file(start: string, depth: number = 20) {
// TODO: rename this, it's actually more like "find_parent_project_file"
// This function appears to be fast enough, but if speed is ever an issue,
// memoizing the result should be straightforward
const folder = path.dirname(start);
if (start == folder) {
return null;
}
const project_file = path.join(folder, "project.godot");
if (fs.existsSync(project_file) && fs.statSync(project_file).isFile()) {
return project_file;
} else {
if (depth === 0) {
return null;
}
return find_project_file(folder, depth - 1);
}
}
export async function find_file(file: string): Promise<vscode.Uri | null> {
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
return vscode.Uri.file(file); return vscode.Uri.file(file);
} else { } else {
@@ -56,7 +113,7 @@ export async function find_file(file: string): Promise<vscode.Uri|null> {
return null; return null;
} }
export async function convert_resource_path_to_uri(resPath: string): Promise<vscode.Uri|null> { export async function convert_resource_path_to_uri(resPath: string): Promise<vscode.Uri | null> {
const files = await vscode.workspace.findFiles("**/project.godot"); const files = await vscode.workspace.findFiles("**/project.godot");
if (!files) { if (!files) {
return null; return null;
@@ -64,3 +121,13 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
const project_dir = files[0].fsPath.replace("project.godot", ""); const project_dir = files[0].fsPath.replace("project.godot", "");
return vscode.Uri.joinPath(vscode.Uri.file(project_dir), resPath.substring(6)); return vscode.Uri.joinPath(vscode.Uri.file(project_dir), resPath.substring(6));
} }
export async function get_free_port(): Promise<number> {
return new Promise(res => {
const srv = createServer();
srv.listen(0, () => {
const port = (srv.address() as AddressInfo).port;
srv.close((err) => res(port));
});
});
}

52
src/utils/subspawn.ts Normal file
View File

@@ -0,0 +1,52 @@
/*
Copied from https://github.com/craigwardman/subspawn
Original library copyright (c) 2022 Craig Wardman
I had to vendor this library to fix the API in a couple places.
*/
import { ChildProcess, execSync, spawn, SpawnOptions } from 'child_process';
interface DictionaryOfStringChildProcessArray {
[key: string]: ChildProcess[];
}
const children: DictionaryOfStringChildProcessArray = {};
export function killSubProcesses(owner: string) {
if (!(owner in children)) {
return;
}
children[owner].forEach((c) => {
try {
if (c.pid) {
if (process.platform === 'win32') {
execSync(`taskkill /pid ${c.pid} /T /F`);
} else {
process.kill(-c.pid);
}
}
} catch { }
});
}
process.on('exit', () => {
Object.keys(children).forEach((owner) => killSubProcesses(owner));
});
function gracefulExitHandler() {
process.exit();
}
process.on('SIGINT', gracefulExitHandler);
process.on('SIGTERM', gracefulExitHandler);
process.on('SIGQUIT', gracefulExitHandler);
export function subProcess(owner: string, command: string, options?: SpawnOptions) {
const childProcess = spawn(command, options);
children[owner] = children[owner] || [];
children[owner].push(childProcess);
return childProcess;
}

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es6", "target": "es2020",
"outDir": "out", "outDir": "out",
"lib": [ "lib": [
"es2020", "es2020",