mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2026-01-03 01:48:11 +03:00
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:
538
package-lock.json
generated
538
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -15,7 +15,7 @@
|
||||
"author": "The Godot Engine community",
|
||||
"publisher": "geequlim",
|
||||
"engines": {
|
||||
"vscode": "^1.68.0"
|
||||
"vscode": "^1.80.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
@@ -26,9 +26,6 @@
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:project.godot",
|
||||
"onLanguage:gdscript",
|
||||
"onLanguage:gdshader",
|
||||
"onLanguage:gdresource",
|
||||
"onDebugResolve:godot"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
@@ -546,7 +561,7 @@
|
||||
"editor/context": [
|
||||
{
|
||||
"command": "godotTools.openTypeDocumentation",
|
||||
"when": "godotTools.context.connectedToEditor",
|
||||
"when": "godotTools.context.connectedToLSP && godotTools.context.typeFound",
|
||||
"group": "navigation@9"
|
||||
},
|
||||
{
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
@@ -93,11 +81,11 @@ 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;
|
||||
@@ -105,15 +93,8 @@ export class GodotTools {
|
||||
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() {
|
||||
@@ -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) => {
|
||||
@@ -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",
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
|
||||
export class Logger {
|
||||
protected buffer: string = "";
|
||||
protected tag: string = '';
|
||||
protected tag: string = "";
|
||||
protected time: boolean = false;
|
||||
|
||||
|
||||
constructor(tag: string, time: boolean) {
|
||||
this.tag = tag;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
|
||||
clear() {
|
||||
this.buffer = "";
|
||||
}
|
||||
|
||||
|
||||
log(...messages) {
|
||||
|
||||
let line = '';
|
||||
|
||||
let line = "";
|
||||
if (this.tag) {
|
||||
line += `[${this.tag}]`;
|
||||
}
|
||||
@@ -22,9 +23,9 @@ export class Logger {
|
||||
line += `[${new Date().toISOString()}]`;
|
||||
}
|
||||
if (line) {
|
||||
line += ' ';
|
||||
line += " ";
|
||||
}
|
||||
|
||||
|
||||
for (let index = 0; index < messages.length; index++) {
|
||||
line += messages[index];
|
||||
if (index < messages.length) {
|
||||
@@ -33,15 +34,75 @@ export class Logger {
|
||||
line += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.buffer += line;
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
|
||||
get_buffer(): string {
|
||||
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;
|
||||
|
||||
331
src/lsp/ClientConnectionManager.ts
Normal file
331
src/lsp/ClientConnectionManager.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { EventEmitter } from "events";
|
||||
import * as vscode from 'vscode';
|
||||
import { LanguageClient, RequestMessage } from "vscode-languageclient/node";
|
||||
import logger from "../logger";
|
||||
import { get_configuration, is_debug_mode } from "../utils";
|
||||
import { LanguageClient, RequestMessage, ResponseMessage } from "vscode-languageclient/node";
|
||||
import { createLogger } from "../logger";
|
||||
import { get_configuration, set_context } from "../utils";
|
||||
import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
|
||||
import NativeDocumentManager from './NativeDocumentManager';
|
||||
|
||||
const log = createLogger("lsp.client");
|
||||
|
||||
export enum ClientStatus {
|
||||
PENDING,
|
||||
DISCONNECTED,
|
||||
@@ -15,19 +17,23 @@ const CUSTOM_MESSAGE = "gdscrip_client/";
|
||||
|
||||
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 _started : boolean = false;
|
||||
private _status : ClientStatus;
|
||||
private _status_changed_callbacks: ((v : ClientStatus)=>void)[] = [];
|
||||
private _started: boolean = false;
|
||||
private _status: ClientStatus;
|
||||
private _status_changed_callbacks: ((v: ClientStatus) => void)[] = [];
|
||||
private _initialize_request: Message = null;
|
||||
private message_handler: MessageHandler = null;
|
||||
private native_doc_manager: NativeDocumentManager = null;
|
||||
|
||||
public get started() : boolean { return this._started; }
|
||||
public get status() : ClientStatus { return this._status; }
|
||||
public set status(v : ClientStatus) {
|
||||
public port: number = -1;
|
||||
public sentMessages = new Map();
|
||||
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) {
|
||||
this._status = v;
|
||||
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) {
|
||||
this._status_changed_callbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
public open_documentation(symbolName: string) {
|
||||
this.native_doc_manager.request_documentation(symbolName);
|
||||
public open_documentation() {
|
||||
const symbol = this.lastSymbolHovered;
|
||||
this.native_doc_manager.request_documentation(symbol);
|
||||
}
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
@@ -51,7 +58,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
`GDScriptLanguageClient`,
|
||||
() => {
|
||||
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() {
|
||||
this.status = ClientStatus.PENDING;
|
||||
let host = get_configuration("lsp.serverHost", "127.0.0.1");
|
||||
let port = get_configuration("lsp.serverPort", 6008);
|
||||
const host = get_configuration("lsp.serverHost");
|
||||
let port = get_configuration("lsp.serverPort");
|
||||
if (this.port !== -1) {
|
||||
port = this.port;
|
||||
}
|
||||
log.info(`attempting to connect to LSP at port ${port}`);
|
||||
this.io.connect_to_language_server(host, port);
|
||||
}
|
||||
|
||||
start(): vscode.Disposable {
|
||||
start() {
|
||||
this._started = true;
|
||||
return super.start();
|
||||
}
|
||||
|
||||
private on_send_message(message: Message) {
|
||||
if (is_debug_mode()) {
|
||||
logger.log("[client]", JSON.stringify(message));
|
||||
}
|
||||
if ((message as RequestMessage).method == "initialize") {
|
||||
private on_send_message(message: RequestMessage) {
|
||||
log.debug("tx: " + JSON.stringify(message));
|
||||
|
||||
this.sentMessages.set(message.id, message.method);
|
||||
|
||||
if (message.method == "initialize") {
|
||||
this._initialize_request = message;
|
||||
}
|
||||
}
|
||||
|
||||
private on_message(message: Message) {
|
||||
if (is_debug_mode()) {
|
||||
logger.log("[server]", JSON.stringify(message));
|
||||
}
|
||||
private on_message(message: ResponseMessage) {
|
||||
const msgString = JSON.stringify(message);
|
||||
log.debug("rx: " + msgString);
|
||||
|
||||
// This is a dirty hack to fix the language server sending us
|
||||
// invalid file URIs
|
||||
// This should be forward-compatible, meaning that it will work
|
||||
// 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) {
|
||||
for (let i = 0; i < message["result"].length; i++) {
|
||||
const x = message["result"][i]["target"];
|
||||
const count = (message["result"] as Array<object>).length;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const x: string = message["result"][i]["target"];
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this._initialize_request) {
|
||||
this.io.writer.write(this._initialize_request);
|
||||
@@ -129,10 +182,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class MessageHandler extends EventEmitter {
|
||||
|
||||
private io: MessageIO = null;
|
||||
|
||||
constructor(io: MessageIO) {
|
||||
@@ -140,8 +190,8 @@ class MessageHandler extends EventEmitter {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
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=>{
|
||||
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 => {
|
||||
if (item == "Reload") {
|
||||
let folderUrl = vscode.Uri.file(params.path);
|
||||
vscode.commands.executeCommand('vscode.openFolder', folderUrl, false);
|
||||
@@ -150,7 +200,6 @@ class MessageHandler extends EventEmitter {
|
||||
}
|
||||
|
||||
on_message(message: any) {
|
||||
|
||||
// FIXME: Hot fix VSCode 1.42 hover position
|
||||
if (message && message.result && message.result.range && message.result.contents) {
|
||||
message.result.range = undefined;
|
||||
|
||||
@@ -10,12 +10,12 @@ const CRLF: string = '\r\n';
|
||||
|
||||
export default class MessageBuffer {
|
||||
|
||||
private encoding: string;
|
||||
private encoding: BufferEncoding;
|
||||
private index: number;
|
||||
private buffer: Buffer;
|
||||
|
||||
constructor(encoding: string = 'utf8') {
|
||||
this.encoding = encoding;
|
||||
this.encoding = encoding as BufferEncoding;
|
||||
this.index = 0;
|
||||
this.buffer = Buffer.allocUnsafe(DefaultSize);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
|
||||
private buffer: MessageBuffer;
|
||||
private nextMessageLength: number;
|
||||
private messageToken: number;
|
||||
private partialMessageTimer: NodeJS.Timer | undefined;
|
||||
private partialMessageTimer: NodeJS.Timeout | undefined;
|
||||
private _partialMessageTimeout: number;
|
||||
|
||||
public constructor(io: MessageIO, encoding: string = 'utf8') {
|
||||
@@ -204,14 +204,14 @@ const CRLF = '\r\n';
|
||||
export class MessageIOWriter extends AbstractMessageWriter implements MessageWriter {
|
||||
|
||||
private io: MessageIO;
|
||||
private encoding: string;
|
||||
private encoding: BufferEncoding;
|
||||
private errorCount: number;
|
||||
|
||||
public constructor(io: MessageIO, encoding: string = 'utf8') {
|
||||
super();
|
||||
this.io = io;
|
||||
this.io.writer = this;
|
||||
this.encoding = encoding;
|
||||
this.encoding = encoding as BufferEncoding;
|
||||
this.errorCount = 0;
|
||||
this.io.on('error', (error: any) => this.fireError(error));
|
||||
this.io.on('close', () => this.fireClose());
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MessageIO } from "./MessageIO";
|
||||
import { NotificationMessage } from "vscode-jsonrpc";
|
||||
import * as Prism from "../deps/prism/prism";
|
||||
import * as marked from "marked";
|
||||
import { get_configuration } from "../utils";
|
||||
import { get_configuration, register_command } from "../utils";
|
||||
import {
|
||||
Methods,
|
||||
NativeSymbolInspectParams,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
GodotNativeClassInfo,
|
||||
GodotCapabilities,
|
||||
} from "./gdscript.capabilities";
|
||||
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
return Prism.highlight(code, GDScriptGrammar, lang);
|
||||
@@ -52,10 +53,7 @@ export default class NativeDocumentManager extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(
|
||||
"godotTools.listNativeClasses",
|
||||
this.list_native_classes.bind(this)
|
||||
);
|
||||
register_command("listNativeClasses", this.list_native_classes.bind(this));
|
||||
}
|
||||
|
||||
public request_documentation(symbolName: string) {
|
||||
@@ -85,7 +83,7 @@ export default class NativeDocumentManager extends EventEmitter {
|
||||
|
||||
private inspect_native_symbol(params: NativeSymbolInspectParams) {
|
||||
let json_data = "";
|
||||
if (get_configuration("lsp.serverProtocol", "tcp") == "ws") {
|
||||
if (get_configuration("lsp.serverProtocol") == "ws") {
|
||||
json_data = JSON.stringify({
|
||||
id: -1,
|
||||
jsonrpc: "2.0",
|
||||
@@ -129,7 +127,7 @@ export default class NativeDocumentManager extends EventEmitter {
|
||||
* configuration and previously opened native symbols.
|
||||
*/
|
||||
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") {
|
||||
return vscode.ViewColumn.Active;
|
||||
@@ -297,8 +295,7 @@ export default class NativeDocumentManager extends EventEmitter {
|
||||
);
|
||||
const title = element(
|
||||
"p",
|
||||
`${
|
||||
with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""
|
||||
`${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""
|
||||
}${s.name}( ${args} )`
|
||||
);
|
||||
const doc = element(
|
||||
@@ -439,9 +436,8 @@ function element<K extends keyof HTMLElementTagNameMap>(
|
||||
props_str += ` ${key}="${props[key]}"`;
|
||||
}
|
||||
}
|
||||
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${
|
||||
new_line ? "\n" : ""
|
||||
}`;
|
||||
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""
|
||||
}`;
|
||||
}
|
||||
function make_link(classname: string, symbol: string) {
|
||||
if (!symbol || symbol == classname) {
|
||||
|
||||
@@ -10,12 +10,16 @@ import {
|
||||
import path = require("path");
|
||||
import fs = require("fs");
|
||||
import * as vscode from "vscode";
|
||||
import { get_configuration, set_configuration, find_file, set_context, convert_resource_path_to_uri } from "./utils";
|
||||
import logger from "./logger";
|
||||
import {
|
||||
get_configuration,
|
||||
find_file,
|
||||
set_context,
|
||||
convert_resource_path_to_uri,
|
||||
register_command,
|
||||
} from "./utils";
|
||||
import { createLogger } from "./logger";
|
||||
|
||||
function log(...messages) {
|
||||
logger.log("[scene preview]", messages);
|
||||
}
|
||||
const log = createLogger("scene preview");
|
||||
|
||||
export class ScenePreviewProvider implements TreeDataProvider<SceneNode> {
|
||||
private root: SceneNode | undefined;
|
||||
@@ -84,15 +88,15 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode> {
|
||||
|
||||
this.tree.onDidChangeSelection(this.tree_selection_changed);
|
||||
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.pin", this.pin_preview.bind(this));
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.unpin", this.unpin_preview.bind(this));
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.copyNodePath", this.copy_node_path.bind(this));
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.copyResourcePath", this.copy_resource_path.bind(this));
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.openScene", this.open_scene.bind(this));
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.openScript", this.open_script.bind(this));
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.goToDefinition", this.go_to_definition.bind(this));
|
||||
register_command("scenePreview.pin", this.pin_preview.bind(this));
|
||||
register_command("scenePreview.unpin", this.unpin_preview.bind(this));
|
||||
register_command("scenePreview.copyNodePath", this.copy_node_path.bind(this));
|
||||
register_command("scenePreview.copyResourcePath", this.copy_resource_path.bind(this));
|
||||
register_command("scenePreview.openScene", this.open_scene.bind(this));
|
||||
register_command("scenePreview.openScript", this.open_script.bind(this));
|
||||
register_command("scenePreview.goToDefinition", this.go_to_definition.bind(this));
|
||||
|
||||
vscode.commands.registerCommand("godotTools.scenePreview.refresh", () =>
|
||||
register_command("scenePreview.refresh", () =>
|
||||
this.refresh()
|
||||
);
|
||||
|
||||
|
||||
117
src/utils.ts
117
src/utils.ts
@@ -1,49 +1,106 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
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) {
|
||||
let config_value = vscode.workspace.getConfiguration(CONFIG_CONTAINER).get(name, null);
|
||||
if (config_value === null) {
|
||||
const config = vscode.workspace.getConfiguration(EXTENSION_PREFIX);
|
||||
|
||||
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 config_value;
|
||||
}
|
||||
|
||||
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 {
|
||||
return process.env.VSCODE_DEBUG_MODE === "true";
|
||||
}
|
||||
|
||||
const CONTEXT_PREFIX = `${EXTENSION_PREFIX}.context.`;
|
||||
|
||||
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) {
|
||||
// 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)) {
|
||||
return project_file;
|
||||
} else {
|
||||
if (depth === 0) {
|
||||
return null;
|
||||
}
|
||||
return find_project_file(folder, depth - 1);
|
||||
}
|
||||
export function register_command(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
|
||||
return vscode.commands.registerCommand(`${EXTENSION_PREFIX}.${command}`, callback);
|
||||
}
|
||||
|
||||
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)) {
|
||||
return vscode.Uri.file(file);
|
||||
} else {
|
||||
@@ -56,7 +113,7 @@ export async function find_file(file: string): Promise<vscode.Uri|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");
|
||||
if (!files) {
|
||||
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", "");
|
||||
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
52
src/utils/subspawn.ts
Normal 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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"target": "es2020",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es2020",
|
||||
|
||||
Reference in New Issue
Block a user