mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2025-12-31 13:48:24 +03:00
Rewrite debugger for Godot 4 support + improved maintainability (#452)
* Significantly rework the debugger to add Godot 4 support. * Simplify debugger internal message handling and shorten code paths, to enable easier maintenance in the future. * Streamline debugger configs: almost all fields are now optional, and the debugger should work out-of-the-box in a wider set of situations. * Add guardrails, error handling, and input prompts to help guide the user to correct usage/configuration. * Add the following commands: * godotTools.debugger.debugCurrentFile * godotTools.debugger.debugPinnedFile * godotTools.debugger.pinFile * godotTools.debugger.unpinFile * godotTools.debugger.openPinnedFile --------- Co-authored-by: RedMser <redmser.jj2@gmail.com> Co-authored-by: Zachary Gardner <30502195+ZachIsAGardner@users.noreply.github.com>
This commit is contained in:
55
.eslintrc.json
Normal file
55
.eslintrc.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"out/**",
|
||||
"node_modules/**",
|
||||
"prism.js"
|
||||
],
|
||||
"overrides": [
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"@typescript-eslint/tslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/ban-types": "warn",
|
||||
"no-async-promise-executor": "warn",
|
||||
"no-inner-declarations": "warn",
|
||||
"no-prototype-builtins": "warn",
|
||||
"no-constant-condition": "warn",
|
||||
"prefer-const": "warn",
|
||||
"no-useless-escape": "off",
|
||||
"no-var": "off",
|
||||
"indent": [
|
||||
"off",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"off",
|
||||
"windows"
|
||||
],
|
||||
"quotes": [
|
||||
"warn",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
46
README.md
46
README.md
@@ -3,12 +3,6 @@
|
||||
A complete set of tools to code games with
|
||||
[Godot Engine](http://www.godotengine.org/) in Visual Studio Code.
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> This plugin requires manual configuration to work with Godot 4!
|
||||
> See the [`gdscript_lsp_server_port` setting](#gdscript_lsp_server_port)
|
||||
> item under the Configuration section below.
|
||||
|
||||
**IMPORTANT NOTE:** Versions 1.0.0 and later of this extension only support
|
||||
Godot 3.2 or later.
|
||||
|
||||
@@ -19,10 +13,13 @@ experience as comfortable as possible:
|
||||
|
||||
- Syntax highlighting for the GDScript (`.gd`) language
|
||||
- Syntax highlighting for the `.tscn` and `.tres` scene formats
|
||||
- Syntax highlighting for the `.gdshader` shader format
|
||||
- Full typed GDScript support
|
||||
- Optional "Smart Mode" to improve productivity with dynamically typed scripts
|
||||
- Function definitions and documentation display on hover (see image below)
|
||||
- Rich autocompletion
|
||||
- Switch from a `.gd` file to the related `.tscn` file (default keybind is `alt+o`)
|
||||
- In-editor Scene Preview
|
||||
- Display script warnings and errors
|
||||
- Ctrl + click on a variable or method call to jump to its definition
|
||||
- Full documentation of the Godot Engine's API supported (select *Godot Tools: List native classes of Godot* in the Command Palette)
|
||||
@@ -73,18 +70,14 @@ for Godot by following these steps:
|
||||
|
||||
You can use the following settings to configure Godot Tools:
|
||||
|
||||
##### `editor_path`
|
||||
- `godotTools.editorPath.godot3`
|
||||
- `godotTools.editorPath.godot4`
|
||||
|
||||
The absolute path to the Godot editor executable. _Under Mac OS, this is the executable inside of Godot.app._
|
||||
The path to the Godot editor executable. _Under Mac OS, this is the executable inside of Godot.app._
|
||||
|
||||
##### `gdscript_lsp_server_port`
|
||||
|
||||
The WebSocket server port of the GDScript language server.
|
||||
|
||||
For Godot 3, the default value of `6008` should work out of the box.
|
||||
|
||||
**For Godot 4, this value must be changed to `6005` for this extension to connect to the language server.**
|
||||
See [this tracking issue](https://github.com/godotengine/godot-vscode-plugin/issues/473) for more information.
|
||||
- `godotTools.lsp.headless`
|
||||
|
||||
When using Godot >3.6 or >4.2, Headless LSP mode is available. In Headless mode, the extension will attempt to launch a windowless instance of the Godot editor to use as its Language Server.
|
||||
|
||||
#### GDScript Debugger
|
||||
|
||||
@@ -102,19 +95,24 @@ To configure the GDScript debugger:
|
||||
5. Change any relevant settings.
|
||||
6. Press F5 to launch.
|
||||
|
||||
*Configurations*
|
||||
### *Configurations*
|
||||
|
||||
_Required_
|
||||
|
||||
- "project": Absolute path to a directory with a project.godot file. Defaults to the currently open VSCode workspace with `${workspaceFolder}`.
|
||||
- "port": Number that represents the port the Godot remote debugger will connect with. Defaults to `6007`.
|
||||
- "address": String that represents the IP address that the Godot remote debugger will connect to. Defaults to `127.0.0.1`.
|
||||
None: seriously. This is valid debugging configuration:
|
||||
|
||||
```json
|
||||
{ "name": "Launch", "type": "godot" }
|
||||
```
|
||||
|
||||
_Optional_
|
||||
|
||||
- "launch_game_instance": true/false. If true, an instance of Godot will be launched. Will use the path provided in `editor_path`. Defaults to `true`.
|
||||
- "launch_scene": true/false. If true, and launch_game_instance is true, will launch an instance of Godot to a currently active opened TSCN file. Defaults to `false`.
|
||||
- "scene_file": Path _relative to the project.godot file_ to a TSCN file. If launch_game_instance and launch_scene are both true, will use this file instead of looking for the currently active opened TSCN file.
|
||||
`project`: Absolute path to a directory with a project.godot file. Defaults to the currently open VSCode workspace with `${workspaceFolder}`.
|
||||
`port`: The port number for the Godot remote debugger to use.
|
||||
`address`: The IP address for the Godot remote debugger to use.
|
||||
`scene_file`: Path to a scene file to run instead of the projects 'main scene'.
|
||||
`editor_path`: Absolute path to the Godot executable to be used for this debug profile.
|
||||
`additional_options`: Additional command line arguments.
|
||||
|
||||
*Usage*
|
||||
|
||||
@@ -166,4 +164,4 @@ When developing for the extension, you can open this project in Visual Studio Co
|
||||
- GDScript is a dynamically typed script language. The language server can't
|
||||
infer all variable types.
|
||||
- To increase the number of results displayed, open the **Editor Settings**,
|
||||
go to the **Language Server** section then check **Enable Smart Resolve**.
|
||||
go to the **Language Server** section then check **Enable Smart Resolve**.
|
||||
|
||||
4141
package-lock.json
generated
4141
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
355
package.json
355
package.json
@@ -31,7 +31,7 @@
|
||||
"main": "./out/extension.js",
|
||||
"scripts": {
|
||||
"compile": "tsc -p ./",
|
||||
"lint": "tslint -p ./",
|
||||
"lint": "eslint ./src --quiet",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"package": "vsce package",
|
||||
"vscode:prepublish": "npm run esbuild-base -- --minify",
|
||||
@@ -43,34 +43,32 @@
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.openEditor",
|
||||
"title": "Godot Tools: Open workspace with Godot editor"
|
||||
"title": "Open workspace with Godot editor"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.startLanguageServer",
|
||||
"title": "Godot Tools: Start the GDScript Language Server for this workspace"
|
||||
"title": "Start the GDScript Language Server for this workspace"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.runProjectDebug",
|
||||
"title": "Godot Tools: Run workspace as Godot project with visible collision shapes and navigation meshes"
|
||||
"title": "Stop the GDScript Language Server for this workspace"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.listNativeClasses",
|
||||
"title": "Godot Tools: List native classes of godot"
|
||||
"title": "List native classes of godot"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.openTypeDocumentation",
|
||||
"title": "Godot Tools: Open Type Documentation"
|
||||
"title": "Open Type Documentation"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.inspectNode",
|
||||
"title": "Inspect Remote Node",
|
||||
"icon": {
|
||||
@@ -79,6 +77,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.refreshSceneTree",
|
||||
"title": "Refresh",
|
||||
"icon": {
|
||||
@@ -87,6 +86,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.refreshInspector",
|
||||
"title": "Refresh",
|
||||
"icon": {
|
||||
@@ -95,6 +95,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.editValue",
|
||||
"title": "Edit value",
|
||||
"icon": {
|
||||
@@ -103,54 +104,85 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "godotTools.scenePreview.refresh",
|
||||
"title": "Godot Tools: Refresh Scene Preview"
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.debugCurrentFile",
|
||||
"title": "Debug Current File",
|
||||
"icon": "$(play)"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.debugPinnedFile",
|
||||
"title": "Debug Pinned File",
|
||||
"icon": "$(play)"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.pinFile",
|
||||
"title": "Pin Scene File",
|
||||
"icon": "resources/pin_off.svg"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.unpinFile",
|
||||
"title": "Unpin Scene File",
|
||||
"icon": "resources/pin_on.svg"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.debugger.openPinnedFile",
|
||||
"title": "Open the currently pinned scene"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.refresh",
|
||||
"title": "Refresh Scene Preview"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.pin",
|
||||
"title": "Pin Scene Preview",
|
||||
"icon": "resources/pin_off.svg"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.unpin",
|
||||
"title": "Unpin Scene Preview",
|
||||
"icon": "resources/pin_on.svg"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.goToDefinition",
|
||||
"title": "Go to Definition"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.copyNodePath",
|
||||
"title": "Copy Node Path"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.copyResourcePath",
|
||||
"title": "Copy Resource Path"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.openScene",
|
||||
"title": "Open Scene"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.scenePreview.openScript",
|
||||
"title": "Open Script"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.switchSceneScript",
|
||||
"title": "Godot Tools: Switch Scene/Script"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.setSceneFile",
|
||||
"title": "Set as Scene File"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.copyResourcePathContext",
|
||||
"title": "Copy Resource Path"
|
||||
"title": "Switch Scene/Script"
|
||||
},
|
||||
{
|
||||
"category": "Godot Tools",
|
||||
"command": "godotTools.copyResourcePath",
|
||||
"title": "Godot Tools: Copy Resource Path"
|
||||
"title": "Copy Resource Path"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
@@ -164,6 +196,16 @@
|
||||
"type": "object",
|
||||
"title": "Godot Tools",
|
||||
"properties": {
|
||||
"godotTools.editorPath.godot3": {
|
||||
"type": "string",
|
||||
"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.lsp.serverProtocol": {
|
||||
"type": [
|
||||
"string"
|
||||
@@ -194,21 +236,6 @@
|
||||
"default": false,
|
||||
"description": "Whether to launch the LSP as a headless child process"
|
||||
},
|
||||
"godotTools.editorPath.godot3": {
|
||||
"type": "string",
|
||||
"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",
|
||||
"default": "",
|
||||
"description": "The scene file to run"
|
||||
},
|
||||
"godotTools.lsp.autoReconnect.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -224,27 +251,27 @@
|
||||
"default": 10,
|
||||
"description": "How many times the client will attempt to reconnect"
|
||||
},
|
||||
"godotTools.forceVisibleCollisionShapes": {
|
||||
"godotTools.debugger.forceVisibleCollisionShapes": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Force the project to run with visible collision shapes"
|
||||
},
|
||||
"godotTools.forceVisibleNavMesh": {
|
||||
"godotTools.debugger.forceVisibleNavMesh": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Force the project to run with visible navigation meshes"
|
||||
},
|
||||
"godotTools.nativeSymbolPlacement": {
|
||||
"godotTools.documentation.newTabPlacement": {
|
||||
"enum": [
|
||||
"active",
|
||||
"beside"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Place the native symbol window in the active tabs group",
|
||||
"Place the native symbol window beside the active tabs group"
|
||||
"Place new documentation views in the active tabs group",
|
||||
"Place new documentation views beside the active tabs group"
|
||||
],
|
||||
"default": "beside",
|
||||
"description": "Where to place the native symbol windows"
|
||||
"description": "Where to place new documentation views"
|
||||
},
|
||||
"godotTools.scenePreview.previewRelatedScenes": {
|
||||
"enum": [
|
||||
@@ -274,6 +301,17 @@
|
||||
],
|
||||
"configuration": "./configurations/gdscript-configuration.json"
|
||||
},
|
||||
{
|
||||
"id": "gdscene",
|
||||
"aliases": [
|
||||
"GDScene",
|
||||
"gdscene"
|
||||
],
|
||||
"extensions": [
|
||||
"tscn"
|
||||
],
|
||||
"configuration": "./configurations/gdresource-configuration.json"
|
||||
},
|
||||
{
|
||||
"id": "gdresource",
|
||||
"aliases": [
|
||||
@@ -283,7 +321,6 @@
|
||||
"extensions": [
|
||||
"godot",
|
||||
"tres",
|
||||
"tscn",
|
||||
"import",
|
||||
"gdns",
|
||||
"gdnlib"
|
||||
@@ -328,64 +365,74 @@
|
||||
{
|
||||
"type": "godot",
|
||||
"label": "GDScript Godot Debug",
|
||||
"program": "./out/debugger/debug_adapter.js",
|
||||
"runtime": "node",
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"required": [
|
||||
"project",
|
||||
"port",
|
||||
"address"
|
||||
],
|
||||
"required": [],
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a directory with a project.godot file.",
|
||||
"default": "${workspaceFolder}"
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "The port number for the Godot remote debugger to use.",
|
||||
"default": 6007
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "The IP address for the Godot remote debugger to use.",
|
||||
"default": "127.0.0.1"
|
||||
},
|
||||
"launch_game_instance": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to launch an instance of the workspace's game, or wait for a debug session to connect.",
|
||||
"default": true
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "The port number for the Godot remote debugger to use.",
|
||||
"default": 6007
|
||||
},
|
||||
"launch_scene": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to launch an instance the currently opened TSCN file, or launch the game project. Only works with launch_game_instance being true.",
|
||||
"default": false
|
||||
},
|
||||
"scene_file": {
|
||||
"scene": {
|
||||
"type": "string",
|
||||
"description": "Relative path from the godot.project file to a TSCN file. If launch_scene and launch_game_instance are true, and this file is defined, will launch the specified file instead of looking for an active TSCN file.",
|
||||
"enum": [
|
||||
"main",
|
||||
"current",
|
||||
"pinned"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Launch the 'main_scene' specified in project.godot",
|
||||
"Launch the scene (or related scene) in the current editor",
|
||||
"Launch the pinned scene"
|
||||
],
|
||||
"description": "Scene file to run when debugging. Choices are 'main', 'current', 'pinned', or providing a custom path to a scene.",
|
||||
"default": ""
|
||||
},
|
||||
"editor_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the Godot executable to be used for this debug profile."
|
||||
},
|
||||
"additional_options": {
|
||||
"type": "string",
|
||||
"description": "Additional command line arguments.",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"attach": {
|
||||
"required": [],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "The IP address for the Godot remote debugger to use.",
|
||||
"default": "127.0.0.1"
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "The port number for the Godot remote debugger to use.",
|
||||
"default": 6007
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"name": "GDScript Godot",
|
||||
"name": "GDScript: Launch Godot",
|
||||
"type": "godot",
|
||||
"request": "launch",
|
||||
"project": "${workspaceFolder}",
|
||||
"port": 6007,
|
||||
"address": "127.0.0.1",
|
||||
"launch_game_instance": true,
|
||||
"launch_scene": false,
|
||||
"additional_options": ""
|
||||
}
|
||||
],
|
||||
@@ -394,15 +441,47 @@
|
||||
"label": "GDScript Godot Debug: Launch",
|
||||
"description": "A new configuration for debugging a Godot project.",
|
||||
"body": {
|
||||
"name": "GDScript: Launch Project",
|
||||
"type": "godot",
|
||||
"request": "launch",
|
||||
"project": "${workspaceFolder}",
|
||||
"port": 6007,
|
||||
"address": "127.0.0.1",
|
||||
"launch_game_instance": true,
|
||||
"launch_scene": false,
|
||||
"additional_options": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "GDScript: Launch Current File",
|
||||
"description": "A new configuration for debugging a Godot project.",
|
||||
"body": {
|
||||
"name": "GDScript: Launch Current File",
|
||||
"type": "godot",
|
||||
"request": "launch",
|
||||
"scene": "current",
|
||||
"project": "${workspaceFolder}",
|
||||
"additional_options": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "GDScript: Launch Pinned File",
|
||||
"description": "A new configuration for debugging a Godot project.",
|
||||
"body": {
|
||||
"name": "GDScript: Launch Pinned File",
|
||||
"type": "godot",
|
||||
"request": "launch",
|
||||
"scene": "pinned",
|
||||
"project": "${workspaceFolder}",
|
||||
"additional_options": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "GDScript Godot Debug: Attach",
|
||||
"description": "A new configuration for debugging a Godot project.",
|
||||
"body": {
|
||||
"name": "GDScript: Attach to Godot",
|
||||
"type": "godot",
|
||||
"request": "attach",
|
||||
"address": "127.0.0.1",
|
||||
"port": 6007
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -424,14 +503,12 @@
|
||||
"views": {
|
||||
"debug": [
|
||||
{
|
||||
"id": "active-scene-tree",
|
||||
"name": "Active Scene Tree",
|
||||
"when": "inDebugMode && debugType == 'godot'"
|
||||
"id": "activeSceneTree",
|
||||
"name": "Active Scene Tree"
|
||||
},
|
||||
{
|
||||
"id": "inspect-node",
|
||||
"name": "Inspector",
|
||||
"when": "inDebugMode && debugType == 'godot'"
|
||||
"id": "inspectNode",
|
||||
"name": "Inspector"
|
||||
}
|
||||
],
|
||||
"godotTools": [
|
||||
@@ -441,6 +518,20 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewsWelcome": [
|
||||
{
|
||||
"view": "activeSceneTree",
|
||||
"contents": "Scene Tree data has not been requested"
|
||||
},
|
||||
{
|
||||
"view": "inspectNode",
|
||||
"contents": "Node has not been inspected"
|
||||
},
|
||||
{
|
||||
"view": "scenePreview",
|
||||
"contents": "Open a Scene to see a preview of its structure"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
@@ -476,46 +567,58 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.copyResourcePathContext",
|
||||
"command": "godotTools.debugger.editValue",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.inspectNode",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.refreshSceneTree",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.refreshInspector",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "godotTools.debugger.refreshSceneTree",
|
||||
"when": "view == active-scene-tree",
|
||||
"when": "view == activeSceneTree",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.refreshInspector",
|
||||
"when": "view == inspect-node",
|
||||
"when": "view == inspectNode",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.scenePreview.pin",
|
||||
"when": "view == scenePreview && !godotTools.context.scenePreviewPinned",
|
||||
"when": "view == scenePreview && !godotTools.context.scenePreview.pinned",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.scenePreview.unpin",
|
||||
"when": "view == scenePreview && godotTools.context.scenePreviewPinned",
|
||||
"when": "view == scenePreview && godotTools.context.scenePreview.pinned",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "godotTools.debugger.inspectNode",
|
||||
"when": "view == active-scene-tree",
|
||||
"when": "view == activeSceneTree",
|
||||
"group": "inline"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.inspectNode",
|
||||
"when": "view == inspect-node && viewItem == remote_object",
|
||||
"when": "view == inspectNode && viewItem == remote_object",
|
||||
"group": "inline"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.editValue",
|
||||
"when": "view == inspect-node && viewItem == editable_value",
|
||||
"when": "view == inspectNode && viewItem == editable_value",
|
||||
"group": "inline"
|
||||
},
|
||||
{
|
||||
@@ -544,18 +647,58 @@
|
||||
],
|
||||
"explorer/context": [
|
||||
{
|
||||
"command": "godotTools.setSceneFile",
|
||||
"group": "2_workspace"
|
||||
"command": "godotTools.debugger.pinFile",
|
||||
"group": "2_workspace",
|
||||
"when": "resourceLangId in godotTools.context.sceneLikeFiles && !(resourcePath in godotTools.context.pinnedScene)"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.copyResourcePathContext",
|
||||
"command": "godotTools.debugger.unpinFile",
|
||||
"group": "2_workspace",
|
||||
"when": "resourceLangId in godotTools.context.sceneLikeFiles && (resourcePath in godotTools.context.pinnedScene)"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.copyResourcePath",
|
||||
"group": "6_copypath"
|
||||
}
|
||||
],
|
||||
"editor/title/run": [
|
||||
{
|
||||
"command": "godotTools.debugger.debugCurrentFile",
|
||||
"group": "navigation@10",
|
||||
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.debugPinnedFile",
|
||||
"group": "navigation@10",
|
||||
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "godotTools.debugger.pinFile",
|
||||
"group": "navigation@11",
|
||||
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace && !(resourcePath in godotTools.context.pinnedScene)"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.unpinFile",
|
||||
"group": "navigation@11",
|
||||
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace && (resourcePath in godotTools.context.pinnedScene)"
|
||||
}
|
||||
],
|
||||
"editor/title/context": [
|
||||
{
|
||||
"command": "godotTools.copyResourcePathContext",
|
||||
"command": "godotTools.copyResourcePath",
|
||||
"group": "1_godot"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.pinFile",
|
||||
"group": "1_godot",
|
||||
"when": "resourceLangId in godotTools.context.sceneLikeFiles && !(resourcePath in godotTools.context.pinnedScene)"
|
||||
},
|
||||
{
|
||||
"command": "godotTools.debugger.unpinFile",
|
||||
"group": "1_godot",
|
||||
"when": "resourceLangId in godotTools.context.sceneLikeFiles && (resourcePath in godotTools.context.pinnedScene)"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
@@ -566,34 +709,40 @@
|
||||
},
|
||||
{
|
||||
"command": "godotTools.switchSceneScript",
|
||||
"when": "editorLangId == 'gdscript' || editorLangId == 'gdresource'",
|
||||
"when": "editorLangId in godotTools.context.sceneLikeFiles",
|
||||
"group": "custom1@1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^0.6.5",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^18.15.0",
|
||||
"@types/prismjs": "^1.16.8",
|
||||
"@types/vscode": "^1.80.0",
|
||||
"@types/ws": "^8.2.2",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"@vscode/vsce": "^2.21.0",
|
||||
"esbuild": "^0.15.2",
|
||||
"esbuild": "^0.17.15",
|
||||
"eslint": "^8.37.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vscode/debugadapter": "^1.64.0",
|
||||
"@vscode/debugprotocol": "^1.64.0",
|
||||
"await-notify": "^1.0.1",
|
||||
"global": "^4.4.0",
|
||||
"marked": "^4.0.11",
|
||||
"net": "^1.0.2",
|
||||
"prismjs": "^1.17.1",
|
||||
"terminate": "^2.5.0",
|
||||
"vscode-debugadapter": "^1.38.0",
|
||||
"vscode-languageclient": "^7.0.0",
|
||||
"ws": "^8.4.2"
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Mediator } from "../mediator";
|
||||
|
||||
export abstract class Command {
|
||||
public param_count: number = -1;
|
||||
|
||||
public abstract trigger(parameters: any[]): void;
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import { Command } from "./command";
|
||||
import { CommandDebugEnter } from "./commands/command_debug_enter";
|
||||
import { CommandOutput } from "./commands/command_output";
|
||||
import { CommandStackDump } from "./commands/command_stack_dump";
|
||||
import { CommandStackFrameVars } from "./commands/command_stack_frame_vars";
|
||||
import { CommandNull } from "./commands/command_null";
|
||||
import { CommandMessageSceneTree } from "./commands/command_message_scene_tree";
|
||||
import { CommandMessageInspectObject } from "./commands/command_message_inspect_object";
|
||||
import { CommandDebugExit } from "./commands/command_debug_exit";
|
||||
import { VariantEncoder } from "../variables/variant_encoder";
|
||||
|
||||
export class CommandParser {
|
||||
private commands: Map<string, () => Command> = new Map([
|
||||
[
|
||||
"output",
|
||||
function () {
|
||||
return new CommandOutput();
|
||||
},
|
||||
],
|
||||
[
|
||||
"message:scene_tree",
|
||||
function () {
|
||||
return new CommandMessageSceneTree();
|
||||
},
|
||||
],
|
||||
[
|
||||
"message:inspect_object",
|
||||
function () {
|
||||
return new CommandMessageInspectObject();
|
||||
},
|
||||
],
|
||||
[
|
||||
"stack_dump",
|
||||
function () {
|
||||
return new CommandStackDump();
|
||||
},
|
||||
],
|
||||
[
|
||||
"stack_frame_vars",
|
||||
function () {
|
||||
return new CommandStackFrameVars();
|
||||
},
|
||||
],
|
||||
[
|
||||
"debug_enter",
|
||||
function () {
|
||||
return new CommandDebugEnter();
|
||||
},
|
||||
],
|
||||
[
|
||||
"debug_exit",
|
||||
function () {
|
||||
return new CommandDebugExit();
|
||||
},
|
||||
],
|
||||
]);
|
||||
private current_command?: Command;
|
||||
private encoder = new VariantEncoder();
|
||||
private parameters: any[] = [];
|
||||
|
||||
public has_command() {
|
||||
return this.current_command;
|
||||
}
|
||||
|
||||
public make_break_command(): Buffer {
|
||||
return this.build_buffered_command("break");
|
||||
}
|
||||
|
||||
public make_continue_command(): Buffer {
|
||||
return this.build_buffered_command("continue");
|
||||
}
|
||||
|
||||
public make_inspect_object_command(object_id: bigint): Buffer {
|
||||
return this.build_buffered_command("inspect_object", [object_id]);
|
||||
}
|
||||
|
||||
public make_next_command(): Buffer {
|
||||
return this.build_buffered_command("next");
|
||||
}
|
||||
|
||||
public make_remove_breakpoint_command(path_to: string, line: number): Buffer {
|
||||
return this.build_buffered_command("breakpoint", [path_to, line, false]);
|
||||
}
|
||||
|
||||
public make_request_scene_tree_command(): Buffer {
|
||||
return this.build_buffered_command("request_scene_tree");
|
||||
}
|
||||
|
||||
public make_send_breakpoint_command(path_to: string, line: number): Buffer {
|
||||
return this.build_buffered_command("breakpoint", [path_to, line, true]);
|
||||
}
|
||||
|
||||
public make_set_object_value_command(
|
||||
object_id: bigint,
|
||||
label: string,
|
||||
new_parsed_value: any
|
||||
): Buffer {
|
||||
return this.build_buffered_command("set_object_property", [
|
||||
object_id,
|
||||
label,
|
||||
new_parsed_value,
|
||||
]);
|
||||
}
|
||||
|
||||
public make_stack_dump_command(): Buffer {
|
||||
return this.build_buffered_command("get_stack_dump");
|
||||
}
|
||||
|
||||
public make_stack_frame_vars_command(frame_id: number): Buffer {
|
||||
return this.build_buffered_command("get_stack_frame_vars", [frame_id]);
|
||||
}
|
||||
|
||||
public make_step_command() {
|
||||
return this.build_buffered_command("step");
|
||||
}
|
||||
|
||||
public parse_message(dataset: any[]) {
|
||||
while (dataset && dataset.length > 0) {
|
||||
if (this.current_command) {
|
||||
this.parameters.push(dataset.shift());
|
||||
if (this.current_command.param_count !== -1) {
|
||||
if (this.current_command.param_count === this.parameters.length) {
|
||||
try {
|
||||
this.current_command.trigger(this.parameters);
|
||||
} catch (e) {
|
||||
// FIXME: Catch exception during trigger command: TypeError: class_name.replace is not a function
|
||||
// class_name is the key of Mediator.inspect_callbacks
|
||||
console.error("Catch exception during trigger command: " + e);
|
||||
} finally {
|
||||
this.current_command = undefined;
|
||||
this.parameters = [];
|
||||
}
|
||||
} else if(this.current_command.param_count < this.parameters.length) {
|
||||
// we debugged that an exception occures during this.current_command.trigger(this.parameters)
|
||||
// because we do not understand the root cause of the exception, we set the current command to undefined
|
||||
// to avoid a infinite loop of parse_message(...)
|
||||
this.current_command = undefined;
|
||||
this.parameters = [];
|
||||
console.log("Exception not catched. Reset current_command to avoid infinite loop.");
|
||||
}
|
||||
} else {
|
||||
this.current_command.param_count = this.parameters.shift();
|
||||
if (this.current_command.param_count === 0) {
|
||||
this.current_command.trigger([]);
|
||||
this.current_command = undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let data = dataset.shift();
|
||||
if (data && this.commands.has(data)) {
|
||||
this.current_command = this.commands.get(data)();
|
||||
} else {
|
||||
this.current_command = new CommandNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private build_buffered_command(command: string, parameters?: any[]) {
|
||||
let command_array: any[] = [command];
|
||||
if (parameters) {
|
||||
parameters.forEach((param) => {
|
||||
command_array.push(param);
|
||||
});
|
||||
}
|
||||
|
||||
let buffer = this.encoder.encode_variant(command_array);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
import { Mediator } from "../../mediator";
|
||||
|
||||
export class CommandDebugEnter extends Command {
|
||||
public trigger(parameters: any[]) {
|
||||
let reason: string = parameters[1];
|
||||
Mediator.notify("debug_enter", [reason]);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
import { Mediator } from "../../mediator";
|
||||
|
||||
export class CommandDebugExit extends Command {
|
||||
public trigger(parameters: any[]) {
|
||||
Mediator.notify("debug_exit");
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
import { RawObject } from "../../variables/variants";
|
||||
import { Mediator } from "../../mediator";
|
||||
|
||||
export class CommandMessageInspectObject extends Command {
|
||||
public trigger(parameters: any[]) {
|
||||
let id = BigInt(parameters[0]);
|
||||
let class_name: string = parameters[1];
|
||||
let properties: any[] = parameters[2];
|
||||
|
||||
let raw_object = new RawObject(class_name);
|
||||
properties.forEach((prop) => {
|
||||
raw_object.set(prop[0], prop[5]);
|
||||
});
|
||||
|
||||
Mediator.notify("inspected_object", [id, raw_object]);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
import { Mediator } from "../../mediator";
|
||||
import { SceneNode } from "../../scene_tree/scene_tree_provider";
|
||||
|
||||
export class CommandMessageSceneTree extends Command {
|
||||
public trigger(parameters: any[]) {
|
||||
let scene = this.parse_next(parameters, { offset: 0 });
|
||||
|
||||
Mediator.notify("scene_tree", [scene]);
|
||||
}
|
||||
|
||||
private parse_next(params: any[], ofs: { offset: number }): SceneNode {
|
||||
let child_count: number = params[ofs.offset++];
|
||||
let name: string = params[ofs.offset++];
|
||||
let class_name: string = params[ofs.offset++];
|
||||
let id: number = params[ofs.offset++];
|
||||
|
||||
let children: SceneNode[] = [];
|
||||
for (let i = 0; i < child_count; ++i) {
|
||||
children.push(this.parse_next(params, ofs));
|
||||
}
|
||||
|
||||
return new SceneNode(name, class_name, id, children);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
|
||||
export class CommandNull extends Command {
|
||||
public trigger(parameters: any[]) {}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
import { Mediator } from "../../mediator";
|
||||
|
||||
export class CommandOutput extends Command {
|
||||
public trigger(parameters: any[]) {
|
||||
let lines: string[] = parameters;
|
||||
Mediator.notify("output", lines);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
import { Mediator } from "../../mediator";
|
||||
import { GodotStackFrame } from "../../debug_runtime";
|
||||
|
||||
export class CommandStackDump extends Command {
|
||||
public trigger(parameters: any[]) {
|
||||
let frames: GodotStackFrame[] = parameters.map((sf, i) => {
|
||||
return {
|
||||
id: i,
|
||||
file: sf.get("file"),
|
||||
function: sf.get("function"),
|
||||
line: sf.get("line"),
|
||||
};
|
||||
});
|
||||
Mediator.notify("stack_dump", frames);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Command } from "../command";
|
||||
import { Mediator } from "../../mediator";
|
||||
|
||||
export class CommandStackFrameVars extends Command {
|
||||
public trigger(parameters: any[]) {
|
||||
let globals: any[] = [];
|
||||
let locals: any[] = [];
|
||||
let members: any[] = [];
|
||||
|
||||
let local_count = parameters[0] * 2;
|
||||
let member_count = parameters[1 + local_count] * 2;
|
||||
let global_count = parameters[2 + local_count + member_count] * 2;
|
||||
|
||||
if (local_count > 0) {
|
||||
let offset = 1;
|
||||
locals = parameters.slice(offset, offset + local_count);
|
||||
}
|
||||
|
||||
if (member_count > 0) {
|
||||
let offset = 2 + local_count;
|
||||
members = parameters.slice(offset, offset + member_count);
|
||||
}
|
||||
|
||||
if (global_count > 0) {
|
||||
let offset = 3 + local_count + member_count;
|
||||
globals = parameters.slice(offset, offset + global_count);
|
||||
}
|
||||
|
||||
Mediator.notify("stack_frame_vars", [locals, members, globals]);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { GodotDebugSession } from "./debug_session";
|
||||
|
||||
GodotDebugSession.run(GodotDebugSession);
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Mediator } from "./mediator";
|
||||
import { SceneTreeProvider } from "./scene_tree/scene_tree_provider";
|
||||
const path = require("path");
|
||||
import { SceneTreeProvider } from "./scene_tree_provider";
|
||||
import path = require("path");
|
||||
import { createLogger } from "../logger";
|
||||
|
||||
const log = createLogger("debugger.runtime");
|
||||
|
||||
export interface GodotBreakpoint {
|
||||
file: string;
|
||||
@@ -15,11 +17,64 @@ export interface GodotStackFrame {
|
||||
line: number;
|
||||
}
|
||||
|
||||
export class GodotStackVars {
|
||||
public remaining = 0;
|
||||
|
||||
constructor(
|
||||
public locals: GodotVariable[] = [],
|
||||
public members: GodotVariable[] = [],
|
||||
public globals: GodotVariable[] = [],
|
||||
) { }
|
||||
|
||||
public reset(count: number = 0) {
|
||||
this.locals = [];
|
||||
this.members = [];
|
||||
this.globals = [];
|
||||
this.remaining = count;
|
||||
}
|
||||
|
||||
public forEach(callbackfn: (value: GodotVariable, index: number, array: GodotVariable[]) => void, thisArg?: any) {
|
||||
this.locals.forEach(callbackfn);
|
||||
this.members.forEach(callbackfn);
|
||||
this.globals.forEach(callbackfn);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GodotVariable {
|
||||
name: string;
|
||||
scope_path?: string;
|
||||
sub_values?: GodotVariable[];
|
||||
value: any;
|
||||
type?: bigint;
|
||||
id?: bigint;
|
||||
}
|
||||
|
||||
export interface GDObject {
|
||||
stringify_value(): string;
|
||||
sub_values(): GodotVariable[];
|
||||
type_name(): string;
|
||||
}
|
||||
|
||||
export class RawObject extends Map<any, any> {
|
||||
constructor(public class_name: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectId implements GDObject {
|
||||
constructor(public id: bigint) { }
|
||||
|
||||
public stringify_value(): string {
|
||||
return `<${this.id}>`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [{ name: "id", value: this.id }];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Object";
|
||||
}
|
||||
}
|
||||
|
||||
export class GodotDebugData {
|
||||
@@ -28,47 +83,18 @@ export class GodotDebugData {
|
||||
|
||||
public last_frame: GodotStackFrame;
|
||||
public last_frames: GodotStackFrame[] = [];
|
||||
public project_path: string;
|
||||
public projectPath: string;
|
||||
public scene_tree?: SceneTreeProvider;
|
||||
public stack_count: number = 0;
|
||||
public stack_files: string[] = [];
|
||||
public session;
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public get_all_breakpoints(): GodotBreakpoint[] {
|
||||
let output: GodotBreakpoint[] = [];
|
||||
Array.from(this.breakpoints.values()).forEach((bp_array) => {
|
||||
output.push(...bp_array);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
public get_breakpoints(path: string) {
|
||||
return this.breakpoints.get(path) || [];
|
||||
}
|
||||
|
||||
public remove_breakpoint(path_to: string, line: number) {
|
||||
let bps = this.breakpoints.get(path_to);
|
||||
|
||||
if (bps) {
|
||||
let index = bps.findIndex((bp) => {
|
||||
return bp.line === line;
|
||||
});
|
||||
if (index !== -1) {
|
||||
let bp = bps[index];
|
||||
bps.splice(index, 1);
|
||||
this.breakpoints.set(path_to, bps);
|
||||
let file = `res://${path.relative(this.project_path, bp.file)}`;
|
||||
Mediator.notify("remove_breakpoint", [
|
||||
file.replace(/\\/g, "/"),
|
||||
bp.line,
|
||||
]);
|
||||
}
|
||||
}
|
||||
public constructor(session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public set_breakpoint(path_to: string, line: number) {
|
||||
let bp = {
|
||||
const bp = {
|
||||
file: path_to.replace(/\\/g, "/"),
|
||||
line: line,
|
||||
id: this.breakpoint_id++,
|
||||
@@ -82,9 +108,65 @@ export class GodotDebugData {
|
||||
|
||||
bps.push(bp);
|
||||
|
||||
if (this.project_path) {
|
||||
let out_file = `res://${path.relative(this.project_path, bp.file)}`;
|
||||
Mediator.notify("set_breakpoint", [out_file.replace(/\\/g, "/"), line]);
|
||||
if (this.projectPath) {
|
||||
const out_file = `res://${path.relative(this.projectPath, bp.file)}`;
|
||||
this.session?.controller.set_breakpoint(out_file.replace(/\\/g, "/"), line);
|
||||
}
|
||||
}
|
||||
|
||||
public remove_breakpoint(pathTo: string, line: number) {
|
||||
const bps = this.breakpoints.get(pathTo);
|
||||
|
||||
if (bps) {
|
||||
const index = bps.findIndex((bp) => {
|
||||
return bp.line === line;
|
||||
});
|
||||
if (index !== -1) {
|
||||
const bp = bps[index];
|
||||
bps.splice(index, 1);
|
||||
this.breakpoints.set(pathTo, bps);
|
||||
const file = `res://${path.relative(this.projectPath, bp.file)}`;
|
||||
this.session?.controller.remove_breakpoint(
|
||||
file.replace(/\\/g, "/"),
|
||||
bp.line,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get_all_breakpoints(): GodotBreakpoint[] {
|
||||
const output: GodotBreakpoint[] = [];
|
||||
Array.from(this.breakpoints.values()).forEach((bp_array) => {
|
||||
output.push(...bp_array);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
public get_breakpoints(path: string) {
|
||||
return this.breakpoints.get(path) || [];
|
||||
}
|
||||
|
||||
public get_breakpoint_string() {
|
||||
const breakpoints = this.get_all_breakpoints();
|
||||
let output = "";
|
||||
if (breakpoints.length > 0) {
|
||||
output += " --breakpoints \"";
|
||||
breakpoints.forEach((bp, i) => {
|
||||
output += `${this.get_breakpoint_path(bp.file)}:${bp.line}`;
|
||||
if (i < breakpoints.length - 1) {
|
||||
output += ",";
|
||||
}
|
||||
});
|
||||
output += "\"";
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public get_breakpoint_path(file: string) {
|
||||
const relativePath = path.relative(this.projectPath, file).replace(/\\/g, "/");
|
||||
if (relativePath.length !== 0) {
|
||||
return `res://${relativePath}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
318
src/debugger/debugger.ts
Normal file
318
src/debugger/debugger.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
debug,
|
||||
window,
|
||||
workspace,
|
||||
ExtensionContext,
|
||||
DebugConfigurationProvider,
|
||||
WorkspaceFolder,
|
||||
DebugAdapterInlineImplementation,
|
||||
DebugAdapterDescriptorFactory,
|
||||
DebugConfiguration,
|
||||
DebugAdapterDescriptor,
|
||||
DebugSession,
|
||||
CancellationToken,
|
||||
ProviderResult,
|
||||
Uri
|
||||
} from "vscode";
|
||||
import { DebugProtocol } from "@vscode/debugprotocol";
|
||||
import { GodotDebugSession as Godot3DebugSession } from "./godot3/debug_session";
|
||||
import { GodotDebugSession as Godot4DebugSession } from "./godot4/debug_session";
|
||||
import { register_command, projectVersion, set_context } from "../utils";
|
||||
import { SceneTreeProvider, SceneNode } from "./scene_tree_provider";
|
||||
import { InspectorProvider, RemoteProperty } from "./inspector_provider";
|
||||
import { createLogger } from "../logger";
|
||||
|
||||
const log = createLogger("debugger", { output: "Godot Debugger" });
|
||||
|
||||
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
|
||||
address: string;
|
||||
port: number;
|
||||
project: string;
|
||||
scene: string;
|
||||
editor_path: string;
|
||||
additional_options: string;
|
||||
}
|
||||
|
||||
export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
|
||||
address: string;
|
||||
port: number;
|
||||
project: string;
|
||||
scene: string;
|
||||
additional_options: string;
|
||||
}
|
||||
|
||||
export let pinnedScene: Uri;
|
||||
|
||||
export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfigurationProvider {
|
||||
public session?: Godot3DebugSession | Godot4DebugSession;
|
||||
public inspectorProvider = new InspectorProvider();
|
||||
public sceneTreeProvider = new SceneTreeProvider();
|
||||
|
||||
|
||||
constructor(private context: ExtensionContext) {
|
||||
log.info("Initializing Godot Debugger");
|
||||
|
||||
context.subscriptions.push(
|
||||
debug.registerDebugConfigurationProvider("godot", this),
|
||||
debug.registerDebugAdapterDescriptorFactory("godot", this),
|
||||
window.registerTreeDataProvider("inspectNode", this.inspectorProvider),
|
||||
window.registerTreeDataProvider("activeSceneTree", this.sceneTreeProvider),
|
||||
register_command("debugger.inspectNode", this.inspect_node.bind(this)),
|
||||
register_command("debugger.refreshSceneTree", this.refresh_scene_tree.bind(this)),
|
||||
register_command("debugger.refreshInspector", this.refresh_inspector.bind(this)),
|
||||
register_command("debugger.editValue", this.edit_value.bind(this)),
|
||||
register_command("debugger.debugCurrentFile", this.debug_current_file.bind(this)),
|
||||
register_command("debugger.debugPinnedFile", this.debug_pinned_file.bind(this)),
|
||||
register_command("debugger.pinFile", this.pin_file.bind(this)),
|
||||
register_command("debugger.unpinFile", this.unpin_file.bind(this)),
|
||||
register_command("debugger.openPinnedFile", this.open_pinned_file.bind(this)),
|
||||
);
|
||||
}
|
||||
|
||||
public createDebugAdapterDescriptor(session: DebugSession): ProviderResult<DebugAdapterDescriptor> {
|
||||
log.info("Creating debug session");
|
||||
log.info(`Project version identified as ${projectVersion}`);
|
||||
|
||||
if (projectVersion.startsWith("4")) {
|
||||
this.session = new Godot4DebugSession();
|
||||
} else {
|
||||
this.session = new Godot3DebugSession();
|
||||
}
|
||||
this.context.subscriptions.push(this.session);
|
||||
|
||||
this.session.sceneTree = this.sceneTreeProvider;
|
||||
return new DebugAdapterInlineImplementation(this.session);
|
||||
}
|
||||
|
||||
public resolveDebugConfiguration(
|
||||
folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
token?: CancellationToken
|
||||
): ProviderResult<DebugConfiguration> {
|
||||
// request is actually a required field according to vscode
|
||||
// however, setting it here lets us catch a possible misconfiguration
|
||||
if (!config.request) {
|
||||
config.request = "launch";
|
||||
}
|
||||
|
||||
if (config.request === "launch") {
|
||||
if (!config.address) {
|
||||
config.address = "127.0.0.1";
|
||||
}
|
||||
if (!config.port) {
|
||||
config.port = -1;
|
||||
}
|
||||
if (!config.project) {
|
||||
config.project = "${workspaceFolder}";
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public debug_current_file() {
|
||||
log.info("Attempting to debug current file");
|
||||
const configs: DebugConfiguration[] = workspace.getConfiguration("launch", window.activeTextEditor.document.uri).get("configurations");
|
||||
const launches = configs.filter((c) => c.request === "launch");
|
||||
const currents = configs.filter((c) => c.scene === "current");
|
||||
|
||||
let path = window.activeTextEditor.document.fileName;
|
||||
if (path.endsWith(".gd")) {
|
||||
const scenePath = path.replace(".gd", ".tscn");
|
||||
if (!fs.existsSync(scenePath)) {
|
||||
log.warn(`Can't find associated scene for '${path}', aborting debug`);
|
||||
window.showWarningMessage(`Can't find associated scene file for '${path}'`);
|
||||
return;
|
||||
}
|
||||
path = scenePath;
|
||||
}
|
||||
|
||||
const default_config = {
|
||||
name: `Debug ${path} : 'File'}`,
|
||||
type: "godot",
|
||||
request: "launch",
|
||||
scene: "current",
|
||||
};
|
||||
|
||||
const config = currents[0] ?? launches[0] ?? configs[0] ?? default_config;
|
||||
config.scene = path;
|
||||
|
||||
log.info(`Starting debug session for '${path}'`);
|
||||
debug.startDebugging(workspace.workspaceFolders[0], config);
|
||||
}
|
||||
|
||||
public debug_pinned_file() {
|
||||
log.info("Attempting to debug pinned scene");
|
||||
const configs: DebugConfiguration[] = workspace.getConfiguration("launch", pinnedScene).get("configurations");
|
||||
const launches = configs.filter((c) => c.request === "launch");
|
||||
const currents = configs.filter((c) => c.scene === "pinned");
|
||||
|
||||
if (!pinnedScene) {
|
||||
log.warn("No pinned scene found, aborting debug");
|
||||
window.showWarningMessage("No pinned scene found");
|
||||
return;
|
||||
}
|
||||
let path = pinnedScene.fsPath;
|
||||
if (path.endsWith(".gd")) {
|
||||
const scenePath = path.replace(".gd", ".tscn");
|
||||
if (!fs.existsSync(scenePath)) {
|
||||
log.warn(`Can't find associated scene for '${path}', aborting debug`);
|
||||
window.showWarningMessage(`Can't find associated scene file for '${path}'`);
|
||||
return;
|
||||
}
|
||||
path = scenePath;
|
||||
}
|
||||
const default_config = {
|
||||
name: `Debug ${path} : 'File'}`,
|
||||
type: "godot",
|
||||
request: "launch",
|
||||
scene: "pinned",
|
||||
};
|
||||
|
||||
const config = currents[0] ?? launches[0] ?? configs[0] ?? default_config;
|
||||
config.scene = path;
|
||||
|
||||
log.info(`Starting debug session for '${path}'`);
|
||||
debug.startDebugging(workspace.workspaceFolders[0], config);
|
||||
}
|
||||
|
||||
public pin_file(uri: Uri) {
|
||||
if (uri === undefined) {
|
||||
uri = window.activeTextEditor.document.uri;
|
||||
}
|
||||
log.info(`Pinning debug target file: '${uri.fsPath}'`);
|
||||
set_context("pinnedScene", [uri.fsPath]);
|
||||
pinnedScene = uri;
|
||||
}
|
||||
|
||||
public unpin_file(uri: Uri) {
|
||||
log.info(`Unpinning debug target file: '${pinnedScene}'`);
|
||||
set_context("pinnedScene", []);
|
||||
pinnedScene = undefined;
|
||||
}
|
||||
|
||||
public open_pinned_file() {
|
||||
log.info(`Opening pinned debug target file: '${pinnedScene}'`);
|
||||
if (pinnedScene){
|
||||
window.showTextDocument(pinnedScene);
|
||||
}
|
||||
}
|
||||
|
||||
public inspect_node(element: SceneNode | RemoteProperty) {
|
||||
this.session?.controller.request_inspect_object(BigInt(element.object_id));
|
||||
this.session?.inspect_callbacks.set(
|
||||
BigInt(element.object_id),
|
||||
(class_name, variable) => {
|
||||
this.inspectorProvider.fill_tree(
|
||||
element.label,
|
||||
class_name,
|
||||
element.object_id,
|
||||
variable
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public refresh_scene_tree() {
|
||||
this.session?.controller.request_scene_tree();
|
||||
}
|
||||
|
||||
public refresh_inspector() {
|
||||
if (this.inspectorProvider.has_tree()) {
|
||||
const name = this.inspectorProvider.get_top_name();
|
||||
const id = this.inspectorProvider.get_top_id();
|
||||
|
||||
this.session?.controller.request_inspect_object(BigInt(id));
|
||||
this.session?.inspect_callbacks.set(
|
||||
BigInt(id),
|
||||
(class_name, variable) => {
|
||||
this.inspectorProvider.fill_tree(
|
||||
name,
|
||||
class_name,
|
||||
id,
|
||||
variable
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public edit_value(property: RemoteProperty) {
|
||||
const previous_value = property.value;
|
||||
const type = typeof previous_value;
|
||||
const is_float = type === "number" && !Number.isInteger(previous_value);
|
||||
window
|
||||
.showInputBox({ value: `${property.description}` })
|
||||
.then((value) => {
|
||||
let new_parsed_value: any;
|
||||
switch (type) {
|
||||
case "string":
|
||||
new_parsed_value = value;
|
||||
break;
|
||||
case "number":
|
||||
if (is_float) {
|
||||
new_parsed_value = parseFloat(value);
|
||||
if (isNaN(new_parsed_value)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
new_parsed_value = parseInt(value);
|
||||
if (isNaN(new_parsed_value)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "boolean":
|
||||
if (
|
||||
value.toLowerCase() === "true" ||
|
||||
value.toLowerCase() === "false"
|
||||
) {
|
||||
new_parsed_value = value.toLowerCase() === "true";
|
||||
} else if (value === "0" || value === "1") {
|
||||
new_parsed_value = value === "1";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (property.changes_parent) {
|
||||
const parents = [property.parent];
|
||||
let idx = 0;
|
||||
while (parents[idx].changes_parent) {
|
||||
parents.push(parents[idx++].parent);
|
||||
}
|
||||
const changed_value = this.inspectorProvider.get_changed_value(
|
||||
parents,
|
||||
property,
|
||||
new_parsed_value
|
||||
);
|
||||
this.session?.controller.set_object_property(
|
||||
BigInt(property.object_id),
|
||||
parents[idx].label,
|
||||
changed_value,
|
||||
);
|
||||
} else {
|
||||
this.session?.controller.set_object_property(
|
||||
BigInt(property.object_id),
|
||||
property.label,
|
||||
new_parsed_value,
|
||||
);
|
||||
}
|
||||
|
||||
const name = this.inspectorProvider.get_top_name();
|
||||
const id = this.inspectorProvider.get_top_id();
|
||||
|
||||
this.session?.controller.request_inspect_object(BigInt(id));
|
||||
this.session?.inspect_callbacks.set(
|
||||
BigInt(id),
|
||||
(class_name, variable) => {
|
||||
this.inspectorProvider.fill_tree(
|
||||
name,
|
||||
class_name,
|
||||
id,
|
||||
variable
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import {
|
||||
ExtensionContext,
|
||||
debug,
|
||||
DebugConfigurationProvider,
|
||||
WorkspaceFolder,
|
||||
DebugAdapterInlineImplementation,
|
||||
DebugAdapterDescriptorFactory,
|
||||
DebugConfiguration,
|
||||
DebugAdapterDescriptor,
|
||||
DebugSession,
|
||||
CancellationToken,
|
||||
ProviderResult,
|
||||
window,
|
||||
commands,
|
||||
} from "vscode";
|
||||
import { GodotDebugSession } from "./debug_session";
|
||||
import fs = require("fs");
|
||||
import { SceneTreeProvider, SceneNode } from "./scene_tree/scene_tree_provider";
|
||||
import {
|
||||
RemoteProperty,
|
||||
InspectorProvider,
|
||||
} from "./scene_tree/inspector_provider";
|
||||
import { Mediator } from "./mediator";
|
||||
|
||||
export function register_debugger(context: ExtensionContext) {
|
||||
let provider = new GodotConfigurationProvider();
|
||||
context.subscriptions.push(
|
||||
debug.registerDebugConfigurationProvider("godot", provider)
|
||||
);
|
||||
|
||||
let inspector_provider = new InspectorProvider();
|
||||
window.registerTreeDataProvider("inspect-node", inspector_provider);
|
||||
|
||||
let scene_tree_provider = new SceneTreeProvider();
|
||||
window.registerTreeDataProvider("active-scene-tree", scene_tree_provider);
|
||||
|
||||
let factory = new GodotDebugAdapterFactory(scene_tree_provider);
|
||||
context.subscriptions.push(
|
||||
debug.registerDebugAdapterDescriptorFactory("godot", factory)
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
"godotTools.debugger.inspectNode",
|
||||
(element: SceneNode | RemoteProperty) => {
|
||||
if (element instanceof SceneNode) {
|
||||
Mediator.notify("inspect_object", [
|
||||
element.object_id,
|
||||
(class_name, variable) => {
|
||||
inspector_provider.fill_tree(
|
||||
element.label,
|
||||
class_name,
|
||||
element.object_id,
|
||||
variable
|
||||
);
|
||||
},
|
||||
]);
|
||||
} else if (element instanceof RemoteProperty) {
|
||||
Mediator.notify("inspect_object", [
|
||||
element.object_id,
|
||||
(class_name, properties) => {
|
||||
inspector_provider.fill_tree(
|
||||
element.label,
|
||||
class_name,
|
||||
element.object_id,
|
||||
properties
|
||||
);
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
commands.registerCommand("godotTools.debugger.refreshSceneTree", () => {
|
||||
Mediator.notify("request_scene_tree", []);
|
||||
});
|
||||
|
||||
commands.registerCommand("godotTools.debugger.refreshInspector", () => {
|
||||
if (inspector_provider.has_tree()) {
|
||||
let name = inspector_provider.get_top_name();
|
||||
let id = inspector_provider.get_top_id();
|
||||
Mediator.notify("inspect_object", [
|
||||
id,
|
||||
(class_name, properties) => {
|
||||
inspector_provider.fill_tree(name, class_name, id, properties);
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
commands.registerCommand(
|
||||
"godotTools.debugger.editValue",
|
||||
(property: RemoteProperty) => {
|
||||
let previous_value = property.value;
|
||||
let type = typeof previous_value;
|
||||
let is_float = type === "number" && !Number.isInteger(previous_value);
|
||||
window
|
||||
.showInputBox({ value: `${property.description}` })
|
||||
.then((value) => {
|
||||
let new_parsed_value: any;
|
||||
switch (type) {
|
||||
case "string":
|
||||
new_parsed_value = value;
|
||||
break;
|
||||
case "number":
|
||||
if (is_float) {
|
||||
new_parsed_value = parseFloat(value);
|
||||
if (isNaN(new_parsed_value)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
new_parsed_value = parseInt(value);
|
||||
if (isNaN(new_parsed_value)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "boolean":
|
||||
if (
|
||||
value.toLowerCase() === "true" ||
|
||||
value.toLowerCase() === "false"
|
||||
) {
|
||||
new_parsed_value = value.toLowerCase() === "true";
|
||||
} else if (value === "0" || value === "1") {
|
||||
new_parsed_value = value === "1";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (property.changes_parent) {
|
||||
let parents = [property.parent];
|
||||
let idx = 0;
|
||||
while (parents[idx].changes_parent) {
|
||||
parents.push(parents[idx++].parent);
|
||||
}
|
||||
let changed_value = inspector_provider.get_changed_value(
|
||||
parents,
|
||||
property,
|
||||
new_parsed_value
|
||||
);
|
||||
Mediator.notify("changed_value", [
|
||||
property.object_id,
|
||||
parents[idx].label,
|
||||
changed_value,
|
||||
]);
|
||||
} else {
|
||||
Mediator.notify("changed_value", [
|
||||
property.object_id,
|
||||
property.label,
|
||||
new_parsed_value,
|
||||
]);
|
||||
}
|
||||
|
||||
Mediator.notify("inspect_object", [
|
||||
inspector_provider.get_top_id(),
|
||||
(class_name, properties) => {
|
||||
inspector_provider.fill_tree(
|
||||
inspector_provider.get_top_name(),
|
||||
class_name,
|
||||
inspector_provider.get_top_id(),
|
||||
properties
|
||||
);
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
context.subscriptions.push(factory);
|
||||
}
|
||||
|
||||
class GodotConfigurationProvider implements DebugConfigurationProvider {
|
||||
public resolveDebugConfiguration(
|
||||
folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
token?: CancellationToken
|
||||
): ProviderResult<DebugConfiguration> {
|
||||
if (!config.type && !config.request && !config.name) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor && fs.existsSync(`${folder.uri.fsPath}/project.godot`)) {
|
||||
config.type = "godot";
|
||||
config.name = "Debug Godot";
|
||||
config.request = "launch";
|
||||
config.project = "${workspaceFolder}";
|
||||
config.port = 6007;
|
||||
config.address = "127.0.0.1";
|
||||
config.launch_game_instance = true;
|
||||
config.launch_scene = false;
|
||||
config.additional_options = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.project) {
|
||||
return window
|
||||
.showInformationMessage(
|
||||
"Cannot find a project.godot in active workspace."
|
||||
)
|
||||
.then(() => {
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
class GodotDebugAdapterFactory implements DebugAdapterDescriptorFactory {
|
||||
public session: GodotDebugSession | undefined;
|
||||
|
||||
constructor(private scene_tree_provider: SceneTreeProvider) {}
|
||||
|
||||
public createDebugAdapterDescriptor(
|
||||
session: DebugSession
|
||||
): ProviderResult<DebugAdapterDescriptor> {
|
||||
this.session = new GodotDebugSession();
|
||||
this.session.set_scene_tree(this.scene_tree_provider);
|
||||
return new DebugAdapterInlineImplementation(this.session);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.session.dispose();
|
||||
this.session = undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,111 @@
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
Breakpoint, InitializedEvent, LoggingDebugSession, Source, Thread
|
||||
} from "vscode-debugadapter";
|
||||
import { DebugProtocol } from "vscode-debugprotocol";
|
||||
import { get_configuration } from "../utils";
|
||||
import { GodotDebugData, GodotVariable } from "./debug_runtime";
|
||||
import { Mediator } from "./mediator";
|
||||
import { SceneTreeProvider } from "./scene_tree/scene_tree_provider";
|
||||
LoggingDebugSession,
|
||||
InitializedEvent,
|
||||
Thread,
|
||||
Source,
|
||||
Breakpoint,
|
||||
StoppedEvent,
|
||||
TerminatedEvent,
|
||||
} from "@vscode/debugadapter";
|
||||
import { DebugProtocol } from "@vscode/debugprotocol";
|
||||
import { debug } from "vscode";
|
||||
import { Subject } from "await-notify";
|
||||
import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime";
|
||||
import { LaunchRequestArguments, AttachRequestArguments } from "../debugger";
|
||||
import { SceneTreeProvider } from "../scene_tree_provider";
|
||||
import { ObjectId } from "./variables/variants";
|
||||
import { parse_variable, is_variable_built_in_type } from "./helpers";
|
||||
import { ServerController } from "./server_controller";
|
||||
import { ObjectId, RawObject } from "./variables/variants";
|
||||
const { Subject } = require("await-notify");
|
||||
import fs = require("fs");
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
|
||||
address: string;
|
||||
launch_game_instance: boolean;
|
||||
launch_scene: boolean;
|
||||
port: number;
|
||||
project: string;
|
||||
scene_file: string;
|
||||
additional_options: string;
|
||||
}
|
||||
const log = createLogger("debugger.session", { output: "Godot Debugger" });
|
||||
|
||||
export class GodotDebugSession extends LoggingDebugSession {
|
||||
private all_scopes: GodotVariable[];
|
||||
private controller?: ServerController;
|
||||
private debug_data = new GodotDebugData();
|
||||
public controller = new ServerController(this);
|
||||
public debug_data = new GodotDebugData(this);
|
||||
public sceneTree: SceneTreeProvider;
|
||||
private exception = false;
|
||||
private got_scope = new Subject();
|
||||
private got_scope: Subject = new Subject();
|
||||
private ongoing_inspections: bigint[] = [];
|
||||
private previous_inspections: bigint[] = [];
|
||||
private configuration_done = new Subject();
|
||||
private configuration_done: Subject = new Subject();
|
||||
private mode: "launch" | "attach" | "" = "";
|
||||
public inspect_callbacks: Map<
|
||||
bigint,
|
||||
(class_name: string, variable: GodotVariable) => void
|
||||
> = new Map();
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.setDebuggerLinesStartAt1(false);
|
||||
this.setDebuggerColumnsStartAt1(false);
|
||||
|
||||
Mediator.set_session(this);
|
||||
this.controller = new ServerController();
|
||||
Mediator.set_controller(this.controller);
|
||||
Mediator.set_debug_data(this.debug_data);
|
||||
}
|
||||
|
||||
public dispose() {}
|
||||
|
||||
public set_exception(exception: boolean) {
|
||||
this.exception = true;
|
||||
public dispose() {
|
||||
this.controller.stop();
|
||||
}
|
||||
|
||||
public set_inspection(id: bigint, replacement: GodotVariable) {
|
||||
let variables = this.all_scopes.filter(
|
||||
(va) => va && va.value instanceof ObjectId && va.value.id === id
|
||||
);
|
||||
protected initializeRequest(
|
||||
response: DebugProtocol.InitializeResponse,
|
||||
args: DebugProtocol.InitializeRequestArguments
|
||||
) {
|
||||
response.body = response.body || {};
|
||||
|
||||
variables.forEach((va) => {
|
||||
let index = this.all_scopes.findIndex((va_id) => va_id === va);
|
||||
let old = this.all_scopes.splice(index, 1);
|
||||
replacement.name = old[0].name;
|
||||
replacement.scope_path = old[0].scope_path;
|
||||
this.append_variable(replacement, index);
|
||||
});
|
||||
response.body.supportsConfigurationDoneRequest = true;
|
||||
response.body.supportsTerminateRequest = true;
|
||||
response.body.supportsEvaluateForHovers = false;
|
||||
response.body.supportsStepBack = false;
|
||||
response.body.supportsGotoTargetsRequest = false;
|
||||
response.body.supportsCancelRequest = false;
|
||||
response.body.supportsCompletionsRequest = false;
|
||||
response.body.supportsFunctionBreakpoints = false;
|
||||
response.body.supportsDataBreakpoints = false;
|
||||
response.body.supportsBreakpointLocationsRequest = false;
|
||||
response.body.supportsConditionalBreakpoints = false;
|
||||
response.body.supportsHitConditionalBreakpoints = false;
|
||||
response.body.supportsLogPoints = false;
|
||||
response.body.supportsModulesRequest = false;
|
||||
response.body.supportsReadMemoryRequest = false;
|
||||
response.body.supportsRestartFrame = false;
|
||||
response.body.supportsRestartRequest = false;
|
||||
response.body.supportsSetExpression = false;
|
||||
response.body.supportsStepInTargetsRequest = false;
|
||||
response.body.supportsTerminateThreadsRequest = false;
|
||||
|
||||
this.ongoing_inspections.splice(
|
||||
this.ongoing_inspections.findIndex((va_id) => va_id === id),
|
||||
1
|
||||
);
|
||||
|
||||
this.previous_inspections.push(id);
|
||||
|
||||
this.add_to_inspections();
|
||||
|
||||
if (this.ongoing_inspections.length === 0) {
|
||||
this.previous_inspections = [];
|
||||
this.got_scope.notify();
|
||||
}
|
||||
this.sendResponse(response);
|
||||
this.sendEvent(new InitializedEvent());
|
||||
}
|
||||
|
||||
public set_scene_tree(scene_tree_provider: SceneTreeProvider) {
|
||||
this.debug_data.scene_tree = scene_tree_provider;
|
||||
protected async launchRequest(
|
||||
response: DebugProtocol.LaunchResponse,
|
||||
args: LaunchRequestArguments
|
||||
) {
|
||||
await this.configuration_done.wait(1000);
|
||||
|
||||
this.mode = "launch";
|
||||
|
||||
this.debug_data.projectPath = args.project;
|
||||
this.exception = false;
|
||||
await this.controller.launch(args);
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async attachRequest(
|
||||
response: DebugProtocol.AttachResponse,
|
||||
args: AttachRequestArguments
|
||||
) {
|
||||
await this.configuration_done.wait(1000);
|
||||
|
||||
this.mode = "attach";
|
||||
|
||||
this.exception = false;
|
||||
await this.controller.attach(args);
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
public configurationDoneRequest(
|
||||
@@ -89,80 +116,35 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
public set_scopes(
|
||||
locals: GodotVariable[],
|
||||
members: GodotVariable[],
|
||||
globals: GodotVariable[]
|
||||
) {
|
||||
this.all_scopes = [
|
||||
undefined,
|
||||
{ name: "local", value: undefined, sub_values: locals, scope_path: "@" },
|
||||
{
|
||||
name: "member",
|
||||
value: undefined,
|
||||
sub_values: members,
|
||||
scope_path: "@",
|
||||
},
|
||||
{
|
||||
name: "global",
|
||||
value: undefined,
|
||||
sub_values: globals,
|
||||
scope_path: "@",
|
||||
},
|
||||
];
|
||||
|
||||
locals.forEach((va) => {
|
||||
va.scope_path = `@.local`;
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
members.forEach((va) => {
|
||||
va.scope_path = `@.member`;
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
globals.forEach((va) => {
|
||||
va.scope_path = `@.global`;
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
this.add_to_inspections();
|
||||
|
||||
if (this.ongoing_inspections.length === 0) {
|
||||
this.previous_inspections = [];
|
||||
this.got_scope.notify();
|
||||
}
|
||||
}
|
||||
|
||||
protected continueRequest(
|
||||
response: DebugProtocol.ContinueResponse,
|
||||
args: DebugProtocol.ContinueArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
response.body = { allThreadsContinued: true };
|
||||
Mediator.notify("continue");
|
||||
this.controller.continue();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected evaluateRequest(
|
||||
protected async evaluateRequest(
|
||||
response: DebugProtocol.EvaluateResponse,
|
||||
args: DebugProtocol.EvaluateArguments
|
||||
) {
|
||||
await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
|
||||
|
||||
if (this.all_scopes) {
|
||||
let expression = args.expression;
|
||||
let matches = expression.match(/^[_a-zA-Z0-9]+?$/);
|
||||
if (matches) {
|
||||
let result_idx = this.all_scopes.findIndex(
|
||||
(va) => va && va.name === expression
|
||||
);
|
||||
if (result_idx !== -1) {
|
||||
let result = this.all_scopes[result_idx];
|
||||
response.body = {
|
||||
result: this.parse_variable(result).value,
|
||||
variablesReference: result_idx,
|
||||
};
|
||||
}
|
||||
var variable = this.get_variable(args.expression, null, null, null);
|
||||
|
||||
if (variable.error == null) {
|
||||
var parsed_variable = parse_variable(variable.variable);
|
||||
response.body = {
|
||||
result: parsed_variable.value,
|
||||
variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0
|
||||
};
|
||||
} else {
|
||||
response.success = false;
|
||||
response.message = variable.error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,76 +158,12 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected initializeRequest(
|
||||
response: DebugProtocol.InitializeResponse,
|
||||
args: DebugProtocol.InitializeRequestArguments
|
||||
) {
|
||||
response.body = response.body || {};
|
||||
|
||||
response.body.supportsConfigurationDoneRequest = true;
|
||||
response.body.supportsTerminateRequest = true;
|
||||
|
||||
response.body.supportsEvaluateForHovers = false;
|
||||
|
||||
response.body.supportsStepBack = false;
|
||||
response.body.supportsGotoTargetsRequest = false;
|
||||
|
||||
response.body.supportsCancelRequest = false;
|
||||
|
||||
response.body.supportsCompletionsRequest = false;
|
||||
|
||||
response.body.supportsFunctionBreakpoints = false;
|
||||
response.body.supportsDataBreakpoints = false;
|
||||
response.body.supportsBreakpointLocationsRequest = false;
|
||||
response.body.supportsConditionalBreakpoints = false;
|
||||
response.body.supportsHitConditionalBreakpoints = false;
|
||||
|
||||
response.body.supportsLogPoints = false;
|
||||
|
||||
response.body.supportsModulesRequest = false;
|
||||
|
||||
response.body.supportsReadMemoryRequest = false;
|
||||
|
||||
response.body.supportsRestartFrame = false;
|
||||
response.body.supportsRestartRequest = false;
|
||||
|
||||
response.body.supportsSetExpression = false;
|
||||
|
||||
response.body.supportsStepInTargetsRequest = false;
|
||||
|
||||
response.body.supportsTerminateThreadsRequest = false;
|
||||
|
||||
this.sendResponse(response);
|
||||
this.sendEvent(new InitializedEvent());
|
||||
}
|
||||
|
||||
protected async launchRequest(
|
||||
response: DebugProtocol.LaunchResponse,
|
||||
args: LaunchRequestArguments
|
||||
) {
|
||||
await this.configuration_done.wait(1000);
|
||||
this.debug_data.project_path = args.project;
|
||||
this.exception = false;
|
||||
Mediator.notify("start", [
|
||||
args.project,
|
||||
args.address,
|
||||
args.port,
|
||||
args.launch_game_instance,
|
||||
args.launch_scene,
|
||||
args.scene_file,
|
||||
args.additional_options,
|
||||
get_configuration("sceneFileConfig", "") || args.scene_file,
|
||||
]);
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected nextRequest(
|
||||
response: DebugProtocol.NextResponse,
|
||||
args: DebugProtocol.NextArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
Mediator.notify("next");
|
||||
this.controller.next();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -255,7 +173,7 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
args: DebugProtocol.PauseArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
Mediator.notify("break");
|
||||
this.controller.break();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -264,10 +182,7 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
response: DebugProtocol.ScopesResponse,
|
||||
args: DebugProtocol.ScopesArguments
|
||||
) {
|
||||
while (this.ongoing_inspections.length > 0) {
|
||||
await this.got_scope.wait(100);
|
||||
}
|
||||
Mediator.notify("get_scopes", [args.frameId]);
|
||||
this.controller.request_stack_frame_vars(args.frameId);
|
||||
await this.got_scope.wait(2000);
|
||||
|
||||
response.body = {
|
||||
@@ -284,12 +199,12 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
response: DebugProtocol.SetBreakpointsResponse,
|
||||
args: DebugProtocol.SetBreakpointsArguments
|
||||
) {
|
||||
let path = (args.source.path as string).replace(/\\/g, "/");
|
||||
let client_lines = args.lines || [];
|
||||
const path = (args.source.path as string).replace(/\\/g, "/");
|
||||
const client_lines = args.lines || [];
|
||||
|
||||
if (fs.existsSync(path)) {
|
||||
let bps = this.debug_data.get_breakpoints(path);
|
||||
let bp_lines = bps.map((bp) => bp.line);
|
||||
const bp_lines = bps.map((bp) => bp.line);
|
||||
|
||||
bps.forEach((bp) => {
|
||||
if (client_lines.indexOf(bp.line) === -1) {
|
||||
@@ -298,7 +213,7 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
});
|
||||
client_lines.forEach((l) => {
|
||||
if (bp_lines.indexOf(l) === -1) {
|
||||
let bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
|
||||
const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
|
||||
if (!bp.condition) {
|
||||
this.debug_data.set_breakpoint(path, l);
|
||||
}
|
||||
@@ -339,7 +254,7 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
column: 1,
|
||||
source: new Source(
|
||||
sf.file,
|
||||
`${this.debug_data.project_path}/${sf.file.replace("res://", "")}`
|
||||
`${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`
|
||||
),
|
||||
};
|
||||
}),
|
||||
@@ -353,7 +268,7 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
args: DebugProtocol.StepInArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
Mediator.notify("step");
|
||||
this.controller.step();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -363,7 +278,7 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
args: DebugProtocol.StepOutArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
Mediator.notify("step_out");
|
||||
this.controller.step_out();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -372,7 +287,10 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
response: DebugProtocol.TerminateResponse,
|
||||
args: DebugProtocol.TerminateArguments
|
||||
) {
|
||||
Mediator.notify("stop");
|
||||
if (this.mode === "launch") {
|
||||
this.controller.stop();
|
||||
this.sendEvent(new TerminatedEvent());
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
@@ -385,25 +303,32 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
response: DebugProtocol.VariablesResponse,
|
||||
args: DebugProtocol.VariablesArguments
|
||||
) {
|
||||
let reference = this.all_scopes[args.variablesReference];
|
||||
if (!this.all_scopes) {
|
||||
response.body = {
|
||||
variables: []
|
||||
};
|
||||
this.sendResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const reference = this.all_scopes[args.variablesReference];
|
||||
let variables: DebugProtocol.Variable[];
|
||||
|
||||
if (!reference.sub_values) {
|
||||
variables = [];
|
||||
} else {
|
||||
variables = reference.sub_values.map((va) => {
|
||||
let sva = this.all_scopes.find(
|
||||
const sva = this.all_scopes.find(
|
||||
(sva) =>
|
||||
sva && sva.scope_path === va.scope_path && sva.name === va.name
|
||||
);
|
||||
if (sva) {
|
||||
return this.parse_variable(
|
||||
return parse_variable(
|
||||
sva,
|
||||
this.all_scopes.findIndex(
|
||||
(va_idx) =>
|
||||
va_idx &&
|
||||
va_idx.scope_path ===
|
||||
`${reference.scope_path}.${reference.name}` &&
|
||||
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
|
||||
va_idx.name === va.name
|
||||
)
|
||||
);
|
||||
@@ -418,83 +343,208 @@ export class GodotDebugSession extends LoggingDebugSession {
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
public set_exception(exception: boolean) {
|
||||
this.exception = true;
|
||||
}
|
||||
|
||||
public set_scopes(stackVars: GodotStackVars) {
|
||||
this.all_scopes = [
|
||||
undefined,
|
||||
{
|
||||
name: "local",
|
||||
value: undefined,
|
||||
sub_values: stackVars.locals,
|
||||
scope_path: "@"
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
value: undefined,
|
||||
sub_values: stackVars.members,
|
||||
scope_path: "@",
|
||||
},
|
||||
{
|
||||
name: "global",
|
||||
value: undefined,
|
||||
sub_values: stackVars.globals,
|
||||
scope_path: "@",
|
||||
},
|
||||
];
|
||||
|
||||
stackVars.locals.forEach((va) => {
|
||||
va.scope_path = "@.local";
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
stackVars.members.forEach((va) => {
|
||||
va.scope_path = "@.member";
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
stackVars.globals.forEach((va) => {
|
||||
va.scope_path = "@.global";
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
this.add_to_inspections();
|
||||
|
||||
if (this.ongoing_inspections.length === 0) {
|
||||
this.previous_inspections = [];
|
||||
this.got_scope.notify();
|
||||
}
|
||||
}
|
||||
|
||||
public set_inspection(id: bigint, replacement: GodotVariable) {
|
||||
const variables = this.all_scopes.filter(
|
||||
(va) => va && va.value instanceof ObjectId && va.value.id === id
|
||||
);
|
||||
|
||||
variables.forEach((va) => {
|
||||
const index = this.all_scopes.findIndex((va_id) => va_id === va);
|
||||
const old = this.all_scopes.splice(index, 1);
|
||||
replacement.name = old[0].name;
|
||||
replacement.scope_path = old[0].scope_path;
|
||||
this.append_variable(replacement, index);
|
||||
});
|
||||
|
||||
this.ongoing_inspections.splice(
|
||||
this.ongoing_inspections.findIndex((va_id) => va_id === id),
|
||||
1
|
||||
);
|
||||
|
||||
this.previous_inspections.push(id);
|
||||
|
||||
// this.add_to_inspections();
|
||||
|
||||
if (this.ongoing_inspections.length === 0) {
|
||||
this.previous_inspections = [];
|
||||
this.got_scope.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private add_to_inspections() {
|
||||
this.all_scopes.forEach((va) => {
|
||||
if (va && va.value instanceof ObjectId) {
|
||||
if (
|
||||
!this.ongoing_inspections.find((va_id) => va_id === va.value.id) &&
|
||||
!this.previous_inspections.find((va_id) => va_id === va.value.id)
|
||||
!this.ongoing_inspections.includes(va.value.id) &&
|
||||
!this.previous_inspections.includes(va.value.id)
|
||||
) {
|
||||
Mediator.notify("inspect_object", [va.value.id]);
|
||||
this.controller.request_inspect_object(va.value.id);
|
||||
this.ongoing_inspections.push(va.value.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } {
|
||||
var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: null };
|
||||
|
||||
if (!root) {
|
||||
if (!expression.includes("self")) {
|
||||
expression = "self." + expression;
|
||||
}
|
||||
|
||||
root = this.all_scopes.find(x => x && x.name == "self");
|
||||
object_id = this.all_scopes.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
|
||||
}
|
||||
|
||||
var items = expression.split(".");
|
||||
var propertyName = items[index + 1];
|
||||
var path = items.slice(0, index + 1).join(".")
|
||||
.split("self.").join("")
|
||||
.split("self").join("")
|
||||
.split("[").join(".")
|
||||
.split("]").join("");
|
||||
|
||||
if (items.length == 1 && items[0] == "self") {
|
||||
propertyName = "self";
|
||||
}
|
||||
|
||||
// Detect index/key
|
||||
var key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0];
|
||||
if (key) {
|
||||
key = key.replace(/['"]+/g, "");
|
||||
propertyName = propertyName.split(/(?<=\[).*(?=\])/).join("").split("\[\]").join("");
|
||||
if (path) path += ".";
|
||||
path += propertyName;
|
||||
propertyName = key;
|
||||
}
|
||||
|
||||
function sanitizeName(name: string) {
|
||||
return name.split("Members/").join("").split("Locals/").join("");
|
||||
}
|
||||
|
||||
function sanitizeScopePath(scope_path: string) {
|
||||
return scope_path.split("@.member.self.").join("")
|
||||
.split("@.member.self").join("")
|
||||
.split("@.member.").join("")
|
||||
.split("@.member").join("")
|
||||
.split("@.local.").join("")
|
||||
.split("@.local").join("")
|
||||
.split("Locals/").join("")
|
||||
.split("Members/").join("")
|
||||
.split("@").join("");
|
||||
}
|
||||
|
||||
var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) {
|
||||
return {
|
||||
sanitized: {
|
||||
name: sanitizeName(x.name),
|
||||
scope_path: sanitizeScopePath(x.scope_path)
|
||||
},
|
||||
real: x
|
||||
};
|
||||
});
|
||||
|
||||
result.variable = sanitized_all_scopes
|
||||
.find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path)
|
||||
?.real;
|
||||
if (!result.variable) {
|
||||
result.error = `Could not find: ${propertyName}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (root.value.entries) {
|
||||
if (result.variable.name == "self") {
|
||||
result.object_id = this.all_scopes
|
||||
.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
|
||||
} else if (key) {
|
||||
var collection = path.split(".")[path.split(".").length - 1];
|
||||
var collection_items = Array.from(root.value.entries())
|
||||
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == collection)[1];
|
||||
result.object_id = collection_items.get
|
||||
? collection_items.get(key)?.id
|
||||
: collection_items[key]?.id;
|
||||
} else {
|
||||
result.object_id = Array.from(root.value.entries())
|
||||
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName)[1].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.object_id) {
|
||||
result.object_id = object_id;
|
||||
}
|
||||
|
||||
result.index = this.all_scopes.findIndex(x => x && x.name == result.variable.name && x.scope_path == result.variable.scope_path);
|
||||
|
||||
if (items.length > 2 && index < items.length - 2) {
|
||||
result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private append_variable(variable: GodotVariable, index?: number) {
|
||||
if (index) {
|
||||
this.all_scopes.splice(index, 0, variable);
|
||||
} else {
|
||||
this.all_scopes.push(variable);
|
||||
}
|
||||
let base_path = `${variable.scope_path}.${variable.name}`;
|
||||
const base_path = `${variable.scope_path}.${variable.name}`;
|
||||
if (variable.sub_values) {
|
||||
variable.sub_values.forEach((va, i) => {
|
||||
va.scope_path = `${base_path}`;
|
||||
va.scope_path = base_path;
|
||||
this.append_variable(va, index ? index + i + 1 : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private parse_variable(va: GodotVariable, i?: number) {
|
||||
let value = va.value;
|
||||
let rendered_value = "";
|
||||
let reference = 0;
|
||||
let array_size = 0;
|
||||
let array_type = undefined;
|
||||
|
||||
if (typeof value === "number") {
|
||||
if (Number.isInteger(value)) {
|
||||
rendered_value = `${value}`;
|
||||
} else {
|
||||
rendered_value = `${parseFloat(value.toFixed(5))}`;
|
||||
}
|
||||
} else if (
|
||||
typeof value === "bigint" ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "string"
|
||||
) {
|
||||
rendered_value = `${value}`;
|
||||
} else if (typeof value === "undefined") {
|
||||
rendered_value = "null";
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
rendered_value = `Array[${value.length}]`;
|
||||
array_size = value.length;
|
||||
array_type = "indexed";
|
||||
reference = i ? i : 0;
|
||||
} else if (value instanceof Map) {
|
||||
if (value instanceof RawObject) {
|
||||
rendered_value = `${value.class_name}`;
|
||||
} else {
|
||||
rendered_value = `Dictionary[${value.size}]`;
|
||||
}
|
||||
array_size = value.size;
|
||||
array_type = "named";
|
||||
reference = i ? i : 0;
|
||||
} else {
|
||||
rendered_value = `${value.type_name()}${value.stringify_value()}`;
|
||||
reference = i ? i : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: va.name,
|
||||
value: rendered_value,
|
||||
variablesReference: reference,
|
||||
array_size: array_size > 0 ? array_size : undefined,
|
||||
filter: array_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
116
src/debugger/godot3/helpers.ts
Normal file
116
src/debugger/godot3/helpers.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { GodotVariable, RawObject } from "../debug_runtime";
|
||||
import { SceneNode } from "../scene_tree_provider";
|
||||
|
||||
export function parse_next_scene_node(params: any[], ofs: { offset: number } = { offset: 0 }): SceneNode {
|
||||
const child_count: number = params[ofs.offset++];
|
||||
const name: string = params[ofs.offset++];
|
||||
const class_name: string = params[ofs.offset++];
|
||||
const id: number = params[ofs.offset++];
|
||||
|
||||
const children: SceneNode[] = [];
|
||||
for (let i = 0; i < child_count; ++i) {
|
||||
children.push(parse_next_scene_node(params, ofs));
|
||||
}
|
||||
|
||||
return new SceneNode(name, class_name, id, children);
|
||||
}
|
||||
|
||||
export function split_buffers(buffer: Buffer) {
|
||||
let len = buffer.byteLength;
|
||||
let offset = 0;
|
||||
const buffers: Buffer[] = [];
|
||||
while (len > 0) {
|
||||
const subLength = buffer.readUInt32LE(offset) + 4;
|
||||
buffers.push(buffer.subarray(offset, offset + subLength));
|
||||
offset += subLength;
|
||||
len -= subLength;
|
||||
}
|
||||
|
||||
return buffers;
|
||||
}
|
||||
|
||||
export function is_variable_built_in_type(va: GodotVariable) {
|
||||
var type = typeof va.value;
|
||||
return ["number", "bigint", "boolean", "string"].some(x => x == type);
|
||||
}
|
||||
|
||||
export function build_sub_values(va: GodotVariable) {
|
||||
const value = va.value;
|
||||
|
||||
let subValues: GodotVariable[] = undefined;
|
||||
|
||||
if (value && Array.isArray(value)) {
|
||||
subValues = value.map((va, i) => {
|
||||
return { name: `${i}`, value: va } as GodotVariable;
|
||||
});
|
||||
} else if (value instanceof Map) {
|
||||
subValues = Array.from(value.keys()).map((va) => {
|
||||
if (typeof va["stringify_value"] === "function") {
|
||||
return {
|
||||
name: `${va.type_name()}${va.stringify_value()}`,
|
||||
value: value.get(va),
|
||||
} as GodotVariable;
|
||||
} else {
|
||||
return {
|
||||
name: `${va}`,
|
||||
value: value.get(va),
|
||||
} as GodotVariable;
|
||||
}
|
||||
});
|
||||
} else if (value && typeof value["sub_values"] === "function") {
|
||||
subValues = value.sub_values().map((sva) => {
|
||||
return { name: sva.name, value: sva.value } as GodotVariable;
|
||||
});
|
||||
}
|
||||
|
||||
va.sub_values = subValues;
|
||||
|
||||
subValues?.forEach((sva) => build_sub_values(sva));
|
||||
}
|
||||
|
||||
export function parse_variable(va: GodotVariable, i?: number) {
|
||||
const value = va.value;
|
||||
let rendered_value = "";
|
||||
let reference = 0;
|
||||
let array_size = 0;
|
||||
let array_type = undefined;
|
||||
|
||||
if (typeof value === "number") {
|
||||
if (Number.isInteger(value)) {
|
||||
rendered_value = `${value}`;
|
||||
} else {
|
||||
rendered_value = `${parseFloat(value.toFixed(5))}`;
|
||||
}
|
||||
} else if (
|
||||
typeof value === "bigint" ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "string"
|
||||
) {
|
||||
rendered_value = `${value}`;
|
||||
} else if (typeof value === "undefined") {
|
||||
rendered_value = "null";
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
rendered_value = `Array[${value.length}]`;
|
||||
array_size = value.length;
|
||||
array_type = "indexed";
|
||||
reference = i ? i : 0;
|
||||
} else if (value instanceof Map) {
|
||||
rendered_value = value["class_name"] ?? `Dictionary[${value.size}]`;
|
||||
array_size = value.size;
|
||||
array_type = "named";
|
||||
reference = i ? i : 0;
|
||||
} else {
|
||||
rendered_value = `${value.type_name()}${value.stringify_value()}`;
|
||||
reference = i ? i : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: va.name,
|
||||
value: rendered_value,
|
||||
variablesReference: reference,
|
||||
array_size: array_size > 0 ? array_size : undefined,
|
||||
filter: array_type,
|
||||
};
|
||||
}
|
||||
523
src/debugger/godot3/server_controller.ts
Normal file
523
src/debugger/godot3/server_controller.ts
Normal file
@@ -0,0 +1,523 @@
|
||||
import * as fs from "fs";
|
||||
import net = require("net");
|
||||
import { debug, window } from "vscode";
|
||||
import { execSync } from "child_process";
|
||||
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
|
||||
import { VariantEncoder } from "./variables/variant_encoder";
|
||||
import { VariantDecoder } from "./variables/variant_decoder";
|
||||
import { RawObject } from "./variables/variants";
|
||||
import { GodotStackFrame, GodotStackVars } from "../debug_runtime";
|
||||
import { GodotDebugSession } from "./debug_session";
|
||||
import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers";
|
||||
import { get_configuration, get_free_port, projectVersion } from "../../utils";
|
||||
import { prompt_for_godot_executable } from "../../utils/prompts";
|
||||
import { subProcess, killSubProcesses } from "../../utils/subspawn";
|
||||
import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
|
||||
const socketLog = createLogger("debugger.socket");
|
||||
|
||||
class Command {
|
||||
public command: string = "";
|
||||
public paramCount: number = -1;
|
||||
public parameters: any[] = [];
|
||||
public complete: boolean = false;
|
||||
public threadId: number = 0;
|
||||
}
|
||||
|
||||
export class ServerController {
|
||||
private commandBuffer: Buffer[] = [];
|
||||
private encoder = new VariantEncoder();
|
||||
private decoder = new VariantDecoder();
|
||||
private draining = false;
|
||||
private exception = "";
|
||||
private server?: net.Server;
|
||||
private socket?: net.Socket;
|
||||
private steppingOut = false;
|
||||
private currentCommand: Command = undefined;
|
||||
private didFirstOutput: boolean = false;
|
||||
private connectedVersion = "";
|
||||
|
||||
public constructor(
|
||||
public session: GodotDebugSession
|
||||
) { }
|
||||
|
||||
public break() {
|
||||
this.send_command("break");
|
||||
}
|
||||
|
||||
public continue() {
|
||||
this.send_command("continue");
|
||||
}
|
||||
|
||||
public next() {
|
||||
this.send_command("next");
|
||||
}
|
||||
|
||||
public step() {
|
||||
this.send_command("step");
|
||||
}
|
||||
|
||||
public step_out() {
|
||||
this.steppingOut = true;
|
||||
this.send_command("next");
|
||||
}
|
||||
|
||||
public set_breakpoint(path_to: string, line: number) {
|
||||
this.send_command("breakpoint", [path_to, line, true]);
|
||||
}
|
||||
|
||||
public remove_breakpoint(path_to: string, line: number) {
|
||||
this.session.debug_data.remove_breakpoint(path_to, line);
|
||||
this.send_command("breakpoint", [path_to, line, false]);
|
||||
}
|
||||
|
||||
public request_inspect_object(object_id: bigint) {
|
||||
this.send_command("inspect_object", [object_id]);
|
||||
}
|
||||
|
||||
public request_scene_tree() {
|
||||
this.send_command("request_scene_tree");
|
||||
}
|
||||
|
||||
public request_stack_dump() {
|
||||
this.send_command("get_stack_dump");
|
||||
}
|
||||
|
||||
public request_stack_frame_vars(frame_id: number) {
|
||||
this.send_command("get_stack_frame_vars", [frame_id]);
|
||||
}
|
||||
|
||||
public set_object_property(objectId: bigint, label: string, newParsedValue: any) {
|
||||
this.send_command("set_object_property", [
|
||||
objectId,
|
||||
label,
|
||||
newParsedValue,
|
||||
]);
|
||||
}
|
||||
|
||||
public set_exception(exception: string) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
private start_game(args: LaunchRequestArguments) {
|
||||
log.info("Starting game process");
|
||||
const settingName = "editorPath.godot3";
|
||||
const godotPath: string = get_configuration(settingName);
|
||||
|
||||
try {
|
||||
log.info(`Verifying version of '${godotPath}'`);
|
||||
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 debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
|
||||
log.warn(message);
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
log.info(`Got version string: '${output}'`);
|
||||
this.connectedVersion = output;
|
||||
if (match[1] !== settingName.slice(-1)) {
|
||||
const message = `Cannot launch debug session: The current project uses Godot '${projectVersion}', but the specified Godot executable is version '${match[0]}'`;
|
||||
log.warn(message);
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
const message = `Cannot launch debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
|
||||
log.warn(message);
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
let command = `"${godotPath}" --path "${args.project}"`;
|
||||
const address = args.address.replace("tcp://", "");
|
||||
command += ` --remote-debug "${address}:${args.port}"`;
|
||||
|
||||
if (get_configuration("debugger.forceVisibleCollisionShapes")) {
|
||||
command += " --debug-collisions";
|
||||
}
|
||||
if (get_configuration("debugger.forceVisibleNavMesh")) {
|
||||
command += " --debug-navigation";
|
||||
}
|
||||
|
||||
if (args.scene && args.scene !== "main") {
|
||||
log.info(`Custom scene argument provided: ${args.scene}`);
|
||||
let filename = args.scene;
|
||||
if (args.scene === "current") {
|
||||
let path = window.activeTextEditor.document.fileName;
|
||||
if (path.endsWith(".gd")) {
|
||||
path = path.replace(".gd", ".tscn");
|
||||
if (!fs.existsSync(path)) {
|
||||
const message = `Can't find associated scene file for ${path}`;
|
||||
log.warn(message);
|
||||
window.showErrorMessage(message, "Ok");
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
filename = path;
|
||||
}
|
||||
if (args.scene === "pinned") {
|
||||
if (!pinnedScene) {
|
||||
const message = "No pinned scene found";
|
||||
log.warn(message);
|
||||
window.showErrorMessage(message, "Ok");
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
let path = pinnedScene.fsPath;
|
||||
if (path.endsWith(".gd")) {
|
||||
path = path.replace(".gd", ".tscn");
|
||||
if (!fs.existsSync(path)) {
|
||||
const message = `Can't find associated scene file for ${path}`;
|
||||
log.warn(message);
|
||||
window.showErrorMessage(message, "Ok");
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
filename = path;
|
||||
}
|
||||
command += ` "${filename}"`;
|
||||
}
|
||||
|
||||
command += this.session.debug_data.get_breakpoint_string();
|
||||
|
||||
if (args.additional_options) {
|
||||
command += " " + args.additional_options;
|
||||
}
|
||||
|
||||
log.info(`Launching game process using command: '${command}'`);
|
||||
const debugProcess = subProcess("debug", command, { shell: true });
|
||||
|
||||
debugProcess.stdout.on("data", (data) => { });
|
||||
debugProcess.stderr.on("data", (data) => { });
|
||||
debugProcess.on("close", (code) => { });
|
||||
}
|
||||
|
||||
private stash: Buffer;
|
||||
|
||||
private on_data(buffer: Buffer) {
|
||||
if (this.stash) {
|
||||
buffer = Buffer.concat([this.stash, buffer]);
|
||||
this.stash = undefined;
|
||||
}
|
||||
|
||||
const buffers = split_buffers(buffer);
|
||||
while (buffers.length > 0) {
|
||||
const chunk = buffers.shift();
|
||||
const data = this.decoder.get_dataset(chunk)?.slice(1);
|
||||
if (data === undefined) {
|
||||
this.stash = Buffer.alloc(chunk.length);
|
||||
chunk.copy(this.stash);
|
||||
return;
|
||||
}
|
||||
this.parse_message(data);
|
||||
}
|
||||
}
|
||||
|
||||
public async launch(args: LaunchRequestArguments) {
|
||||
log.info("Starting debug controller in 'launch' mode");
|
||||
|
||||
this.server = net.createServer((socket) => {
|
||||
this.socket = socket;
|
||||
|
||||
socket.on("data", this.on_data.bind(this));
|
||||
|
||||
socket.on("close", (had_error) => {
|
||||
// log.debug("socket close");
|
||||
this.abort();
|
||||
});
|
||||
|
||||
socket.on("end", () => {
|
||||
// log.debug("socket end");
|
||||
this.abort();
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
// log.debug("socket error");
|
||||
// this.session.sendEvent(new TerminatedEvent());
|
||||
// this.stop();
|
||||
});
|
||||
|
||||
socket.on("drain", () => {
|
||||
// log.debug("socket drain");
|
||||
socket.resume();
|
||||
this.draining = false;
|
||||
this.send_buffer();
|
||||
});
|
||||
});
|
||||
|
||||
if (args.port === -1) {
|
||||
args.port = await get_free_port();
|
||||
}
|
||||
|
||||
this.server.listen(args.port, args.address);
|
||||
|
||||
this.start_game(args);
|
||||
}
|
||||
|
||||
public async attach(args: AttachRequestArguments) {
|
||||
log.info("Starting debug controller in 'attach' mode");
|
||||
|
||||
this.server = net.createServer((socket) => {
|
||||
this.socket = socket;
|
||||
|
||||
socket.on("data", this.on_data.bind(this));
|
||||
|
||||
socket.on("close", (had_error) => {
|
||||
// log.debug("socket close");
|
||||
// this.session.sendEvent(new TerminatedEvent());
|
||||
// this.stop();
|
||||
});
|
||||
|
||||
socket.on("end", () => {
|
||||
// log.debug("socket end");
|
||||
// this.session.sendEvent(new TerminatedEvent());
|
||||
// this.stop();
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
// log.error("socket error", error);
|
||||
});
|
||||
|
||||
socket.on("drain", () => {
|
||||
// log.debug("socket drain");
|
||||
socket.resume();
|
||||
this.draining = false;
|
||||
this.send_buffer();
|
||||
});
|
||||
});
|
||||
|
||||
this.server.listen(args.port, args.address);
|
||||
}
|
||||
|
||||
private parse_message(dataset: any[]) {
|
||||
if (!this.currentCommand || this.currentCommand.complete) {
|
||||
this.currentCommand = new Command();
|
||||
this.currentCommand.command = dataset.shift();
|
||||
}
|
||||
|
||||
while (dataset && dataset.length > 0) {
|
||||
if (this.currentCommand.paramCount === -1) {
|
||||
this.currentCommand.paramCount = dataset.shift();
|
||||
} else {
|
||||
this.currentCommand.parameters.push(dataset.shift());
|
||||
}
|
||||
|
||||
if (this.currentCommand.paramCount === this.currentCommand.parameters.length) {
|
||||
this.currentCommand.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentCommand.complete) {
|
||||
socketLog.debug("rx:", [this.currentCommand.command, ...this.currentCommand.parameters]);
|
||||
this.handle_command(this.currentCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private handle_command(command: Command) {
|
||||
switch (command.command) {
|
||||
case "debug_enter": {
|
||||
const reason: string = command.parameters[1];
|
||||
if (reason !== "Breakpoint") {
|
||||
this.set_exception(reason);
|
||||
} else {
|
||||
this.set_exception("");
|
||||
}
|
||||
this.request_stack_dump();
|
||||
break;
|
||||
}
|
||||
case "debug_exit":
|
||||
break;
|
||||
case "message:click_ctrl":
|
||||
// TODO: what is this?
|
||||
break;
|
||||
case "performance":
|
||||
// TODO: what is this?
|
||||
break;
|
||||
case "message:scene_tree": {
|
||||
const tree = parse_next_scene_node(command.parameters);
|
||||
this.session.sceneTree.fill_tree(tree);
|
||||
break;
|
||||
}
|
||||
case "message:inspect_object": {
|
||||
const id = BigInt(command.parameters[0]);
|
||||
const className: string = command.parameters[1];
|
||||
const properties: any[] = command.parameters[2];
|
||||
|
||||
const rawObject = new RawObject(className);
|
||||
properties.forEach((prop) => {
|
||||
rawObject.set(prop[0], prop[5]);
|
||||
});
|
||||
const inspectedVariable = { name: "", value: rawObject };
|
||||
build_sub_values(inspectedVariable);
|
||||
if (this.session.inspect_callbacks.has(BigInt(id))) {
|
||||
this.session.inspect_callbacks.get(BigInt(id))(
|
||||
inspectedVariable.name,
|
||||
inspectedVariable
|
||||
);
|
||||
this.session.inspect_callbacks.delete(BigInt(id));
|
||||
}
|
||||
this.session.set_inspection(id, inspectedVariable);
|
||||
break;
|
||||
}
|
||||
case "stack_dump": {
|
||||
const frames: GodotStackFrame[] = command.parameters.map((sf, i) => {
|
||||
return {
|
||||
id: i,
|
||||
file: sf.get("file"),
|
||||
function: sf.get("function"),
|
||||
line: sf.get("line"),
|
||||
};
|
||||
});
|
||||
this.trigger_breakpoint(frames);
|
||||
this.request_scene_tree();
|
||||
break;
|
||||
}
|
||||
case "stack_frame_vars": {
|
||||
this.do_stack_frame_vars(command.parameters);
|
||||
break;
|
||||
}
|
||||
case "output": {
|
||||
if (!this.didFirstOutput) {
|
||||
this.didFirstOutput = true;
|
||||
// this.request_scene_tree();
|
||||
}
|
||||
|
||||
command.parameters.forEach((line) => {
|
||||
debug.activeDebugConsole.appendLine(line[0]);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abort() {
|
||||
log.info("Aborting debug controller");
|
||||
this.session.sendEvent(new TerminatedEvent());
|
||||
this.stop();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
log.info("Stopping debug controller");
|
||||
killSubProcesses("debug");
|
||||
|
||||
this.socket?.destroy();
|
||||
this.server?.close((error) => {
|
||||
if (error) {
|
||||
log.error(error);
|
||||
}
|
||||
this.server.unref();
|
||||
this.server = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public trigger_breakpoint(stackFrames: GodotStackFrame[]) {
|
||||
let continueStepping = false;
|
||||
const stackCount = stackFrames.length;
|
||||
if (stackCount === 0) {
|
||||
// Engine code is being executed, no user stack trace
|
||||
this.session.debug_data.last_frames = [];
|
||||
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = stackFrames[0].file.replace("res://", `${this.session.debug_data.projectPath}/`);
|
||||
const line = stackFrames[0].line;
|
||||
|
||||
if (this.steppingOut) {
|
||||
const breakpoint = this.session.debug_data
|
||||
.get_breakpoints(file)
|
||||
.find((bp) => bp.line === line);
|
||||
if (!breakpoint) {
|
||||
if (this.session.debug_data.stack_count > 1) {
|
||||
continueStepping = this.session.debug_data.stack_count === stackCount;
|
||||
} else {
|
||||
const fileSame =
|
||||
stackFrames[0].file === this.session.debug_data.last_frame.file;
|
||||
const funcSame =
|
||||
stackFrames[0].function === this.session.debug_data.last_frame.function;
|
||||
const lineGreater =
|
||||
stackFrames[0].line >= this.session.debug_data.last_frame.line;
|
||||
|
||||
continueStepping = fileSame && funcSame && lineGreater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.session.debug_data.stack_count = stackCount;
|
||||
this.session.debug_data.last_frame = stackFrames[0];
|
||||
this.session.debug_data.last_frames = stackFrames;
|
||||
|
||||
if (continueStepping) {
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
|
||||
this.steppingOut = false;
|
||||
|
||||
this.session.debug_data.stack_files = stackFrames.map((sf) => {
|
||||
return sf.file;
|
||||
});
|
||||
|
||||
if (this.exception.length === 0) {
|
||||
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
|
||||
} else {
|
||||
this.session.set_exception(true);
|
||||
this.session.sendEvent(
|
||||
new StoppedEvent("exception", 0, this.exception)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private send_command(command: string, parameters: any[] = []) {
|
||||
const commandArray: any[] = [command, ...parameters];
|
||||
socketLog.debug("tx:", commandArray);
|
||||
const buffer = this.encoder.encode_variant(commandArray);
|
||||
this.commandBuffer.push(buffer);
|
||||
this.send_buffer();
|
||||
}
|
||||
|
||||
private send_buffer() {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!this.draining && this.commandBuffer.length > 0) {
|
||||
const command = this.commandBuffer.shift();
|
||||
this.draining = !this.socket.write(command);
|
||||
}
|
||||
}
|
||||
|
||||
private do_stack_frame_vars(parameters: any[]) {
|
||||
const stackVars = new GodotStackVars();
|
||||
|
||||
let localsRemaining = parameters[0];
|
||||
let membersRemaining = parameters[1 + (localsRemaining * 2)];
|
||||
let globalsRemaining = parameters[2 + ((localsRemaining + membersRemaining) * 2)];
|
||||
|
||||
let i = 1;
|
||||
while (localsRemaining--) {
|
||||
stackVars.locals.push({ name: parameters[i++], value: parameters[i++] });
|
||||
}
|
||||
i++;
|
||||
while (membersRemaining--) {
|
||||
stackVars.members.push({ name: parameters[i++], value: parameters[i++] });
|
||||
}
|
||||
i++;
|
||||
while (globalsRemaining--) {
|
||||
stackVars.globals.push({ name: parameters[i++], value: parameters[i++] });
|
||||
}
|
||||
|
||||
stackVars.forEach(item => build_sub_values(item));
|
||||
|
||||
this.session.set_scopes(stackVars);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
|
||||
export class VariantDecoder {
|
||||
public decode_variant(model: BufferModel) {
|
||||
let type = this.decode_UInt32(model);
|
||||
const type = this.decode_UInt32(model);
|
||||
switch (type & 0xff) {
|
||||
case GDScriptTypes.BOOL:
|
||||
return this.decode_UInt32(model) !== 0;
|
||||
@@ -87,18 +87,21 @@ export class VariantDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public get_dataset(buffer: Buffer, offset: number) {
|
||||
let len = buffer.readUInt32LE(offset);
|
||||
let model: BufferModel = {
|
||||
public get_dataset(buffer: Buffer) {
|
||||
const len = buffer.readUInt32LE(0);
|
||||
if (buffer.length != len + 4) {
|
||||
return undefined;
|
||||
}
|
||||
const model: BufferModel = {
|
||||
buffer: buffer,
|
||||
offset: offset + 4,
|
||||
offset: 4, // data starts after the initial length
|
||||
len: len,
|
||||
};
|
||||
|
||||
let output = [];
|
||||
const output = [];
|
||||
output.push(len + 4);
|
||||
do {
|
||||
let value = this.decode_variant(model);
|
||||
const value = this.decode_variant(model);
|
||||
output.push(value);
|
||||
} while (model.len > 0);
|
||||
|
||||
@@ -110,12 +113,12 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Array(model: BufferModel) {
|
||||
let output: Array<any> = [];
|
||||
const output: Array<any> = [];
|
||||
|
||||
let count = this.decode_UInt32(model);
|
||||
const count = this.decode_UInt32(model);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let value = this.decode_variant(model);
|
||||
const value = this.decode_variant(model);
|
||||
output.push(value);
|
||||
}
|
||||
|
||||
@@ -131,19 +134,19 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Color(model: BufferModel) {
|
||||
let rgb = this.decode_Vector3(model);
|
||||
let a = this.decode_Float(model);
|
||||
const rgb = this.decode_Vector3(model);
|
||||
const a = this.decode_Float(model);
|
||||
|
||||
return new Color(rgb.x, rgb.y, rgb.z, a);
|
||||
}
|
||||
|
||||
private decode_Dictionary(model: BufferModel) {
|
||||
let output = new Map<any, any>();
|
||||
const output = new Map<any, any>();
|
||||
|
||||
let count = this.decode_UInt32(model);
|
||||
const count = this.decode_UInt32(model);
|
||||
for (let i = 0; i < count; i++) {
|
||||
let key = this.decode_variant(model);
|
||||
let value = this.decode_variant(model);
|
||||
const key = this.decode_variant(model);
|
||||
const value = this.decode_variant(model);
|
||||
output.set(key, value);
|
||||
}
|
||||
|
||||
@@ -151,7 +154,7 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Double(model: BufferModel) {
|
||||
let d = model.buffer.readDoubleLE(model.offset);
|
||||
const d = model.buffer.readDoubleLE(model.offset);
|
||||
|
||||
model.offset += 8;
|
||||
model.len -= 8;
|
||||
@@ -160,7 +163,7 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Float(model: BufferModel) {
|
||||
let f = model.buffer.readFloatLE(model.offset);
|
||||
const f = model.buffer.readFloatLE(model.offset);
|
||||
|
||||
model.offset += 4;
|
||||
model.len -= 4;
|
||||
@@ -169,41 +172,53 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Int32(model: BufferModel) {
|
||||
let u = model.buffer.readInt32LE(model.offset);
|
||||
const result = model.buffer.readInt32LE(model.offset);
|
||||
|
||||
model.len -= 4;
|
||||
model.offset += 4;
|
||||
|
||||
return u;
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_UInt32(model: BufferModel) {
|
||||
const result = model.buffer.readUInt32LE(model.offset);
|
||||
model.len -= 4;
|
||||
model.offset += 4;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_Int64(model: BufferModel) {
|
||||
let hi = model.buffer.readInt32LE(model.offset);
|
||||
let lo = model.buffer.readInt32LE(model.offset + 4);
|
||||
|
||||
let u: BigInt = BigInt((hi << 32) | lo);
|
||||
|
||||
const result = model.buffer.readBigInt64LE(model.offset);
|
||||
model.len -= 8;
|
||||
model.offset += 8;
|
||||
|
||||
return u;
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_UInt64(model: BufferModel) {
|
||||
const result = model.buffer.readBigUInt64LE(model.offset);
|
||||
model.len -= 8;
|
||||
model.offset += 8;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_NodePath(model: BufferModel) {
|
||||
let name_count = this.decode_UInt32(model) & 0x7fffffff;
|
||||
const name_count = this.decode_UInt32(model) & 0x7fffffff;
|
||||
let subname_count = this.decode_UInt32(model);
|
||||
let flags = this.decode_UInt32(model);
|
||||
let is_absolute = (flags & 1) === 1;
|
||||
const flags = this.decode_UInt32(model);
|
||||
const is_absolute = (flags & 1) === 1;
|
||||
if (flags & 2) {
|
||||
//Obsolete format with property separate from subPath
|
||||
subname_count++;
|
||||
}
|
||||
|
||||
let total = name_count + subname_count;
|
||||
let names: string[] = [];
|
||||
let sub_names: string[] = [];
|
||||
const total = name_count + subname_count;
|
||||
const names: string[] = [];
|
||||
const sub_names: string[] = [];
|
||||
for (let i = 0; i < total; i++) {
|
||||
let str = this.decode_String(model);
|
||||
const str = this.decode_String(model);
|
||||
if (i < name_count) {
|
||||
names.push(str);
|
||||
} else {
|
||||
@@ -215,13 +230,13 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Object(model: BufferModel) {
|
||||
let class_name = this.decode_String(model);
|
||||
let prop_count = this.decode_UInt32(model);
|
||||
let output = new RawObject(class_name);
|
||||
const class_name = this.decode_String(model);
|
||||
const prop_count = this.decode_UInt32(model);
|
||||
const output = new RawObject(class_name);
|
||||
|
||||
for (let i = 0; i < prop_count; i++) {
|
||||
let name = this.decode_String(model);
|
||||
let value = this.decode_variant(model);
|
||||
const name = this.decode_String(model);
|
||||
const value = this.decode_variant(model);
|
||||
output.set(name, value);
|
||||
}
|
||||
|
||||
@@ -229,23 +244,23 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Object_id(model: BufferModel) {
|
||||
let id = this.decode_UInt64(model);
|
||||
const id = this.decode_UInt64(model);
|
||||
|
||||
return new ObjectId(id);
|
||||
}
|
||||
|
||||
private decode_Plane(model: BufferModel) {
|
||||
let x = this.decode_Float(model);
|
||||
let y = this.decode_Float(model);
|
||||
let z = this.decode_Float(model);
|
||||
let d = this.decode_Float(model);
|
||||
const x = this.decode_Float(model);
|
||||
const y = this.decode_Float(model);
|
||||
const z = this.decode_Float(model);
|
||||
const d = this.decode_Float(model);
|
||||
|
||||
return new Plane(x, y, z, d);
|
||||
}
|
||||
|
||||
private decode_PoolByteArray(model: BufferModel) {
|
||||
let count = this.decode_UInt32(model);
|
||||
let output: number[] = [];
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(model.buffer.readUInt8(model.offset));
|
||||
model.offset++;
|
||||
@@ -256,8 +271,8 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_PoolColorArray(model: BufferModel) {
|
||||
let count = this.decode_UInt32(model);
|
||||
let output: Color[] = [];
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Color[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Color(model));
|
||||
}
|
||||
@@ -266,8 +281,8 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_PoolFloatArray(model: BufferModel) {
|
||||
let count = this.decode_UInt32(model);
|
||||
let output: number[] = [];
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Float(model));
|
||||
}
|
||||
@@ -276,8 +291,8 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_PoolIntArray(model: BufferModel) {
|
||||
let count = this.decode_UInt32(model);
|
||||
let output: number[] = [];
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Int32(model));
|
||||
}
|
||||
@@ -286,8 +301,8 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_PoolStringArray(model: BufferModel) {
|
||||
let count = this.decode_UInt32(model);
|
||||
let output: string[] = [];
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_String(model));
|
||||
}
|
||||
@@ -296,8 +311,8 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_PoolVector2Array(model: BufferModel) {
|
||||
let count = this.decode_UInt32(model);
|
||||
let output: Vector2[] = [];
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Vector2[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Vector2(model));
|
||||
}
|
||||
@@ -306,8 +321,8 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_PoolVector3Array(model: BufferModel) {
|
||||
let count = this.decode_UInt32(model);
|
||||
let output: Vector3[] = [];
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Vector3[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Vector3(model));
|
||||
}
|
||||
@@ -316,10 +331,10 @@ export class VariantDecoder {
|
||||
}
|
||||
|
||||
private decode_Quat(model: BufferModel) {
|
||||
let x = this.decode_Float(model);
|
||||
let y = this.decode_Float(model);
|
||||
let z = this.decode_Float(model);
|
||||
let w = this.decode_Float(model);
|
||||
const x = this.decode_Float(model);
|
||||
const y = this.decode_Float(model);
|
||||
const z = this.decode_Float(model);
|
||||
const w = this.decode_Float(model);
|
||||
|
||||
return new Quat(x, y, z, w);
|
||||
}
|
||||
@@ -335,7 +350,7 @@ export class VariantDecoder {
|
||||
pad = 4 - (len % 4);
|
||||
}
|
||||
|
||||
let str = model.buffer.toString("utf8", model.offset, model.offset + len);
|
||||
const str = model.buffer.toString("utf8", model.offset, model.offset + len);
|
||||
len += pad;
|
||||
|
||||
model.offset += len;
|
||||
@@ -356,36 +371,17 @@ export class VariantDecoder {
|
||||
);
|
||||
}
|
||||
|
||||
private decode_UInt32(model: BufferModel) {
|
||||
let u = model.buffer.readUInt32LE(model.offset);
|
||||
model.len -= 4;
|
||||
model.offset += 4;
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
private decode_UInt64(model: BufferModel) {
|
||||
let hi = model.buffer.readUInt32LE(model.offset);
|
||||
let lo = model.buffer.readUInt32LE(model.offset + 4);
|
||||
|
||||
let u = BigInt((hi << 32) | lo);
|
||||
model.len -= 8;
|
||||
model.offset += 8;
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
private decode_Vector2(model: BufferModel) {
|
||||
let x = this.decode_Float(model);
|
||||
let y = this.decode_Float(model);
|
||||
const x = this.decode_Float(model);
|
||||
const y = this.decode_Float(model);
|
||||
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
private decode_Vector3(model: BufferModel) {
|
||||
let x = this.decode_Float(model);
|
||||
let y = this.decode_Float(model);
|
||||
let z = this.decode_Float(model);
|
||||
const x = this.decode_Float(model);
|
||||
const y = this.decode_Float(model);
|
||||
const z = this.decode_Float(model);
|
||||
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
@@ -35,8 +35,8 @@ export class VariantEncoder {
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
let size = this.size_variant(value);
|
||||
let buffer = Buffer.alloc(size + 4);
|
||||
const size = this.size_variant(value);
|
||||
const buffer = Buffer.alloc(size + 4);
|
||||
model = {
|
||||
buffer: buffer,
|
||||
offset: 0,
|
||||
@@ -48,7 +48,7 @@ export class VariantEncoder {
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
{
|
||||
let is_integer = Number.isInteger(value);
|
||||
const is_integer = Number.isInteger(value);
|
||||
if (is_integer) {
|
||||
this.encode_UInt32(GDScriptTypes.INT, model);
|
||||
this.encode_UInt32(value, model);
|
||||
@@ -123,7 +123,7 @@ export class VariantEncoder {
|
||||
}
|
||||
|
||||
private encode_Array(arr: any[], model: BufferModel) {
|
||||
let size = arr.length;
|
||||
const size = arr.length;
|
||||
this.encode_UInt32(size, model);
|
||||
arr.forEach((e) => {
|
||||
this.encode_variant(e, model);
|
||||
@@ -148,11 +148,11 @@ export class VariantEncoder {
|
||||
}
|
||||
|
||||
private encode_Dictionary(dict: Map<any, any>, model: BufferModel) {
|
||||
let size = dict.size;
|
||||
const size = dict.size;
|
||||
this.encode_UInt32(size, model);
|
||||
let keys = Array.from(dict.keys());
|
||||
const keys = Array.from(dict.keys());
|
||||
keys.forEach((key) => {
|
||||
let value = dict.get(key);
|
||||
const value = dict.get(key);
|
||||
this.encode_variant(key, model);
|
||||
this.encode_variant(value, model);
|
||||
});
|
||||
@@ -217,11 +217,8 @@ export class VariantEncoder {
|
||||
}
|
||||
|
||||
private encode_UInt64(value: bigint, model: BufferModel) {
|
||||
let hi = Number(value >> BigInt(32));
|
||||
let lo = Number(value);
|
||||
|
||||
this.encode_UInt32(lo, model);
|
||||
this.encode_UInt32(hi, model);
|
||||
model.buffer.writeBigUInt64LE(value, model.offset);
|
||||
model.offset += 8;
|
||||
}
|
||||
|
||||
private encode_Vector2(value: Vector2, model: BufferModel) {
|
||||
@@ -241,9 +238,9 @@ export class VariantEncoder {
|
||||
|
||||
private size_Dictionary(dict: Map<any, any>): number {
|
||||
let size = this.size_UInt32();
|
||||
let keys = Array.from(dict.keys());
|
||||
const keys = Array.from(dict.keys());
|
||||
keys.forEach((key) => {
|
||||
let value = dict.get(key);
|
||||
const value = dict.get(key);
|
||||
size += this.size_variant(key);
|
||||
size += this.size_variant(value);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GodotVariable } from "../debug_runtime";
|
||||
import { GodotVariable } from "../../debug_runtime";
|
||||
|
||||
export enum GDScriptTypes {
|
||||
NIL,
|
||||
550
src/debugger/godot4/debug_session.ts
Normal file
550
src/debugger/godot4/debug_session.ts
Normal file
@@ -0,0 +1,550 @@
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
LoggingDebugSession,
|
||||
InitializedEvent,
|
||||
Thread,
|
||||
Source,
|
||||
Breakpoint,
|
||||
StoppedEvent,
|
||||
TerminatedEvent,
|
||||
} from "@vscode/debugadapter";
|
||||
import { DebugProtocol } from "@vscode/debugprotocol";
|
||||
import { debug } from "vscode";
|
||||
import { Subject } from "await-notify";
|
||||
import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime";
|
||||
import { LaunchRequestArguments, AttachRequestArguments } from "../debugger";
|
||||
import { SceneTreeProvider } from "../scene_tree_provider";
|
||||
import { ObjectId } from "./variables/variants";
|
||||
import { parse_variable, is_variable_built_in_type } from "./helpers";
|
||||
import { ServerController } from "./server_controller";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const log = createLogger("debugger.session", { output: "Godot Debugger" });
|
||||
|
||||
export class GodotDebugSession extends LoggingDebugSession {
|
||||
private all_scopes: GodotVariable[];
|
||||
public controller = new ServerController(this);
|
||||
public debug_data = new GodotDebugData(this);
|
||||
public sceneTree: SceneTreeProvider;
|
||||
private exception = false;
|
||||
private got_scope: Subject = new Subject();
|
||||
private ongoing_inspections: bigint[] = [];
|
||||
private previous_inspections: bigint[] = [];
|
||||
private configuration_done: Subject = new Subject();
|
||||
private mode: "launch" | "attach" | "" = "";
|
||||
public inspect_callbacks: Map<
|
||||
bigint,
|
||||
(class_name: string, variable: GodotVariable) => void
|
||||
> = new Map();
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.setDebuggerLinesStartAt1(false);
|
||||
this.setDebuggerColumnsStartAt1(false);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.controller.stop();
|
||||
}
|
||||
|
||||
protected initializeRequest(
|
||||
response: DebugProtocol.InitializeResponse,
|
||||
args: DebugProtocol.InitializeRequestArguments
|
||||
) {
|
||||
response.body = response.body || {};
|
||||
|
||||
response.body.supportsConfigurationDoneRequest = true;
|
||||
response.body.supportsTerminateRequest = true;
|
||||
response.body.supportsEvaluateForHovers = false;
|
||||
response.body.supportsStepBack = false;
|
||||
response.body.supportsGotoTargetsRequest = false;
|
||||
response.body.supportsCancelRequest = false;
|
||||
response.body.supportsCompletionsRequest = false;
|
||||
response.body.supportsFunctionBreakpoints = false;
|
||||
response.body.supportsDataBreakpoints = false;
|
||||
response.body.supportsBreakpointLocationsRequest = false;
|
||||
response.body.supportsConditionalBreakpoints = false;
|
||||
response.body.supportsHitConditionalBreakpoints = false;
|
||||
response.body.supportsLogPoints = false;
|
||||
response.body.supportsModulesRequest = false;
|
||||
response.body.supportsReadMemoryRequest = false;
|
||||
response.body.supportsRestartFrame = false;
|
||||
response.body.supportsRestartRequest = false;
|
||||
response.body.supportsSetExpression = false;
|
||||
response.body.supportsStepInTargetsRequest = false;
|
||||
response.body.supportsTerminateThreadsRequest = false;
|
||||
|
||||
this.sendResponse(response);
|
||||
this.sendEvent(new InitializedEvent());
|
||||
}
|
||||
|
||||
protected async launchRequest(
|
||||
response: DebugProtocol.LaunchResponse,
|
||||
args: LaunchRequestArguments
|
||||
) {
|
||||
await this.configuration_done.wait(1000);
|
||||
|
||||
this.mode = "launch";
|
||||
|
||||
this.debug_data.projectPath = args.project;
|
||||
this.exception = false;
|
||||
await this.controller.launch(args);
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async attachRequest(
|
||||
response: DebugProtocol.AttachResponse,
|
||||
args: AttachRequestArguments
|
||||
) {
|
||||
await this.configuration_done.wait(1000);
|
||||
|
||||
this.mode = "attach";
|
||||
|
||||
this.exception = false;
|
||||
await this.controller.attach(args);
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
public configurationDoneRequest(
|
||||
response: DebugProtocol.ConfigurationDoneResponse,
|
||||
args: DebugProtocol.ConfigurationDoneArguments
|
||||
) {
|
||||
this.configuration_done.notify();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected continueRequest(
|
||||
response: DebugProtocol.ContinueResponse,
|
||||
args: DebugProtocol.ContinueArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
response.body = { allThreadsContinued: true };
|
||||
this.controller.continue();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected async evaluateRequest(
|
||||
response: DebugProtocol.EvaluateResponse,
|
||||
args: DebugProtocol.EvaluateArguments
|
||||
) {
|
||||
await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
|
||||
|
||||
if (this.all_scopes) {
|
||||
var variable = this.get_variable(args.expression, null, null, null);
|
||||
|
||||
if (variable.error == null) {
|
||||
var parsed_variable = parse_variable(variable.variable);
|
||||
response.body = {
|
||||
result: parsed_variable.value,
|
||||
variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0
|
||||
};
|
||||
} else {
|
||||
response.success = false;
|
||||
response.message = variable.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
response.body = {
|
||||
result: "null",
|
||||
variablesReference: 0,
|
||||
};
|
||||
}
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected nextRequest(
|
||||
response: DebugProtocol.NextResponse,
|
||||
args: DebugProtocol.NextArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
this.controller.next();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected pauseRequest(
|
||||
response: DebugProtocol.PauseResponse,
|
||||
args: DebugProtocol.PauseArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
this.controller.break();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected async scopesRequest(
|
||||
response: DebugProtocol.ScopesResponse,
|
||||
args: DebugProtocol.ScopesArguments
|
||||
) {
|
||||
this.controller.request_stack_frame_vars(args.frameId);
|
||||
await this.got_scope.wait(2000);
|
||||
|
||||
response.body = {
|
||||
scopes: [
|
||||
{ name: "Locals", variablesReference: 1, expensive: false },
|
||||
{ name: "Members", variablesReference: 2, expensive: false },
|
||||
{ name: "Globals", variablesReference: 3, expensive: false },
|
||||
],
|
||||
};
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected setBreakPointsRequest(
|
||||
response: DebugProtocol.SetBreakpointsResponse,
|
||||
args: DebugProtocol.SetBreakpointsArguments
|
||||
) {
|
||||
const path = (args.source.path as string).replace(/\\/g, "/");
|
||||
const client_lines = args.lines || [];
|
||||
|
||||
if (fs.existsSync(path)) {
|
||||
let bps = this.debug_data.get_breakpoints(path);
|
||||
const bp_lines = bps.map((bp) => bp.line);
|
||||
|
||||
bps.forEach((bp) => {
|
||||
if (client_lines.indexOf(bp.line) === -1) {
|
||||
this.debug_data.remove_breakpoint(path, bp.line);
|
||||
}
|
||||
});
|
||||
client_lines.forEach((l) => {
|
||||
if (bp_lines.indexOf(l) === -1) {
|
||||
const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
|
||||
if (!bp.condition) {
|
||||
this.debug_data.set_breakpoint(path, l);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bps = this.debug_data.get_breakpoints(path);
|
||||
// Sort to ensure breakpoints aren't out-of-order, which would confuse VS Code.
|
||||
bps.sort((a, b) => (a.line < b.line ? -1 : 1));
|
||||
|
||||
response.body = {
|
||||
breakpoints: bps.map((bp) => {
|
||||
return new Breakpoint(
|
||||
true,
|
||||
bp.line,
|
||||
1,
|
||||
new Source(bp.file.split("/").reverse()[0], bp.file)
|
||||
);
|
||||
}),
|
||||
};
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected stackTraceRequest(
|
||||
response: DebugProtocol.StackTraceResponse,
|
||||
args: DebugProtocol.StackTraceArguments
|
||||
) {
|
||||
if (this.debug_data.last_frame) {
|
||||
response.body = {
|
||||
totalFrames: this.debug_data.last_frames.length,
|
||||
stackFrames: this.debug_data.last_frames.map((sf) => {
|
||||
return {
|
||||
id: sf.id,
|
||||
name: sf.function,
|
||||
line: sf.line,
|
||||
column: 1,
|
||||
source: new Source(
|
||||
sf.file,
|
||||
`${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected stepInRequest(
|
||||
response: DebugProtocol.StepInResponse,
|
||||
args: DebugProtocol.StepInArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
this.controller.step();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected stepOutRequest(
|
||||
response: DebugProtocol.StepOutResponse,
|
||||
args: DebugProtocol.StepOutArguments
|
||||
) {
|
||||
if (!this.exception) {
|
||||
this.controller.step_out();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected terminateRequest(
|
||||
response: DebugProtocol.TerminateResponse,
|
||||
args: DebugProtocol.TerminateArguments
|
||||
) {
|
||||
if (this.mode === "launch") {
|
||||
this.controller.stop();
|
||||
this.sendEvent(new TerminatedEvent());
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
|
||||
response.body = { threads: [new Thread(0, "thread_1")] };
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected async variablesRequest(
|
||||
response: DebugProtocol.VariablesResponse,
|
||||
args: DebugProtocol.VariablesArguments
|
||||
) {
|
||||
if (!this.all_scopes) {
|
||||
response.body = {
|
||||
variables: []
|
||||
};
|
||||
this.sendResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const reference = this.all_scopes[args.variablesReference];
|
||||
let variables: DebugProtocol.Variable[];
|
||||
|
||||
if (!reference.sub_values) {
|
||||
variables = [];
|
||||
} else {
|
||||
variables = reference.sub_values.map((va) => {
|
||||
const sva = this.all_scopes.find(
|
||||
(sva) =>
|
||||
sva && sva.scope_path === va.scope_path && sva.name === va.name
|
||||
);
|
||||
if (sva) {
|
||||
return parse_variable(
|
||||
sva,
|
||||
this.all_scopes.findIndex(
|
||||
(va_idx) =>
|
||||
va_idx &&
|
||||
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
|
||||
va_idx.name === va.name
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
response.body = {
|
||||
variables: variables,
|
||||
};
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
public set_exception(exception: boolean) {
|
||||
this.exception = true;
|
||||
}
|
||||
|
||||
public set_scopes(stackVars: GodotStackVars) {
|
||||
this.all_scopes = [
|
||||
undefined,
|
||||
{
|
||||
name: "local",
|
||||
value: undefined,
|
||||
sub_values: stackVars.locals,
|
||||
scope_path: "@"
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
value: undefined,
|
||||
sub_values: stackVars.members,
|
||||
scope_path: "@",
|
||||
},
|
||||
{
|
||||
name: "global",
|
||||
value: undefined,
|
||||
sub_values: stackVars.globals,
|
||||
scope_path: "@",
|
||||
},
|
||||
];
|
||||
|
||||
stackVars.locals.forEach((va) => {
|
||||
va.scope_path = "@.local";
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
stackVars.members.forEach((va) => {
|
||||
va.scope_path = "@.member";
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
stackVars.globals.forEach((va) => {
|
||||
va.scope_path = "@.global";
|
||||
this.append_variable(va);
|
||||
});
|
||||
|
||||
this.add_to_inspections();
|
||||
|
||||
if (this.ongoing_inspections.length === 0) {
|
||||
this.previous_inspections = [];
|
||||
this.got_scope.notify();
|
||||
}
|
||||
}
|
||||
|
||||
public set_inspection(id: bigint, replacement: GodotVariable) {
|
||||
const variables = this.all_scopes.filter(
|
||||
(va) => va && va.value instanceof ObjectId && va.value.id === id
|
||||
);
|
||||
|
||||
variables.forEach((va) => {
|
||||
const index = this.all_scopes.findIndex((va_id) => va_id === va);
|
||||
const old = this.all_scopes.splice(index, 1);
|
||||
replacement.name = old[0].name;
|
||||
replacement.scope_path = old[0].scope_path;
|
||||
this.append_variable(replacement, index);
|
||||
});
|
||||
|
||||
this.ongoing_inspections.splice(
|
||||
this.ongoing_inspections.findIndex((va_id) => va_id === id),
|
||||
1
|
||||
);
|
||||
|
||||
this.previous_inspections.push(id);
|
||||
|
||||
// this.add_to_inspections();
|
||||
|
||||
if (this.ongoing_inspections.length === 0) {
|
||||
this.previous_inspections = [];
|
||||
this.got_scope.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private add_to_inspections() {
|
||||
this.all_scopes.forEach((va) => {
|
||||
if (va && va.value instanceof ObjectId) {
|
||||
if (
|
||||
!this.ongoing_inspections.includes(va.value.id) &&
|
||||
!this.previous_inspections.includes(va.value.id)
|
||||
) {
|
||||
this.controller.request_inspect_object(va.value.id);
|
||||
this.ongoing_inspections.push(va.value.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } {
|
||||
var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: null };
|
||||
|
||||
if (!root) {
|
||||
if (!expression.includes("self")) {
|
||||
expression = "self." + expression;
|
||||
}
|
||||
|
||||
root = this.all_scopes.find(x => x && x.name == "self");
|
||||
object_id = this.all_scopes.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
|
||||
}
|
||||
|
||||
var items = expression.split(".");
|
||||
var propertyName = items[index + 1];
|
||||
var path = items.slice(0, index + 1).join(".")
|
||||
.split("self.").join("")
|
||||
.split("self").join("")
|
||||
.split("[").join(".")
|
||||
.split("]").join("");
|
||||
|
||||
if (items.length == 1 && items[0] == "self") {
|
||||
propertyName = "self";
|
||||
}
|
||||
|
||||
// Detect index/key
|
||||
var key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0];
|
||||
if (key) {
|
||||
key = key.replace(/['"]+/g, "");
|
||||
propertyName = propertyName.split(/(?<=\[).*(?=\])/).join("").split("\[\]").join("");
|
||||
if (path) path += ".";
|
||||
path += propertyName;
|
||||
propertyName = key;
|
||||
}
|
||||
|
||||
function sanitizeName(name: string) {
|
||||
return name.split("Members/").join("").split("Locals/").join("");
|
||||
}
|
||||
|
||||
function sanitizeScopePath(scope_path: string) {
|
||||
return scope_path.split("@.member.self.").join("")
|
||||
.split("@.member.self").join("")
|
||||
.split("@.member.").join("")
|
||||
.split("@.member").join("")
|
||||
.split("@.local.").join("")
|
||||
.split("@.local").join("")
|
||||
.split("Locals/").join("")
|
||||
.split("Members/").join("")
|
||||
.split("@").join("");
|
||||
}
|
||||
|
||||
var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) {
|
||||
return {
|
||||
sanitized: {
|
||||
name: sanitizeName(x.name),
|
||||
scope_path: sanitizeScopePath(x.scope_path)
|
||||
},
|
||||
real: x
|
||||
};
|
||||
});
|
||||
|
||||
result.variable = sanitized_all_scopes
|
||||
.find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path)
|
||||
?.real;
|
||||
if (!result.variable) {
|
||||
result.error = `Could not find: ${propertyName}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (root.value.entries) {
|
||||
if (result.variable.name == "self") {
|
||||
result.object_id = this.all_scopes
|
||||
.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
|
||||
} else if (key) {
|
||||
var collection = path.split(".")[path.split(".").length - 1];
|
||||
var collection_items = Array.from(root.value.entries())
|
||||
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == collection)[1];
|
||||
result.object_id = collection_items.get
|
||||
? collection_items.get(key)?.id
|
||||
: collection_items[key]?.id;
|
||||
} else {
|
||||
result.object_id = Array.from(root.value.entries())
|
||||
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName)[1].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.object_id) {
|
||||
result.object_id = object_id;
|
||||
}
|
||||
|
||||
result.index = this.all_scopes.findIndex(x => x && x.name == result.variable.name && x.scope_path == result.variable.scope_path);
|
||||
|
||||
if (items.length > 2 && index < items.length - 2) {
|
||||
result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private append_variable(variable: GodotVariable, index?: number) {
|
||||
if (index) {
|
||||
this.all_scopes.splice(index, 0, variable);
|
||||
} else {
|
||||
this.all_scopes.push(variable);
|
||||
}
|
||||
const base_path = `${variable.scope_path}.${variable.name}`;
|
||||
if (variable.sub_values) {
|
||||
variable.sub_values.forEach((va, i) => {
|
||||
va.scope_path = base_path;
|
||||
this.append_variable(va, index ? index + i + 1 : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/debugger/godot4/helpers.ts
Normal file
123
src/debugger/godot4/helpers.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { GodotVariable, RawObject } from "../debug_runtime";
|
||||
import { SceneNode } from "../scene_tree_provider";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const log = createLogger("debugger.helpers");
|
||||
|
||||
export function parse_next_scene_node(params: any[], ofs: { offset: number } = { offset: 0 }): SceneNode {
|
||||
const child_count: number = params[ofs.offset++];
|
||||
const name: string = params[ofs.offset++];
|
||||
const class_name: string = params[ofs.offset++];
|
||||
const id: number = params[ofs.offset++];
|
||||
const scene_file_path: string = params[ofs.offset++];
|
||||
const view_flags: number = params[ofs.offset++];
|
||||
|
||||
const children: SceneNode[] = [];
|
||||
for (let i = 0; i < child_count; ++i) {
|
||||
children.push(parse_next_scene_node(params, ofs));
|
||||
}
|
||||
|
||||
return new SceneNode(name, class_name, id, children, scene_file_path, view_flags);
|
||||
}
|
||||
|
||||
export function split_buffers(buffer: Buffer) {
|
||||
let len = buffer.byteLength;
|
||||
let offset = 0;
|
||||
const buffers: Buffer[] = [];
|
||||
while (len > 0) {
|
||||
const subLength = buffer.readUInt32LE(offset) + 4;
|
||||
buffers.push(buffer.subarray(offset, offset + subLength));
|
||||
offset += subLength;
|
||||
len -= subLength;
|
||||
}
|
||||
|
||||
return buffers;
|
||||
}
|
||||
|
||||
export function is_variable_built_in_type(va: GodotVariable) {
|
||||
var type = typeof va.value;
|
||||
return ["number", "bigint", "boolean", "string"].some(x => x == type);
|
||||
}
|
||||
|
||||
|
||||
export function build_sub_values(va: GodotVariable) {
|
||||
const value = va.value;
|
||||
|
||||
let subValues: GodotVariable[] = undefined;
|
||||
|
||||
if (value && Array.isArray(value)) {
|
||||
subValues = value.map((va, i) => {
|
||||
return { name: `${i}`, value: va } as GodotVariable;
|
||||
});
|
||||
} else if (value instanceof Map) {
|
||||
subValues = Array.from(value.keys()).map((va) => {
|
||||
if (typeof va["stringify_value"] === "function") {
|
||||
return {
|
||||
name: `${va.type_name()}${va.stringify_value()}`,
|
||||
value: value.get(va),
|
||||
} as GodotVariable;
|
||||
} else {
|
||||
return {
|
||||
name: `${va}`,
|
||||
value: value.get(va),
|
||||
} as GodotVariable;
|
||||
}
|
||||
});
|
||||
} else if (value && typeof value["sub_values"] === "function") {
|
||||
subValues = value.sub_values().map((sva) => {
|
||||
return { name: sva.name, value: sva.value } as GodotVariable;
|
||||
});
|
||||
}
|
||||
|
||||
va.sub_values = subValues;
|
||||
|
||||
subValues?.forEach(build_sub_values);
|
||||
}
|
||||
|
||||
|
||||
export function parse_variable(va: GodotVariable, i?: number) {
|
||||
const value = va.value;
|
||||
let rendered_value = "";
|
||||
let reference = 0;
|
||||
let array_size = 0;
|
||||
let array_type = undefined;
|
||||
|
||||
if (typeof value === "number") {
|
||||
if (Number.isInteger(value)) {
|
||||
rendered_value = `${value}`;
|
||||
} else {
|
||||
rendered_value = `${parseFloat(value.toFixed(5))}`;
|
||||
}
|
||||
} else if (
|
||||
typeof value === "bigint" ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "string"
|
||||
) {
|
||||
rendered_value = `${value}`;
|
||||
} else if (typeof value === "undefined") {
|
||||
rendered_value = "null";
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
rendered_value = `Array[${value.length}]`;
|
||||
array_size = value.length;
|
||||
array_type = "indexed";
|
||||
reference = i ? i : 0;
|
||||
} else if (value instanceof Map) {
|
||||
rendered_value = value["class_name"] ?? `Dictionary[${value.size}]`;
|
||||
array_size = value.size;
|
||||
array_type = "named";
|
||||
reference = i ? i : 0;
|
||||
} else {
|
||||
rendered_value = `${value.type_name()}${value.stringify_value()}`;
|
||||
reference = i ? i : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: va.name,
|
||||
value: rendered_value,
|
||||
variablesReference: reference,
|
||||
array_size: array_size > 0 ? array_size : undefined,
|
||||
filter: array_type,
|
||||
};
|
||||
}
|
||||
529
src/debugger/godot4/server_controller.ts
Normal file
529
src/debugger/godot4/server_controller.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
import * as fs from "fs";
|
||||
import net = require("net");
|
||||
import { debug, window } from "vscode";
|
||||
import { execSync } from "child_process";
|
||||
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
|
||||
import { VariantEncoder } from "./variables/variant_encoder";
|
||||
import { VariantDecoder } from "./variables/variant_decoder";
|
||||
import { RawObject } from "./variables/variants";
|
||||
import { GodotStackFrame, GodotVariable, GodotStackVars } from "../debug_runtime";
|
||||
import { GodotDebugSession } from "./debug_session";
|
||||
import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers";
|
||||
import { get_configuration, get_free_port, projectVersion } from "../../utils";
|
||||
import { prompt_for_godot_executable } from "../../utils/prompts";
|
||||
import { subProcess, killSubProcesses } from "../../utils/subspawn";
|
||||
import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
|
||||
const socketLog = createLogger("debugger.socket");
|
||||
|
||||
class Command {
|
||||
public command: string = "";
|
||||
public paramCount: number = -1;
|
||||
public parameters: any[] = [];
|
||||
public complete: boolean = false;
|
||||
public threadId: number = 0;
|
||||
}
|
||||
|
||||
export class ServerController {
|
||||
private commandBuffer: Buffer[] = [];
|
||||
private encoder = new VariantEncoder();
|
||||
private decoder = new VariantDecoder();
|
||||
private draining = false;
|
||||
private exception = "";
|
||||
private threadId: number;
|
||||
private server?: net.Server;
|
||||
private socket?: net.Socket;
|
||||
private steppingOut = false;
|
||||
private didFirstOutput: boolean = false;
|
||||
private partialStackVars = new GodotStackVars();
|
||||
private connectedVersion = "";
|
||||
|
||||
public constructor(
|
||||
public session: GodotDebugSession
|
||||
) { }
|
||||
|
||||
public break() {
|
||||
this.send_command("break");
|
||||
}
|
||||
|
||||
public continue() {
|
||||
this.send_command("continue");
|
||||
}
|
||||
|
||||
public next() {
|
||||
this.send_command("next");
|
||||
}
|
||||
|
||||
public step() {
|
||||
this.send_command("step");
|
||||
}
|
||||
|
||||
public step_out() {
|
||||
this.steppingOut = true;
|
||||
this.send_command("next");
|
||||
}
|
||||
|
||||
public set_breakpoint(path_to: string, line: number) {
|
||||
this.send_command("breakpoint", [path_to, line, true]);
|
||||
}
|
||||
|
||||
public remove_breakpoint(path_to: string, line: number) {
|
||||
this.session.debug_data.remove_breakpoint(path_to, line);
|
||||
this.send_command("breakpoint", [path_to, line, false]);
|
||||
}
|
||||
|
||||
public request_inspect_object(object_id: bigint) {
|
||||
this.send_command("scene:inspect_object", [object_id]);
|
||||
}
|
||||
|
||||
public request_scene_tree() {
|
||||
this.send_command("scene:request_scene_tree");
|
||||
}
|
||||
|
||||
public request_stack_dump() {
|
||||
this.send_command("get_stack_dump");
|
||||
}
|
||||
|
||||
public request_stack_frame_vars(frame_id: number) {
|
||||
this.send_command("get_stack_frame_vars", [frame_id]);
|
||||
}
|
||||
|
||||
public set_object_property(objectId: bigint, label: string, newParsedValue: any) {
|
||||
this.send_command("scene:set_object_property", [
|
||||
objectId,
|
||||
label,
|
||||
newParsedValue,
|
||||
]);
|
||||
}
|
||||
|
||||
public set_exception(exception: string) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
private start_game(args: LaunchRequestArguments) {
|
||||
log.info("Starting game process");
|
||||
const settingName = "editorPath.godot4";
|
||||
const godotPath: string = get_configuration(settingName);
|
||||
|
||||
try {
|
||||
log.info(`Verifying version of '${godotPath}'`);
|
||||
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 debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
|
||||
log.warn(message);
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
log.info(`Got version string: '${output}'`);
|
||||
this.connectedVersion = output;
|
||||
if (match[1] !== settingName.slice(-1)) {
|
||||
const message = `Cannot launch debug session: The current project uses Godot '${projectVersion}', but the specified Godot executable is version '${match[0]}'`;
|
||||
log.warn(message);
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
const message = `Cannot launch debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
|
||||
log.warn(message);
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
let command = `"${godotPath}" --path "${args.project}"`;
|
||||
const address = args.address.replace("tcp://", "");
|
||||
command += ` --remote-debug "tcp://${address}:${args.port}"`;
|
||||
|
||||
if (get_configuration("debugger.forceVisibleCollisionShapes")) {
|
||||
command += " --debug-collisions";
|
||||
}
|
||||
if (get_configuration("debugger.forceVisibleNavMesh")) {
|
||||
command += " --debug-navigation";
|
||||
}
|
||||
|
||||
if (args.scene && args.scene !== "main") {
|
||||
log.info(`Custom scene argument provided: ${args.scene}`);
|
||||
let filename = args.scene;
|
||||
if (args.scene === "current") {
|
||||
let path = window.activeTextEditor.document.fileName;
|
||||
if (path.endsWith(".gd")) {
|
||||
path = path.replace(".gd", ".tscn");
|
||||
if (!fs.existsSync(path)) {
|
||||
const message = `Can't find associated scene file for ${path}`;
|
||||
log.warn(message);
|
||||
window.showErrorMessage(message, "Ok");
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
filename = path;
|
||||
}
|
||||
if (args.scene === "pinned") {
|
||||
if (!pinnedScene) {
|
||||
const message = "No pinned scene found";
|
||||
log.warn(message);
|
||||
window.showErrorMessage(message, "Ok");
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
let path = pinnedScene.fsPath;
|
||||
if (path.endsWith(".gd")) {
|
||||
path = path.replace(".gd", ".tscn");
|
||||
if (!fs.existsSync(path)) {
|
||||
const message = `Can't find associated scene file for ${path}`;
|
||||
log.warn(message);
|
||||
window.showErrorMessage(message, "Ok");
|
||||
this.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
filename = path;
|
||||
}
|
||||
command += ` "${filename}"`;
|
||||
}
|
||||
|
||||
command += this.session.debug_data.get_breakpoint_string();
|
||||
|
||||
if (args.additional_options) {
|
||||
command += " " + args.additional_options;
|
||||
}
|
||||
|
||||
log.info(`Launching game process using command: '${command}'`);
|
||||
const debugProcess = subProcess("debug", command, { shell: true });
|
||||
|
||||
debugProcess.stdout.on("data", (data) => { });
|
||||
debugProcess.stderr.on("data", (data) => { });
|
||||
debugProcess.on("close", (code) => { });
|
||||
}
|
||||
|
||||
private stash: Buffer;
|
||||
|
||||
private on_data(buffer: Buffer) {
|
||||
if (this.stash) {
|
||||
buffer = Buffer.concat([this.stash, buffer]);
|
||||
this.stash = undefined;
|
||||
}
|
||||
|
||||
const buffers = split_buffers(buffer);
|
||||
while (buffers.length > 0) {
|
||||
const chunk = buffers.shift();
|
||||
const data = this.decoder.get_dataset(chunk)?.slice(1);
|
||||
if (data === undefined) {
|
||||
this.stash = Buffer.alloc(chunk.length);
|
||||
chunk.copy(this.stash);
|
||||
return;
|
||||
}
|
||||
|
||||
socketLog.debug("rx:", data[0]);
|
||||
const command = this.parse_message(data[0]);
|
||||
this.handle_command(command);
|
||||
}
|
||||
}
|
||||
|
||||
public async launch(args: LaunchRequestArguments) {
|
||||
log.info("Starting debug controller in 'launch' mode");
|
||||
|
||||
this.server = net.createServer((socket) => {
|
||||
this.socket = socket;
|
||||
|
||||
socket.on("data", this.on_data.bind(this));
|
||||
|
||||
socket.on("close", (had_error) => {
|
||||
// log.debug("socket close");
|
||||
this.abort();
|
||||
});
|
||||
|
||||
socket.on("end", () => {
|
||||
// log.debug("socket end");
|
||||
this.abort();
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
// log.debug("socket error");
|
||||
// this.session.sendEvent(new TerminatedEvent());
|
||||
// this.stop();
|
||||
});
|
||||
|
||||
socket.on("drain", () => {
|
||||
// log.debug("socket drain");
|
||||
socket.resume();
|
||||
this.draining = false;
|
||||
this.send_buffer();
|
||||
});
|
||||
});
|
||||
|
||||
if (args.port === -1) {
|
||||
args.port = await get_free_port();
|
||||
}
|
||||
|
||||
this.server.listen(args.port, args.address);
|
||||
|
||||
this.start_game(args);
|
||||
}
|
||||
|
||||
public async attach(args: AttachRequestArguments) {
|
||||
log.info("Starting debug controller in 'attach' mode");
|
||||
|
||||
this.server = net.createServer((socket) => {
|
||||
this.socket = socket;
|
||||
|
||||
socket.on("data", this.on_data.bind(this));
|
||||
|
||||
socket.on("close", (had_error) => {
|
||||
// log.debug("socket close");
|
||||
// this.session.sendEvent(new TerminatedEvent());
|
||||
// this.stop();
|
||||
});
|
||||
|
||||
socket.on("end", () => {
|
||||
// log.debug("socket end");
|
||||
// this.session.sendEvent(new TerminatedEvent());
|
||||
// this.stop();
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
// log.error("socket error", error);
|
||||
});
|
||||
|
||||
socket.on("drain", () => {
|
||||
// log.debug("socket drain");
|
||||
socket.resume();
|
||||
this.draining = false;
|
||||
this.send_buffer();
|
||||
});
|
||||
});
|
||||
|
||||
this.server.listen(args.port, args.address);
|
||||
}
|
||||
|
||||
private parse_message(dataset: any[]) {
|
||||
const command = new Command();
|
||||
let i = 0;
|
||||
command.command = dataset[i++];
|
||||
if (this.connectedVersion[2] >= "2") {
|
||||
command.threadId = dataset[i++];
|
||||
}
|
||||
command.parameters = dataset[i++];
|
||||
return command;
|
||||
}
|
||||
|
||||
private handle_command(command: Command) {
|
||||
switch (command.command) {
|
||||
case "debug_enter": {
|
||||
const reason: string = command.parameters[1];
|
||||
if (reason !== "Breakpoint") {
|
||||
this.set_exception(reason);
|
||||
} else {
|
||||
this.set_exception("");
|
||||
}
|
||||
this.request_stack_dump();
|
||||
break;
|
||||
}
|
||||
case "debug_exit":
|
||||
break;
|
||||
case "message:click_ctrl":
|
||||
// TODO: what is this?
|
||||
break;
|
||||
case "performance:profile_frame":
|
||||
// TODO: what is this?
|
||||
break;
|
||||
case "set_pid":
|
||||
this.threadId = command.threadId;
|
||||
break;
|
||||
case "scene:scene_tree": {
|
||||
const tree = parse_next_scene_node(command.parameters);
|
||||
this.session.sceneTree.fill_tree(tree);
|
||||
break;
|
||||
}
|
||||
case "scene:inspect_object": {
|
||||
const id = BigInt(command.parameters[0]);
|
||||
const className: string = command.parameters[1];
|
||||
const properties: any[] = command.parameters[2];
|
||||
|
||||
const rawObject = new RawObject(className);
|
||||
properties.forEach((prop) => {
|
||||
rawObject.set(prop[0], prop[5]);
|
||||
});
|
||||
const inspectedVariable = { name: "", value: rawObject };
|
||||
build_sub_values(inspectedVariable);
|
||||
if (this.session.inspect_callbacks.has(BigInt(id))) {
|
||||
this.session.inspect_callbacks.get(BigInt(id))(
|
||||
inspectedVariable.name,
|
||||
inspectedVariable
|
||||
);
|
||||
this.session.inspect_callbacks.delete(BigInt(id));
|
||||
}
|
||||
this.session.set_inspection(id, inspectedVariable);
|
||||
break;
|
||||
}
|
||||
case "stack_dump": {
|
||||
const frames: GodotStackFrame[] = [];
|
||||
|
||||
for (let i = 1; i < command.parameters.length; i += 3) {
|
||||
frames.push({
|
||||
id: frames.length,
|
||||
file: command.parameters[i + 0],
|
||||
line: command.parameters[i + 1],
|
||||
function: command.parameters[i + 2],
|
||||
});
|
||||
}
|
||||
|
||||
this.trigger_breakpoint(frames);
|
||||
this.request_scene_tree();
|
||||
break;
|
||||
}
|
||||
case "stack_frame_vars": {
|
||||
this.partialStackVars.reset(command.parameters[0]);
|
||||
this.session.set_scopes(this.partialStackVars);
|
||||
break;
|
||||
}
|
||||
case "stack_frame_var": {
|
||||
this.do_stack_frame_var(
|
||||
command.parameters[0],
|
||||
command.parameters[1],
|
||||
command.parameters[2],
|
||||
command.parameters[3],
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "output": {
|
||||
if (!this.didFirstOutput) {
|
||||
this.didFirstOutput = true;
|
||||
// this.request_scene_tree();
|
||||
}
|
||||
|
||||
debug.activeDebugConsole.appendLine(command.parameters[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abort() {
|
||||
log.info("Aborting debug controller");
|
||||
this.session.sendEvent(new TerminatedEvent());
|
||||
this.stop();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
log.info("Stopping debug controller");
|
||||
killSubProcesses("debug");
|
||||
|
||||
this.socket?.destroy();
|
||||
this.server?.close((error) => {
|
||||
if (error) {
|
||||
log.error(error);
|
||||
}
|
||||
this.server.unref();
|
||||
this.server = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public trigger_breakpoint(stackFrames: GodotStackFrame[]) {
|
||||
let continueStepping = false;
|
||||
const stackCount = stackFrames.length;
|
||||
if (stackCount === 0) {
|
||||
// Engine code is being executed, no user stack trace
|
||||
this.session.debug_data.last_frames = [];
|
||||
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
|
||||
return;
|
||||
}
|
||||
|
||||
const file = stackFrames[0].file.replace("res://", `${this.session.debug_data.projectPath}/`);
|
||||
const line = stackFrames[0].line;
|
||||
|
||||
if (this.steppingOut) {
|
||||
const breakpoint = this.session.debug_data
|
||||
.get_breakpoints(file)
|
||||
.find((bp) => bp.line === line);
|
||||
if (!breakpoint) {
|
||||
if (this.session.debug_data.stack_count > 1) {
|
||||
continueStepping = this.session.debug_data.stack_count === stackCount;
|
||||
} else {
|
||||
const fileSame =
|
||||
stackFrames[0].file === this.session.debug_data.last_frame.file;
|
||||
const funcSame =
|
||||
stackFrames[0].function === this.session.debug_data.last_frame.function;
|
||||
const lineGreater =
|
||||
stackFrames[0].line >= this.session.debug_data.last_frame.line;
|
||||
|
||||
continueStepping = fileSame && funcSame && lineGreater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.session.debug_data.stack_count = stackCount;
|
||||
this.session.debug_data.last_frame = stackFrames[0];
|
||||
this.session.debug_data.last_frames = stackFrames;
|
||||
|
||||
if (continueStepping) {
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
|
||||
this.steppingOut = false;
|
||||
|
||||
this.session.debug_data.stack_files = stackFrames.map((sf) => {
|
||||
return sf.file;
|
||||
});
|
||||
|
||||
if (this.exception.length === 0) {
|
||||
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
|
||||
} else {
|
||||
this.session.set_exception(true);
|
||||
this.session.sendEvent(
|
||||
new StoppedEvent("exception", 0, this.exception)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private send_command(command: string, parameters?: any[]) {
|
||||
const commandArray: any[] = [command];
|
||||
if (this.connectedVersion[2] >= "2") {
|
||||
commandArray.push(this.threadId);
|
||||
}
|
||||
commandArray.push(parameters ?? []);
|
||||
socketLog.debug("tx:", commandArray);
|
||||
const buffer = this.encoder.encode_variant(commandArray);
|
||||
this.commandBuffer.push(buffer);
|
||||
this.send_buffer();
|
||||
}
|
||||
|
||||
private send_buffer() {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!this.draining && this.commandBuffer.length > 0) {
|
||||
const command = this.commandBuffer.shift();
|
||||
this.draining = !this.socket.write(command);
|
||||
}
|
||||
}
|
||||
|
||||
private do_stack_frame_var(
|
||||
name: string,
|
||||
scope: 0 | 1 | 2, // 0 = locals, 1 = members, 2 = globals
|
||||
type: bigint,
|
||||
value: any,
|
||||
) {
|
||||
if (this.partialStackVars.remaining === 0) {
|
||||
throw new Error("More stack frame variables were sent than expected.");
|
||||
}
|
||||
|
||||
const variable: GodotVariable = { name, value, type };
|
||||
build_sub_values(variable);
|
||||
|
||||
const scopeName = ["locals", "members", "globals"][scope];
|
||||
this.partialStackVars[scopeName].push(variable);
|
||||
this.partialStackVars.remaining--;
|
||||
|
||||
if (this.partialStackVars.remaining === 0) {
|
||||
this.session.set_scopes(this.partialStackVars);
|
||||
}
|
||||
}
|
||||
}
|
||||
652
src/debugger/godot4/variables/variant_decoder.ts
Normal file
652
src/debugger/godot4/variables/variant_decoder.ts
Normal file
@@ -0,0 +1,652 @@
|
||||
import {
|
||||
GDScriptTypes,
|
||||
BufferModel,
|
||||
Vector3,
|
||||
Vector2,
|
||||
Basis,
|
||||
AABB,
|
||||
Color,
|
||||
NodePath,
|
||||
ObjectId,
|
||||
Plane,
|
||||
Quat,
|
||||
Rect2,
|
||||
Transform3D,
|
||||
Transform2D,
|
||||
RawObject,
|
||||
Vector2i,
|
||||
Vector3i,
|
||||
Rect2i,
|
||||
Vector4,
|
||||
Vector4i,
|
||||
StringName,
|
||||
Projection,
|
||||
ENCODE_FLAG_64,
|
||||
ENCODE_FLAG_OBJECT_AS_ID,
|
||||
RID,
|
||||
Callable,
|
||||
Signal,
|
||||
} from "./variants";
|
||||
|
||||
export class VariantDecoder {
|
||||
public decode_variant(model: BufferModel) {
|
||||
const type = this.decode_UInt32(model);
|
||||
switch (type & 0xff) {
|
||||
case GDScriptTypes.BOOL:
|
||||
return this.decode_UInt32(model) !== 0;
|
||||
case GDScriptTypes.INT:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Int64(model);
|
||||
} else {
|
||||
return this.decode_Int32(model);
|
||||
}
|
||||
case GDScriptTypes.FLOAT:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Float64(model);
|
||||
} else {
|
||||
return this.decode_Float32(model);
|
||||
}
|
||||
case GDScriptTypes.STRING:
|
||||
return this.decode_String(model);
|
||||
case GDScriptTypes.VECTOR2:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Vector2d(model);
|
||||
} else {
|
||||
return this.decode_Vector2f(model);
|
||||
}
|
||||
case GDScriptTypes.VECTOR2I:
|
||||
return this.decode_Vector2i(model);
|
||||
case GDScriptTypes.RECT2:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Rect2d(model);
|
||||
} else {
|
||||
return this.decode_Rect2f(model);
|
||||
}
|
||||
case GDScriptTypes.RECT2I:
|
||||
return this.decode_Rect2i(model);
|
||||
case GDScriptTypes.VECTOR3:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Vector3d(model);
|
||||
} else {
|
||||
return this.decode_Vector3f(model);
|
||||
}
|
||||
case GDScriptTypes.VECTOR3I:
|
||||
return this.decode_Vector3i(model);
|
||||
case GDScriptTypes.TRANSFORM2D:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Transform2Dd(model);
|
||||
} else {
|
||||
return this.decode_Transform2Df(model);
|
||||
}
|
||||
case GDScriptTypes.PLANE:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Planed(model);
|
||||
} else {
|
||||
return this.decode_Planef(model);
|
||||
}
|
||||
case GDScriptTypes.VECTOR4:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Vector4d(model);
|
||||
} else {
|
||||
return this.decode_Vector4f(model);
|
||||
}
|
||||
case GDScriptTypes.VECTOR4I:
|
||||
return this.decode_Vector4i(model);
|
||||
case GDScriptTypes.QUATERNION:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Quaterniond(model);
|
||||
} else {
|
||||
return this.decode_Quaternionf(model);
|
||||
}
|
||||
case GDScriptTypes.AABB:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_AABBd(model);
|
||||
} else {
|
||||
return this.decode_AABBf(model);
|
||||
}
|
||||
case GDScriptTypes.BASIS:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Basisd(model);
|
||||
} else {
|
||||
return this.decode_Basisf(model);
|
||||
}
|
||||
case GDScriptTypes.TRANSFORM3D:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Transform3Dd(model);
|
||||
} else {
|
||||
return this.decode_Transform3Df(model);
|
||||
}
|
||||
case GDScriptTypes.PROJECTION:
|
||||
if (type & ENCODE_FLAG_64) {
|
||||
return this.decode_Projectiond(model);
|
||||
} else {
|
||||
return this.decode_Projectionf(model);
|
||||
}
|
||||
case GDScriptTypes.COLOR:
|
||||
return this.decode_Color(model);
|
||||
case GDScriptTypes.STRING_NAME:
|
||||
return this.decode_StringName(model);
|
||||
case GDScriptTypes.NODE_PATH:
|
||||
return this.decode_NodePath(model);
|
||||
case GDScriptTypes.RID:
|
||||
return this.decode_RID(model);
|
||||
case GDScriptTypes.OBJECT:
|
||||
if (type & ENCODE_FLAG_OBJECT_AS_ID) {
|
||||
return this.decode_Object_id(model);
|
||||
} else {
|
||||
return this.decode_Object(model);
|
||||
}
|
||||
case GDScriptTypes.CALLABLE:
|
||||
return this.decode_Callable(model);
|
||||
case GDScriptTypes.SIGNAL:
|
||||
return this.decode_Signal(model);
|
||||
case GDScriptTypes.DICTIONARY:
|
||||
return this.decode_Dictionary(model);
|
||||
case GDScriptTypes.ARRAY:
|
||||
return this.decode_Array(model);
|
||||
case GDScriptTypes.PACKED_BYTE_ARRAY:
|
||||
return this.decode_PackedByteArray(model);
|
||||
case GDScriptTypes.PACKED_INT32_ARRAY:
|
||||
return this.decode_PackedInt32Array(model);
|
||||
case GDScriptTypes.PACKED_INT64_ARRAY:
|
||||
return this.decode_PackedInt64Array(model);
|
||||
case GDScriptTypes.PACKED_FLOAT32_ARRAY:
|
||||
return this.decode_PackedFloat32Array(model);
|
||||
case GDScriptTypes.PACKED_FLOAT64_ARRAY:
|
||||
return this.decode_PackedFloat32Array(model);
|
||||
case GDScriptTypes.PACKED_STRING_ARRAY:
|
||||
return this.decode_PackedStringArray(model);
|
||||
case GDScriptTypes.PACKED_VECTOR2_ARRAY:
|
||||
if (type & ENCODE_FLAG_OBJECT_AS_ID) {
|
||||
return this.decode_PackedVector2dArray(model);
|
||||
} else {
|
||||
return this.decode_PackedVector2fArray(model);
|
||||
}
|
||||
case GDScriptTypes.PACKED_VECTOR3_ARRAY:
|
||||
if (type & ENCODE_FLAG_OBJECT_AS_ID) {
|
||||
return this.decode_PackedVector3dArray(model);
|
||||
} else {
|
||||
return this.decode_PackedVector3fArray(model);
|
||||
}
|
||||
case GDScriptTypes.PACKED_COLOR_ARRAY:
|
||||
return this.decode_PackedColorArray(model);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public get_dataset(buffer: Buffer) {
|
||||
const len = buffer.readUInt32LE(0);
|
||||
if (buffer.length != len + 4) {
|
||||
return undefined;
|
||||
}
|
||||
const model: BufferModel = {
|
||||
buffer: buffer,
|
||||
offset: 4, // data starts after the initial length
|
||||
len: len,
|
||||
};
|
||||
|
||||
const output = [];
|
||||
output.push(len + 4);
|
||||
do {
|
||||
const value = this.decode_variant(model);
|
||||
if (value === undefined) {
|
||||
throw new Error("Unable to decode variant.");
|
||||
}
|
||||
output.push(value);
|
||||
} while (model.len > 0);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_AABBf(model: BufferModel) {
|
||||
return new AABB(this.decode_Vector3f(model), this.decode_Vector3f(model));
|
||||
}
|
||||
|
||||
private decode_AABBd(model: BufferModel) {
|
||||
return new AABB(this.decode_Vector3d(model), this.decode_Vector3d(model));
|
||||
}
|
||||
|
||||
private decode_Array(model: BufferModel) {
|
||||
const output: Array<any> = [];
|
||||
|
||||
const count = this.decode_UInt32(model);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const value = this.decode_variant(model);
|
||||
output.push(value);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_Basisf(model: BufferModel) {
|
||||
return new Basis(
|
||||
this.decode_Vector3f(model),
|
||||
this.decode_Vector3f(model),
|
||||
this.decode_Vector3f(model)
|
||||
);
|
||||
}
|
||||
|
||||
private decode_Basisd(model: BufferModel) {
|
||||
return new Basis(
|
||||
this.decode_Vector3d(model),
|
||||
this.decode_Vector3d(model),
|
||||
this.decode_Vector3d(model)
|
||||
);
|
||||
}
|
||||
|
||||
private decode_Color(model: BufferModel) {
|
||||
const rgb = this.decode_Vector3f(model);
|
||||
const a = this.decode_Float32(model);
|
||||
|
||||
return new Color(rgb.x, rgb.y, rgb.z, a);
|
||||
}
|
||||
|
||||
private decode_Dictionary(model: BufferModel) {
|
||||
const output = new Map<any, any>();
|
||||
|
||||
const count = this.decode_UInt32(model);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const key = this.decode_variant(model);
|
||||
const value = this.decode_variant(model);
|
||||
output.set(key, value);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_Float32(model: BufferModel) {
|
||||
const f = model.buffer.readFloatLE(model.offset);
|
||||
|
||||
model.offset += 4;
|
||||
model.len -= 4;
|
||||
|
||||
return f; // + (f < 0 ? -1e-10 : 1e-10);
|
||||
}
|
||||
|
||||
private decode_Float64(model: BufferModel) {
|
||||
const f = model.buffer.readDoubleLE(model.offset);
|
||||
|
||||
model.offset += 8;
|
||||
model.len -= 8;
|
||||
|
||||
return f; // + (f < 0 ? -1e-10 : 1e-10);
|
||||
}
|
||||
|
||||
private decode_Int32(model: BufferModel) {
|
||||
const result = model.buffer.readInt32LE(model.offset);
|
||||
|
||||
model.len -= 4;
|
||||
model.offset += 4;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_UInt32(model: BufferModel) {
|
||||
const result = model.buffer.readUInt32LE(model.offset);
|
||||
model.len -= 4;
|
||||
model.offset += 4;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_Int64(model: BufferModel) {
|
||||
const result = model.buffer.readBigInt64LE(model.offset);
|
||||
model.len -= 8;
|
||||
model.offset += 8;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_UInt64(model: BufferModel) {
|
||||
const result = model.buffer.readBigUInt64LE(model.offset);
|
||||
model.len -= 8;
|
||||
model.offset += 8;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private decode_NodePath(model: BufferModel) {
|
||||
const name_count = this.decode_UInt32(model) & 0x7fffffff;
|
||||
let subname_count = this.decode_UInt32(model);
|
||||
const flags = this.decode_UInt32(model);
|
||||
const is_absolute = (flags & 1) === 1;
|
||||
if (flags & 2) {
|
||||
//Obsolete format with property separate from subPath
|
||||
subname_count++;
|
||||
}
|
||||
|
||||
const total = name_count + subname_count;
|
||||
const names: string[] = [];
|
||||
const sub_names: string[] = [];
|
||||
for (let i = 0; i < total; i++) {
|
||||
const str = this.decode_String(model);
|
||||
if (i < name_count) {
|
||||
names.push(str);
|
||||
} else {
|
||||
sub_names.push(str);
|
||||
}
|
||||
}
|
||||
|
||||
return new NodePath(names, sub_names, is_absolute);
|
||||
}
|
||||
|
||||
private decode_Object(model: BufferModel) {
|
||||
const class_name = this.decode_String(model);
|
||||
const prop_count = this.decode_UInt32(model);
|
||||
const output = new RawObject(class_name);
|
||||
|
||||
for (let i = 0; i < prop_count; i++) {
|
||||
const name = this.decode_String(model);
|
||||
const value = this.decode_variant(model);
|
||||
output.set(name, value);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_Object_id(model: BufferModel) {
|
||||
const id = this.decode_UInt64(model);
|
||||
|
||||
return new ObjectId(id);
|
||||
}
|
||||
|
||||
private decode_RID(model: BufferModel) {
|
||||
const id = this.decode_UInt64(model);
|
||||
|
||||
return new RID(id);
|
||||
}
|
||||
|
||||
private decode_Callable(model: BufferModel) {
|
||||
return new Callable();
|
||||
}
|
||||
|
||||
private decode_Signal(model: BufferModel) {
|
||||
return new Signal(this.decode_String(model), this.decode_Object_id(model));
|
||||
}
|
||||
|
||||
private decode_Planef(model: BufferModel) {
|
||||
const x = this.decode_Float32(model);
|
||||
const y = this.decode_Float32(model);
|
||||
const z = this.decode_Float32(model);
|
||||
const d = this.decode_Float32(model);
|
||||
|
||||
return new Plane(x, y, z, d);
|
||||
}
|
||||
|
||||
private decode_Planed(model: BufferModel) {
|
||||
const x = this.decode_Float64(model);
|
||||
const y = this.decode_Float64(model);
|
||||
const z = this.decode_Float64(model);
|
||||
const d = this.decode_Float64(model);
|
||||
|
||||
return new Plane(x, y, z, d);
|
||||
}
|
||||
|
||||
private decode_PackedByteArray(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(model.buffer.readUInt8(model.offset));
|
||||
model.offset++;
|
||||
model.len--;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedColorArray(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Color[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Color(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedFloat32Array(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Float32(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedFloat64Array(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Float64(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedInt32Array(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: number[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Int32(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedInt64Array(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: bigint[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Int64(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedStringArray(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_String(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedVector2fArray(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Vector2[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Vector2f(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedVector3fArray(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Vector3[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Vector3f(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedVector2dArray(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Vector2[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Vector2d(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_PackedVector3dArray(model: BufferModel) {
|
||||
const count = this.decode_UInt32(model);
|
||||
const output: Vector3[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
output.push(this.decode_Vector3d(model));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private decode_Quaternionf(model: BufferModel) {
|
||||
const x = this.decode_Float32(model);
|
||||
const y = this.decode_Float32(model);
|
||||
const z = this.decode_Float32(model);
|
||||
const w = this.decode_Float32(model);
|
||||
|
||||
return new Quat(x, y, z, w);
|
||||
}
|
||||
|
||||
private decode_Quaterniond(model: BufferModel) {
|
||||
const x = this.decode_Float64(model);
|
||||
const y = this.decode_Float64(model);
|
||||
const z = this.decode_Float64(model);
|
||||
const w = this.decode_Float64(model);
|
||||
|
||||
return new Quat(x, y, z, w);
|
||||
}
|
||||
|
||||
private decode_Rect2f(model: BufferModel) {
|
||||
return new Rect2(this.decode_Vector2f(model), this.decode_Vector2f(model));
|
||||
}
|
||||
|
||||
private decode_Rect2d(model: BufferModel) {
|
||||
return new Rect2(this.decode_Vector2d(model), this.decode_Vector2d(model));
|
||||
}
|
||||
|
||||
private decode_Rect2i(model: BufferModel) {
|
||||
return new Rect2i(this.decode_Vector2f(model), this.decode_Vector2f(model));
|
||||
}
|
||||
|
||||
private decode_String(model: BufferModel) {
|
||||
let len = this.decode_UInt32(model);
|
||||
let pad = 0;
|
||||
if (len % 4 !== 0) {
|
||||
pad = 4 - (len % 4);
|
||||
}
|
||||
|
||||
const str = model.buffer.toString("utf8", model.offset, model.offset + len);
|
||||
len += pad;
|
||||
|
||||
model.offset += len;
|
||||
model.len -= len;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private decode_StringName(model: BufferModel) {
|
||||
return new StringName(this.decode_String(model));
|
||||
}
|
||||
|
||||
private decode_Transform3Df(model: BufferModel) {
|
||||
return new Transform3D(this.decode_Basisf(model), this.decode_Vector3f(model));
|
||||
}
|
||||
|
||||
private decode_Transform3Dd(model: BufferModel) {
|
||||
return new Transform3D(this.decode_Basisd(model), this.decode_Vector3d(model));
|
||||
}
|
||||
|
||||
private decode_Projectionf(model: BufferModel) {
|
||||
return new Projection(this.decode_Vector4f(model), this.decode_Vector4f(model), this.decode_Vector4f(model), this.decode_Vector4f(model));
|
||||
}
|
||||
|
||||
private decode_Projectiond(model: BufferModel) {
|
||||
return new Projection(this.decode_Vector4d(model), this.decode_Vector4d(model), this.decode_Vector4d(model), this.decode_Vector4d(model));
|
||||
}
|
||||
|
||||
private decode_Transform2Df(model: BufferModel) {
|
||||
return new Transform2D(
|
||||
this.decode_Vector2f(model),
|
||||
this.decode_Vector2f(model),
|
||||
this.decode_Vector2f(model)
|
||||
);
|
||||
}
|
||||
|
||||
private decode_Transform2Dd(model: BufferModel) {
|
||||
return new Transform2D(
|
||||
this.decode_Vector2d(model),
|
||||
this.decode_Vector2d(model),
|
||||
this.decode_Vector2d(model)
|
||||
);
|
||||
}
|
||||
|
||||
private decode_Vector2f(model: BufferModel) {
|
||||
const x = this.decode_Float32(model);
|
||||
const y = this.decode_Float32(model);
|
||||
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
private decode_Vector2d(model: BufferModel) {
|
||||
const x = this.decode_Float64(model);
|
||||
const y = this.decode_Float64(model);
|
||||
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
private decode_Vector2i(model: BufferModel) {
|
||||
const x = this.decode_Int32(model);
|
||||
const y = this.decode_Int32(model);
|
||||
|
||||
return new Vector2i(x, y);
|
||||
}
|
||||
|
||||
private decode_Vector3f(model: BufferModel) {
|
||||
const x = this.decode_Float32(model);
|
||||
const y = this.decode_Float32(model);
|
||||
const z = this.decode_Float32(model);
|
||||
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
|
||||
private decode_Vector3d(model: BufferModel) {
|
||||
const x = this.decode_Float64(model);
|
||||
const y = this.decode_Float64(model);
|
||||
const z = this.decode_Float64(model);
|
||||
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
|
||||
private decode_Vector3i(model: BufferModel) {
|
||||
const x = this.decode_Int32(model);
|
||||
const y = this.decode_Int32(model);
|
||||
const z = this.decode_Int32(model);
|
||||
|
||||
return new Vector3i(x, y, z);
|
||||
}
|
||||
|
||||
private decode_Vector4f(model: BufferModel) {
|
||||
const x = this.decode_Float32(model);
|
||||
const y = this.decode_Float32(model);
|
||||
const z = this.decode_Float32(model);
|
||||
const w = this.decode_Float32(model);
|
||||
|
||||
return new Vector4(x, y, z, w);
|
||||
}
|
||||
|
||||
private decode_Vector4d(model: BufferModel) {
|
||||
const x = this.decode_Float64(model);
|
||||
const y = this.decode_Float64(model);
|
||||
const z = this.decode_Float64(model);
|
||||
const w = this.decode_Float64(model);
|
||||
|
||||
return new Vector4(x, y, z, w);
|
||||
}
|
||||
|
||||
private decode_Vector4i(model: BufferModel) {
|
||||
const x = this.decode_Int32(model);
|
||||
const y = this.decode_Int32(model);
|
||||
const z = this.decode_Int32(model);
|
||||
const w = this.decode_Int32(model);
|
||||
|
||||
return new Vector4i(x, y, z, w);
|
||||
}
|
||||
}
|
||||
446
src/debugger/godot4/variables/variant_encoder.ts
Normal file
446
src/debugger/godot4/variables/variant_encoder.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import {
|
||||
GDScriptTypes,
|
||||
BufferModel,
|
||||
Vector3,
|
||||
Vector2,
|
||||
Basis,
|
||||
AABB,
|
||||
Color,
|
||||
Plane,
|
||||
Quat,
|
||||
Rect2,
|
||||
Transform3D,
|
||||
Transform2D,
|
||||
Vector3i,
|
||||
Vector2i,
|
||||
Rect2i,
|
||||
Vector4i,
|
||||
Vector4,
|
||||
StringName,
|
||||
Projection,
|
||||
ENCODE_FLAG_64,
|
||||
} from "./variants";
|
||||
|
||||
export class VariantEncoder {
|
||||
public encode_variant(
|
||||
value:
|
||||
| number
|
||||
| bigint
|
||||
| boolean
|
||||
| string
|
||||
| Map<any, any>
|
||||
| Array<any>
|
||||
| object
|
||||
| undefined,
|
||||
model?: BufferModel
|
||||
) {
|
||||
if (
|
||||
typeof value === "number" &&
|
||||
Number.isInteger(value) &&
|
||||
(value > 2147483647 || value < -2147483648)
|
||||
) {
|
||||
value = BigInt(value);
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
const size = this.size_variant(value);
|
||||
const buffer = Buffer.alloc(size + 4);
|
||||
model = {
|
||||
buffer: buffer,
|
||||
offset: 0,
|
||||
len: 0,
|
||||
};
|
||||
this.encode_UInt32(size, model);
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
{
|
||||
const is_integer = Number.isInteger(value);
|
||||
if (is_integer) {
|
||||
this.encode_UInt32(GDScriptTypes.INT, model);
|
||||
this.encode_UInt32(value, model);
|
||||
} else {
|
||||
this.encode_UInt32(GDScriptTypes.FLOAT, model);
|
||||
this.encode_Float32(value, model);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "bigint":
|
||||
this.encode_UInt32(GDScriptTypes.INT | ENCODE_FLAG_64, model);
|
||||
this.encode_UInt64(value, model);
|
||||
break;
|
||||
case "boolean":
|
||||
this.encode_UInt32(GDScriptTypes.BOOL, model);
|
||||
this.encode_Bool(value, model);
|
||||
break;
|
||||
case "string":
|
||||
this.encode_UInt32(GDScriptTypes.STRING, model);
|
||||
this.encode_String(value, model);
|
||||
break;
|
||||
case "undefined":
|
||||
break;
|
||||
default:
|
||||
if (Array.isArray(value)) {
|
||||
this.encode_UInt32(GDScriptTypes.ARRAY, model);
|
||||
this.encode_Array(value, model);
|
||||
} else if (value instanceof Map) {
|
||||
this.encode_UInt32(GDScriptTypes.DICTIONARY, model);
|
||||
this.encode_Dictionary(value, model);
|
||||
} else {
|
||||
if (value instanceof Vector2i) {
|
||||
this.encode_UInt32(GDScriptTypes.VECTOR2I, model);
|
||||
this.encode_Vector2i(value, model);
|
||||
} else if (value instanceof Vector2) {
|
||||
this.encode_UInt32(GDScriptTypes.VECTOR2, model);
|
||||
this.encode_Vector2(value, model);
|
||||
} else if (value instanceof Rect2i) {
|
||||
this.encode_UInt32(GDScriptTypes.RECT2I, model);
|
||||
this.encode_Rect2i(value, model);
|
||||
} else if (value instanceof Rect2) {
|
||||
this.encode_UInt32(GDScriptTypes.RECT2, model);
|
||||
this.encode_Rect2(value, model);
|
||||
} else if (value instanceof Vector3i) {
|
||||
this.encode_UInt32(GDScriptTypes.VECTOR3I, model);
|
||||
this.encode_Vector3i(value, model);
|
||||
} else if (value instanceof Vector3) {
|
||||
this.encode_UInt32(GDScriptTypes.VECTOR3, model);
|
||||
this.encode_Vector3(value, model);
|
||||
} else if (value instanceof Vector4i) {
|
||||
this.encode_UInt32(GDScriptTypes.VECTOR4I, model);
|
||||
this.encode_Vector4i(value, model);
|
||||
} else if (value instanceof Vector4) {
|
||||
this.encode_UInt32(GDScriptTypes.VECTOR4, model);
|
||||
this.encode_Vector4(value, model);
|
||||
} else if (value instanceof Transform2D) {
|
||||
this.encode_UInt32(GDScriptTypes.TRANSFORM2D, model);
|
||||
this.encode_Transform2D(value, model);
|
||||
} else if (value instanceof StringName) {
|
||||
this.encode_UInt32(GDScriptTypes.STRING_NAME, model);
|
||||
this.encode_StringName(value, model);
|
||||
} else if (value instanceof Plane) {
|
||||
this.encode_UInt32(GDScriptTypes.PLANE, model);
|
||||
this.encode_Plane(value, model);
|
||||
} else if (value instanceof Projection) {
|
||||
this.encode_UInt32(GDScriptTypes.PROJECTION, model);
|
||||
this.encode_Projection(value, model);
|
||||
} else if (value instanceof Quat) {
|
||||
this.encode_UInt32(GDScriptTypes.QUATERNION, model);
|
||||
this.encode_Quaternion(value, model);
|
||||
} else if (value instanceof AABB) {
|
||||
this.encode_UInt32(GDScriptTypes.AABB, model);
|
||||
this.encode_AABB(value, model);
|
||||
} else if (value instanceof Basis) {
|
||||
this.encode_UInt32(GDScriptTypes.BASIS, model);
|
||||
this.encode_Basis(value, model);
|
||||
} else if (value instanceof Transform3D) {
|
||||
this.encode_UInt32(GDScriptTypes.TRANSFORM3D, model);
|
||||
this.encode_Transform3D(value, model);
|
||||
} else if (value instanceof Color) {
|
||||
this.encode_UInt32(GDScriptTypes.COLOR, model);
|
||||
this.encode_Color(value, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model.buffer;
|
||||
}
|
||||
|
||||
private encode_AABB(value: AABB, model: BufferModel) {
|
||||
this.encode_Vector3(value.position, model);
|
||||
this.encode_Vector3(value.size, model);
|
||||
}
|
||||
|
||||
private encode_Array(arr: any[], model: BufferModel) {
|
||||
const size = arr.length;
|
||||
this.encode_UInt32(size, model);
|
||||
arr.forEach((e) => {
|
||||
this.encode_variant(e, model);
|
||||
});
|
||||
}
|
||||
|
||||
private encode_Basis(value: Basis, model: BufferModel) {
|
||||
this.encode_Vector3(value.x, model);
|
||||
this.encode_Vector3(value.y, model);
|
||||
this.encode_Vector3(value.z, model);
|
||||
}
|
||||
|
||||
private encode_Bool(bool: boolean, model: BufferModel) {
|
||||
this.encode_UInt32(bool ? 1 : 0, model);
|
||||
}
|
||||
|
||||
private encode_Color(value: Color, model: BufferModel) {
|
||||
this.encode_Float32(value.r, model);
|
||||
this.encode_Float32(value.g, model);
|
||||
this.encode_Float32(value.b, model);
|
||||
this.encode_Float32(value.a, model);
|
||||
}
|
||||
|
||||
private encode_Dictionary(dict: Map<any, any>, model: BufferModel) {
|
||||
const size = dict.size;
|
||||
this.encode_UInt32(size, model);
|
||||
const keys = Array.from(dict.keys());
|
||||
keys.forEach((key) => {
|
||||
const value = dict.get(key);
|
||||
this.encode_variant(key, model);
|
||||
this.encode_variant(value, model);
|
||||
});
|
||||
}
|
||||
|
||||
private encode_Float64(value: number, model: BufferModel) {
|
||||
model.buffer.writeDoubleLE(value, model.offset);
|
||||
model.offset += 8;
|
||||
}
|
||||
|
||||
private encode_Float32(value: number, model: BufferModel) {
|
||||
model.buffer.writeFloatLE(value, model.offset);
|
||||
model.offset += 4;
|
||||
}
|
||||
|
||||
private encode_Plane(value: Plane, model: BufferModel) {
|
||||
this.encode_Float32(value.x, model);
|
||||
this.encode_Float32(value.y, model);
|
||||
this.encode_Float32(value.z, model);
|
||||
this.encode_Float32(value.d, model);
|
||||
}
|
||||
|
||||
private encode_Quaternion(value: Quat, model: BufferModel) {
|
||||
this.encode_Float32(value.x, model);
|
||||
this.encode_Float32(value.y, model);
|
||||
this.encode_Float32(value.z, model);
|
||||
this.encode_Float32(value.w, model);
|
||||
}
|
||||
|
||||
private encode_Rect2(value: Rect2, model: BufferModel) {
|
||||
this.encode_Vector2(value.position, model);
|
||||
this.encode_Vector2(value.size, model);
|
||||
}
|
||||
|
||||
private encode_Rect2i(value: Rect2i, model: BufferModel) {
|
||||
this.encode_Vector2i(value.position, model);
|
||||
this.encode_Vector2i(value.size, model);
|
||||
}
|
||||
|
||||
private encode_String(str: string, model: BufferModel) {
|
||||
let str_len = str.length;
|
||||
this.encode_UInt32(str_len, model);
|
||||
model.buffer.write(str, model.offset, str_len, "utf8");
|
||||
model.offset += str_len;
|
||||
str_len += 4;
|
||||
while (str_len % 4) {
|
||||
str_len++;
|
||||
model.buffer.writeUInt8(0, model.offset);
|
||||
model.offset++;
|
||||
}
|
||||
}
|
||||
|
||||
private encode_Transform3D(value: Transform3D, model: BufferModel) {
|
||||
this.encode_Basis(value.basis, model);
|
||||
this.encode_Vector3(value.origin, model);
|
||||
}
|
||||
|
||||
private encode_Transform2D(value: Transform2D, model: BufferModel) {
|
||||
this.encode_Vector2(value.origin, model);
|
||||
this.encode_Vector2(value.x, model);
|
||||
this.encode_Vector2(value.y, model);
|
||||
}
|
||||
|
||||
private encode_Projection(value: Projection, model: BufferModel) {
|
||||
this.encode_Vector4(value.x, model);
|
||||
this.encode_Vector4(value.y, model);
|
||||
this.encode_Vector4(value.z, model);
|
||||
this.encode_Vector4(value.w, model);
|
||||
}
|
||||
|
||||
private encode_UInt32(int: number, model: BufferModel) {
|
||||
model.buffer.writeUInt32LE(int, model.offset);
|
||||
model.offset += 4;
|
||||
}
|
||||
|
||||
private encode_Int32(int: number, model: BufferModel) {
|
||||
model.buffer.writeInt32LE(int, model.offset);
|
||||
model.offset += 4;
|
||||
}
|
||||
|
||||
private encode_UInt64(value: bigint, model: BufferModel) {
|
||||
model.buffer.writeBigUInt64LE(value, model.offset);
|
||||
model.offset += 8;
|
||||
}
|
||||
|
||||
private encode_Vector2(value: Vector2, model: BufferModel) {
|
||||
this.encode_Float32(value.x, model);
|
||||
this.encode_Float32(value.y, model);
|
||||
}
|
||||
|
||||
private encode_Vector3(value: Vector3, model: BufferModel) {
|
||||
this.encode_Float32(value.x, model);
|
||||
this.encode_Float32(value.y, model);
|
||||
this.encode_Float32(value.z, model);
|
||||
}
|
||||
|
||||
private encode_Vector4(value: Vector4, model: BufferModel) {
|
||||
this.encode_Float32(value.x, model);
|
||||
this.encode_Float32(value.y, model);
|
||||
this.encode_Float32(value.z, model);
|
||||
this.encode_Float32(value.w, model);
|
||||
}
|
||||
|
||||
private encode_Vector2i(value: Vector2i, model: BufferModel) {
|
||||
this.encode_Int32(value.x, model);
|
||||
this.encode_Int32(value.y, model);
|
||||
}
|
||||
|
||||
private encode_Vector3i(value: Vector3i, model: BufferModel) {
|
||||
this.encode_Int32(value.x, model);
|
||||
this.encode_Int32(value.y, model);
|
||||
this.encode_Int32(value.z, model);
|
||||
}
|
||||
|
||||
private encode_Vector4i(value: Vector4i, model: BufferModel) {
|
||||
this.encode_Int32(value.x, model);
|
||||
this.encode_Int32(value.y, model);
|
||||
this.encode_Int32(value.z, model);
|
||||
this.encode_Int32(value.w, model);
|
||||
}
|
||||
|
||||
private encode_StringName(value: StringName, model: BufferModel) {
|
||||
this.encode_String(value.value, model);
|
||||
}
|
||||
|
||||
private size_Bool(): number {
|
||||
return this.size_UInt32();
|
||||
}
|
||||
|
||||
private size_Dictionary(dict: Map<any, any>): number {
|
||||
let size = this.size_UInt32();
|
||||
const keys = Array.from(dict.keys());
|
||||
keys.forEach((key) => {
|
||||
const value = dict.get(key);
|
||||
size += this.size_variant(key);
|
||||
size += this.size_variant(value);
|
||||
});
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private size_String(str: string): number {
|
||||
let size = this.size_UInt32() + str.length;
|
||||
while (size % 4) {
|
||||
size++;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
private size_UInt32(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
private size_UInt64(): number {
|
||||
return 8;
|
||||
}
|
||||
|
||||
private size_array(arr: any[]): number {
|
||||
let size = this.size_UInt32();
|
||||
arr.forEach((e) => {
|
||||
size += this.size_variant(e);
|
||||
});
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private size_variant(
|
||||
value:
|
||||
| number
|
||||
| bigint
|
||||
| boolean
|
||||
| string
|
||||
| Map<any, any>
|
||||
| any[]
|
||||
| object
|
||||
| undefined
|
||||
): number {
|
||||
let size = 4;
|
||||
|
||||
if (
|
||||
typeof value === "number" &&
|
||||
(value > 2147483647 || value < -2147483648)
|
||||
) {
|
||||
value = BigInt(value);
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
size += this.size_UInt32();
|
||||
break;
|
||||
case "bigint":
|
||||
size += this.size_UInt64();
|
||||
break;
|
||||
case "boolean":
|
||||
size += this.size_Bool();
|
||||
break;
|
||||
case "string":
|
||||
size += this.size_String(value);
|
||||
break;
|
||||
case "undefined":
|
||||
break;
|
||||
default:
|
||||
// TODO: size of nodepath, rid, object, callable, signal
|
||||
if (Array.isArray(value)) {
|
||||
size += this.size_array(value);
|
||||
break;
|
||||
} else if (value instanceof Map) {
|
||||
size += this.size_Dictionary(value);
|
||||
break;
|
||||
} else if (value instanceof StringName) {
|
||||
size += this.size_String(value.value);
|
||||
break;
|
||||
} else {
|
||||
switch (value["__type__"]) {
|
||||
case "Vector2":
|
||||
case "Vector2i":
|
||||
size += this.size_UInt32() * 2;
|
||||
break;
|
||||
case "Rect2":
|
||||
case "Rect2i":
|
||||
size += this.size_UInt32() * 4;
|
||||
break;
|
||||
case "Vector3":
|
||||
case "Vector3i":
|
||||
size += this.size_UInt32() * 3;
|
||||
break;
|
||||
case "Vector4":
|
||||
case "Vector4i":
|
||||
size += this.size_UInt32() * 4;
|
||||
break;
|
||||
case "Transform2D":
|
||||
size += this.size_UInt32() * 6;
|
||||
break;
|
||||
case "Projection":
|
||||
size += this.size_UInt32() * 16;
|
||||
break;
|
||||
case "Plane":
|
||||
size += this.size_UInt32() * 4;
|
||||
break;
|
||||
case "Quaternion":
|
||||
size += this.size_UInt32() * 4;
|
||||
break;
|
||||
case "AABB":
|
||||
size += this.size_UInt32() * 6;
|
||||
break;
|
||||
case "Basis":
|
||||
size += this.size_UInt32() * 9;
|
||||
break;
|
||||
case "Transform3D":
|
||||
size += this.size_UInt32() * 12;
|
||||
break;
|
||||
case "Color":
|
||||
size += this.size_UInt32() * 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
475
src/debugger/godot4/variables/variants.ts
Normal file
475
src/debugger/godot4/variables/variants.ts
Normal file
@@ -0,0 +1,475 @@
|
||||
import { GodotVariable } from "../../debug_runtime";
|
||||
|
||||
export enum GDScriptTypes {
|
||||
NIL,
|
||||
|
||||
// atomic types
|
||||
BOOL,
|
||||
INT,
|
||||
FLOAT,
|
||||
STRING,
|
||||
|
||||
// math types
|
||||
VECTOR2,
|
||||
VECTOR2I,
|
||||
RECT2,
|
||||
RECT2I,
|
||||
VECTOR3,
|
||||
VECTOR3I,
|
||||
TRANSFORM2D,
|
||||
VECTOR4,
|
||||
VECTOR4I,
|
||||
PLANE,
|
||||
QUATERNION,
|
||||
AABB,
|
||||
BASIS,
|
||||
TRANSFORM3D,
|
||||
PROJECTION,
|
||||
|
||||
// misc types
|
||||
COLOR,
|
||||
STRING_NAME,
|
||||
NODE_PATH,
|
||||
RID,
|
||||
OBJECT,
|
||||
CALLABLE,
|
||||
SIGNAL,
|
||||
DICTIONARY,
|
||||
ARRAY,
|
||||
|
||||
// typed arrays
|
||||
PACKED_BYTE_ARRAY,
|
||||
PACKED_INT32_ARRAY,
|
||||
PACKED_INT64_ARRAY,
|
||||
PACKED_FLOAT32_ARRAY,
|
||||
PACKED_FLOAT64_ARRAY,
|
||||
PACKED_STRING_ARRAY,
|
||||
PACKED_VECTOR2_ARRAY,
|
||||
PACKED_VECTOR3_ARRAY,
|
||||
PACKED_COLOR_ARRAY,
|
||||
|
||||
VARIANT_MAX
|
||||
}
|
||||
|
||||
export const ENCODE_FLAG_64 = 1 << 16;
|
||||
export const ENCODE_FLAG_OBJECT_AS_ID = 1 << 16;
|
||||
|
||||
export interface BufferModel {
|
||||
buffer: Buffer;
|
||||
len: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface GDObject {
|
||||
stringify_value(): string;
|
||||
sub_values(): GodotVariable[];
|
||||
type_name(): string;
|
||||
}
|
||||
|
||||
function clean_number(value: number) {
|
||||
return +Number.parseFloat(String(value)).toFixed(1);
|
||||
}
|
||||
|
||||
export class Vector3 implements GDObject {
|
||||
constructor(
|
||||
public x: number = 0.0,
|
||||
public y: number = 0.0,
|
||||
public z: number = 0.0
|
||||
) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${clean_number(
|
||||
this.z
|
||||
)})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
{ name: "z", value: this.z },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Vector3";
|
||||
}
|
||||
}
|
||||
|
||||
export class Vector3i extends Vector3 {
|
||||
// TODO: Truncate values in sub_values and stringify_value
|
||||
public type_name(): string {
|
||||
return "Vector3i";
|
||||
}
|
||||
}
|
||||
|
||||
export class Vector4 implements GDObject {
|
||||
constructor(
|
||||
public x: number = 0.0,
|
||||
public y: number = 0.0,
|
||||
public z: number = 0.0,
|
||||
public w: number = 0.0
|
||||
) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${
|
||||
clean_number(this.z)}, ${clean_number(this.w)})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
{ name: "z", value: this.z },
|
||||
{ name: "w", value: this.w },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Vector4";
|
||||
}
|
||||
}
|
||||
|
||||
export class Vector4i extends Vector4 {
|
||||
// TODO: Truncate values in sub_values and stringify_value
|
||||
public type_name(): string {
|
||||
return "Vector4i";
|
||||
}
|
||||
}
|
||||
|
||||
export class Vector2 implements GDObject {
|
||||
constructor(public x: number = 0.0, public y: number = 0.0) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${clean_number(this.x)}, ${clean_number(this.y)})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Vector2";
|
||||
}
|
||||
}
|
||||
|
||||
export class Vector2i extends Vector2 {
|
||||
// TODO: Truncate values in sub_values and stringify_value
|
||||
public type_name(): string {
|
||||
return "Vector2i";
|
||||
}
|
||||
}
|
||||
|
||||
export class Basis implements GDObject {
|
||||
constructor(public x: Vector3, public y: Vector3, public z: Vector3) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${this.x.stringify_value()}, ${this.y.stringify_value()}, ${this.z.stringify_value()})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
{ name: "z", value: this.z },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Basis";
|
||||
}
|
||||
}
|
||||
|
||||
export class AABB implements GDObject {
|
||||
constructor(public position: Vector3, public size: Vector3) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${this.position.stringify_value()}, ${this.size.stringify_value()})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "position", value: this.position },
|
||||
{ name: "size", value: this.size },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "AABB";
|
||||
}
|
||||
}
|
||||
|
||||
export class Color implements GDObject {
|
||||
constructor(
|
||||
public r: number,
|
||||
public g: number,
|
||||
public b: number,
|
||||
public a: number = 1.0
|
||||
) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${clean_number(this.r)}, ${clean_number(this.g)}, ${clean_number(
|
||||
this.b
|
||||
)}, ${clean_number(this.a)})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "r", value: this.r },
|
||||
{ name: "g", value: this.g },
|
||||
{ name: "b", value: this.b },
|
||||
{ name: "a", value: this.a },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Color";
|
||||
}
|
||||
}
|
||||
|
||||
export class NodePath implements GDObject {
|
||||
constructor(
|
||||
public names: string[],
|
||||
public sub_names: string[],
|
||||
public absolute: boolean
|
||||
) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(/${this.names.join("/")}${
|
||||
this.sub_names.length > 0 ? ":" : ""
|
||||
}${this.sub_names.join(":")})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "names", value: this.names },
|
||||
{ name: "sub_names", value: this.sub_names },
|
||||
{ name: "absolute", value: this.absolute },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "NodePath";
|
||||
}
|
||||
}
|
||||
|
||||
export class RawObject extends Map<any, any> {
|
||||
constructor(public class_name: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectId implements GDObject {
|
||||
constructor(public id: bigint) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `<${this.id}>`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [{ name: "id", value: this.id }];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Object";
|
||||
}
|
||||
}
|
||||
|
||||
export class RID extends ObjectId {
|
||||
public type_name(): string {
|
||||
return "RID";
|
||||
}
|
||||
}
|
||||
|
||||
export class Plane implements GDObject {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
public z: number,
|
||||
public d: number
|
||||
) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${clean_number(
|
||||
this.z
|
||||
)}, ${clean_number(this.d)})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
{ name: "z", value: this.z },
|
||||
{ name: "d", value: this.d },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Plane";
|
||||
}
|
||||
}
|
||||
|
||||
export class Quat implements GDObject {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
public z: number,
|
||||
public w: number
|
||||
) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${clean_number(
|
||||
this.z
|
||||
)}, ${clean_number(this.w)})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
{ name: "z", value: this.z },
|
||||
{ name: "w", value: this.w },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Quat";
|
||||
}
|
||||
}
|
||||
|
||||
export class Rect2 implements GDObject {
|
||||
constructor(public position: Vector2, public size: Vector2) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${this.position.stringify_value()} - ${this.size.stringify_value()})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "position", value: this.position },
|
||||
{ name: "size", value: this.size },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Rect2";
|
||||
}
|
||||
}
|
||||
|
||||
export class Rect2i extends Rect2 {
|
||||
// TODO: Truncate values in sub_values and stringify_value
|
||||
public type_name(): string {
|
||||
return "Rect2i";
|
||||
}
|
||||
}
|
||||
|
||||
export class Projection implements GDObject {
|
||||
constructor(public x: Vector4, public y: Vector4, public z: Vector4, public w: Vector4) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${this.x.stringify_value()}, ${this.y.stringify_value()}, ${this.z.stringify_value()}, ${this.w.stringify_value()})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
{ name: "z", value: this.z },
|
||||
{ name: "w", value: this.w },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Projection";
|
||||
}
|
||||
}
|
||||
|
||||
export class Transform3D implements GDObject {
|
||||
constructor(public basis: Basis, public origin: Vector3) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${this.basis.stringify_value()} - ${this.origin.stringify_value()})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "basis", value: this.basis },
|
||||
{ name: "origin", value: this.origin },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Transform";
|
||||
}
|
||||
}
|
||||
|
||||
export class Transform2D implements GDObject {
|
||||
constructor(public origin: Vector2, public x: Vector2, public y: Vector2) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `(${this.origin.stringify_value()} - (${this.x.stringify_value()}, ${this.y.stringify_value()})`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "origin", value: this.origin },
|
||||
{ name: "x", value: this.x },
|
||||
{ name: "y", value: this.y },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Transform2D";
|
||||
}
|
||||
}
|
||||
|
||||
export class StringName implements GDObject {
|
||||
constructor(public value: string) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [
|
||||
{ name: "value", value: this.value },
|
||||
];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "StringName";
|
||||
}
|
||||
}
|
||||
|
||||
export class Callable implements GDObject {
|
||||
public stringify_value(): string {
|
||||
return "()";
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Callable";
|
||||
}
|
||||
}
|
||||
|
||||
export class Signal implements GDObject {
|
||||
constructor(public name: string, public oid: ObjectId) {}
|
||||
|
||||
public stringify_value(): string {
|
||||
return `${this.name}() ${this.oid.stringify_value()}`;
|
||||
}
|
||||
|
||||
public sub_values(): GodotVariable[] {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public type_name(): string {
|
||||
return "Signal";
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
TreeItem,
|
||||
TreeItemCollapsibleState,
|
||||
} from "vscode";
|
||||
import { GodotVariable } from "../debug_runtime";
|
||||
import { RawObject, ObjectId } from "../variables/variants";
|
||||
import { GodotVariable, RawObject, ObjectId } from "./debug_runtime";
|
||||
|
||||
export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
|
||||
private _on_did_change_tree_data: EventEmitter<
|
||||
@@ -63,10 +62,10 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
|
||||
property: RemoteProperty,
|
||||
new_parsed_value: any
|
||||
) {
|
||||
let idx = parents.length - 1;
|
||||
let value = parents[idx].value;
|
||||
const idx = parents.length - 1;
|
||||
const value = parents[idx].value;
|
||||
if (Array.isArray(value)) {
|
||||
let idx = parseInt(property.label);
|
||||
const idx = parseInt(property.label);
|
||||
if (idx < value.length) {
|
||||
value[idx] = new_parsed_value;
|
||||
}
|
||||
@@ -98,7 +97,7 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
|
||||
}
|
||||
|
||||
private parse_variable(va: GodotVariable, object_id?: number) {
|
||||
let value = va.value;
|
||||
const value = va.value;
|
||||
let rendered_value = "";
|
||||
|
||||
if (typeof value === "number") {
|
||||
@@ -132,31 +131,31 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
|
||||
let child_props: RemoteProperty[] = [];
|
||||
|
||||
if (value) {
|
||||
let sub_variables =
|
||||
const sub_variables =
|
||||
typeof value["sub_values"] === "function" &&
|
||||
value instanceof ObjectId === false
|
||||
? value.sub_values()
|
||||
: Array.isArray(value)
|
||||
? value.map((va, i) => {
|
||||
return { name: `${i}`, value: va };
|
||||
})
|
||||
})
|
||||
: value instanceof Map
|
||||
? Array.from(value.keys()).map((va) => {
|
||||
let name =
|
||||
const name =
|
||||
typeof va["rendered_value"] === "function"
|
||||
? va.rendered_value()
|
||||
: `${va}`;
|
||||
let map_value = value.get(va);
|
||||
const map_value = value.get(va);
|
||||
|
||||
return { name: name, value: map_value };
|
||||
})
|
||||
})
|
||||
: [];
|
||||
child_props = sub_variables?.map((va) => {
|
||||
return this.parse_variable(va, object_id);
|
||||
});
|
||||
}
|
||||
|
||||
let out_prop = new RemoteProperty(
|
||||
const out_prop = new RemoteProperty(
|
||||
va.name,
|
||||
value,
|
||||
object_id,
|
||||
@@ -1,263 +0,0 @@
|
||||
import { ServerController } from "./server_controller";
|
||||
import { window, OutputChannel } from "vscode";
|
||||
import { GodotDebugSession } from "./debug_session";
|
||||
import { StoppedEvent, TerminatedEvent } from "vscode-debugadapter";
|
||||
import { GodotDebugData, GodotVariable } from "./debug_runtime";
|
||||
|
||||
export class Mediator {
|
||||
private static controller?: ServerController;
|
||||
private static debug_data?: GodotDebugData;
|
||||
private static inspect_callbacks: Map<
|
||||
number,
|
||||
(class_name: string, variable: GodotVariable) => void
|
||||
> = new Map();
|
||||
private static session?: GodotDebugSession;
|
||||
private static first_output = false;
|
||||
private static output: OutputChannel = window.createOutputChannel("Godot");
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static notify(event: string, parameters: any[] = []) {
|
||||
switch (event) {
|
||||
case "output":
|
||||
if (!this.first_output) {
|
||||
this.first_output = true;
|
||||
this.output.show(true);
|
||||
this.output.clear();
|
||||
this.controller?.send_request_scene_tree_command();
|
||||
}
|
||||
|
||||
let lines: string[] = parameters;
|
||||
lines.forEach((line) => {
|
||||
let message_content: string = line[0];
|
||||
//let message_kind: number = line[1];
|
||||
|
||||
// OutputChannel doesn't give a way to distinguish between a
|
||||
// regular string (message_kind == 0) and an error string (message_kind == 1).
|
||||
|
||||
this.output.appendLine(message_content);
|
||||
});
|
||||
break;
|
||||
|
||||
case "continue":
|
||||
this.controller?.continue();
|
||||
break;
|
||||
|
||||
case "next":
|
||||
this.controller?.next();
|
||||
break;
|
||||
|
||||
case "step":
|
||||
this.controller?.step();
|
||||
break;
|
||||
|
||||
case "step_out":
|
||||
this.controller?.step_out();
|
||||
break;
|
||||
|
||||
case "inspect_object":
|
||||
this.controller?.send_inspect_object_request(parameters[0]);
|
||||
if (parameters[1]) {
|
||||
this.inspect_callbacks.set(parameters[0], parameters[1]);
|
||||
}
|
||||
break;
|
||||
|
||||
case "inspected_object":
|
||||
let inspected_variable = { name: "", value: parameters[1] };
|
||||
this.build_sub_values(inspected_variable);
|
||||
if (this.inspect_callbacks.has(Number(parameters[0]))) {
|
||||
this.inspect_callbacks.get(Number(parameters[0]))(
|
||||
inspected_variable.name,
|
||||
inspected_variable
|
||||
);
|
||||
this.inspect_callbacks.delete(Number(parameters[0]));
|
||||
} else {
|
||||
this.session?.set_inspection(parameters[0], inspected_variable);
|
||||
}
|
||||
break;
|
||||
|
||||
case "stack_dump":
|
||||
this.controller?.trigger_breakpoint(parameters);
|
||||
this.controller?.send_request_scene_tree_command();
|
||||
break;
|
||||
|
||||
case "request_scene_tree":
|
||||
this.controller?.send_request_scene_tree_command();
|
||||
break;
|
||||
|
||||
case "scene_tree":
|
||||
this.debug_data?.scene_tree?.fill_tree(parameters[0]);
|
||||
break;
|
||||
|
||||
case "get_scopes":
|
||||
this.controller?.send_scope_request(parameters[0]);
|
||||
break;
|
||||
|
||||
case "stack_frame_vars":
|
||||
this.do_stack_frame_vars(parameters[0], parameters[1], parameters[2]);
|
||||
break;
|
||||
|
||||
case "remove_breakpoint":
|
||||
this.controller?.remove_breakpoint(parameters[0], parameters[1]);
|
||||
break;
|
||||
|
||||
case "set_breakpoint":
|
||||
this.controller?.set_breakpoint(parameters[0], parameters[1]);
|
||||
break;
|
||||
|
||||
case "stopped_on_breakpoint":
|
||||
this.debug_data.last_frames = parameters[0];
|
||||
this.session?.sendEvent(new StoppedEvent("breakpoint", 0));
|
||||
break;
|
||||
|
||||
case "stopped_on_exception":
|
||||
this.debug_data.last_frames = parameters[0];
|
||||
this.session?.set_exception(true);
|
||||
this.session?.sendEvent(
|
||||
new StoppedEvent("exception", 0, parameters[1])
|
||||
);
|
||||
break;
|
||||
|
||||
case "break":
|
||||
this.controller?.break();
|
||||
break;
|
||||
|
||||
case "changed_value":
|
||||
this.controller?.set_object_property(
|
||||
parameters[0],
|
||||
parameters[1],
|
||||
parameters[2]
|
||||
);
|
||||
break;
|
||||
|
||||
case "debug_enter":
|
||||
let reason: string = parameters[0];
|
||||
if (reason !== "Breakpoint") {
|
||||
this.controller?.set_exception(reason);
|
||||
} else {
|
||||
this.controller?.set_exception("");
|
||||
}
|
||||
this.controller?.stack_dump();
|
||||
break;
|
||||
|
||||
case "start":
|
||||
this.first_output = false;
|
||||
this.controller?.start(
|
||||
parameters[0],
|
||||
parameters[1],
|
||||
parameters[2],
|
||||
parameters[3],
|
||||
parameters[4],
|
||||
parameters[5],
|
||||
parameters[6],
|
||||
this.debug_data
|
||||
);
|
||||
break;
|
||||
|
||||
case "debug_exit":
|
||||
break;
|
||||
|
||||
case "stop":
|
||||
this.controller?.stop();
|
||||
this.session?.sendEvent(new TerminatedEvent());
|
||||
break;
|
||||
|
||||
case "error":
|
||||
this.controller?.set_exception(parameters[0]);
|
||||
this.controller?.stop();
|
||||
this.session?.sendEvent(new TerminatedEvent());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static set_controller(controller: ServerController) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public static set_debug_data(debug_data: GodotDebugData) {
|
||||
this.debug_data = debug_data;
|
||||
}
|
||||
|
||||
public static set_session(debug_session: GodotDebugSession) {
|
||||
this.session = debug_session;
|
||||
}
|
||||
|
||||
private static build_sub_values(va: GodotVariable) {
|
||||
let value = va.value;
|
||||
|
||||
let sub_values: GodotVariable[] = undefined;
|
||||
|
||||
if (value && Array.isArray(value)) {
|
||||
sub_values = value.map((va, i) => {
|
||||
return { name: `${i}`, value: va } as GodotVariable;
|
||||
});
|
||||
} else if (value instanceof Map) {
|
||||
sub_values = Array.from(value.keys()).map((va) => {
|
||||
if (typeof va["stringify_value"] === "function") {
|
||||
return {
|
||||
name: `${va.type_name()}${va.stringify_value()}`,
|
||||
value: value.get(va),
|
||||
} as GodotVariable;
|
||||
} else {
|
||||
return {
|
||||
name: `${va}`,
|
||||
value: value.get(va),
|
||||
} as GodotVariable;
|
||||
}
|
||||
});
|
||||
} else if (value && typeof value["sub_values"] === "function") {
|
||||
sub_values = value.sub_values().map((sva) => {
|
||||
return { name: sva.name, value: sva.value } as GodotVariable;
|
||||
});
|
||||
}
|
||||
|
||||
va.sub_values = sub_values;
|
||||
|
||||
sub_values?.forEach((sva) => this.build_sub_values(sva));
|
||||
}
|
||||
|
||||
private static do_stack_frame_vars(
|
||||
locals: any[],
|
||||
members: any[],
|
||||
globals: any[]
|
||||
) {
|
||||
let locals_out: GodotVariable[] = [];
|
||||
let members_out: GodotVariable[] = [];
|
||||
let globals_out: GodotVariable[] = [];
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < locals.length + members.length + globals.length;
|
||||
i += 2
|
||||
) {
|
||||
const name =
|
||||
i < locals.length
|
||||
? locals[i]
|
||||
: i < members.length + locals.length
|
||||
? members[i - locals.length]
|
||||
: globals[i - locals.length - members.length];
|
||||
|
||||
const value =
|
||||
i < locals.length
|
||||
? locals[i + 1]
|
||||
: i < members.length + locals.length
|
||||
? members[i - locals.length + 1]
|
||||
: globals[i - locals.length - members.length + 1];
|
||||
|
||||
let variable: GodotVariable = {
|
||||
name: name,
|
||||
value: value,
|
||||
};
|
||||
|
||||
this.build_sub_values(variable);
|
||||
|
||||
i < locals.length
|
||||
? locals_out.push(variable)
|
||||
: i < members.length + locals.length
|
||||
? members_out.push(variable)
|
||||
: globals_out.push(variable);
|
||||
}
|
||||
|
||||
this.session?.set_scopes(locals_out, members_out, globals_out);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
TreeItemCollapsibleState,
|
||||
} from "vscode";
|
||||
import path = require("path");
|
||||
import fs = require("fs");
|
||||
|
||||
export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
|
||||
private _on_did_change_tree_data: EventEmitter<
|
||||
@@ -18,7 +17,7 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
|
||||
public readonly onDidChangeTreeData: Event<SceneNode> | undefined = this
|
||||
._on_did_change_tree_data.event;
|
||||
|
||||
constructor() {}
|
||||
constructor() { }
|
||||
|
||||
public fill_tree(tree: SceneNode) {
|
||||
this.tree = tree;
|
||||
@@ -38,9 +37,8 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
|
||||
}
|
||||
|
||||
public getTreeItem(element: SceneNode): TreeItem | Thenable<TreeItem> {
|
||||
let has_children = element.children.length > 0;
|
||||
let tree_item: TreeItem | undefined;
|
||||
tree_item = new TreeItem(
|
||||
const has_children = element.children.length > 0;
|
||||
const tree_item: TreeItem = new TreeItem(
|
||||
element.label,
|
||||
has_children
|
||||
? element === this.tree
|
||||
@@ -51,77 +49,38 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
|
||||
|
||||
tree_item.description = element.class_name;
|
||||
tree_item.iconPath = element.iconPath;
|
||||
if (element.scene_file_path) {
|
||||
let tooltip = "";
|
||||
tooltip += `${element.label}`;
|
||||
tooltip += `\n${element.class_name}`;
|
||||
tooltip += `\n${element.object_id}`;
|
||||
if (element.scene_file_path) {
|
||||
tooltip += `\n${element.scene_file_path}`;
|
||||
}
|
||||
tree_item.tooltip = tooltip;
|
||||
}
|
||||
|
||||
return tree_item;
|
||||
}
|
||||
}
|
||||
|
||||
function match_icon_to_class(class_name: string) {
|
||||
let icon_name = `icon${class_name
|
||||
.replace(/(2|3)D/, "$1d")
|
||||
.replace(/([A-Z0-9])/g, "_$1")
|
||||
.toLowerCase()}.svg`;
|
||||
return icon_name;
|
||||
}
|
||||
|
||||
export class SceneNode extends TreeItem {
|
||||
constructor(
|
||||
public label: string,
|
||||
public class_name: string,
|
||||
public object_id: number,
|
||||
public children: SceneNode[],
|
||||
public collapsibleState?: TreeItemCollapsibleState
|
||||
public scene_file_path?: string,
|
||||
public view_flags?: number,
|
||||
) {
|
||||
super(label, collapsibleState);
|
||||
super(label);
|
||||
|
||||
let light = path.join(
|
||||
__filename,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"light",
|
||||
match_icon_to_class(class_name)
|
||||
);
|
||||
if (!fs.existsSync(light)) {
|
||||
path.join(
|
||||
__filename,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"light",
|
||||
"node.svg"
|
||||
);
|
||||
}
|
||||
let dark = path.join(
|
||||
__filename,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"dark",
|
||||
match_icon_to_class(class_name)
|
||||
);
|
||||
if (!fs.existsSync(light)) {
|
||||
path.join(
|
||||
__filename,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"resources",
|
||||
"dark",
|
||||
"node.svg"
|
||||
);
|
||||
}
|
||||
const iconDir = path.join(__filename, "..", "..", "..", "resources", "godot_icons");
|
||||
const iconName = class_name + ".svg";
|
||||
|
||||
this.iconPath = {
|
||||
light: light,
|
||||
dark: dark,
|
||||
light: path.join(iconDir, "light", iconName),
|
||||
dark: path.join(iconDir, "dark", iconName),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
import { CommandParser } from "./commands/command_parser";
|
||||
import { Mediator } from "./mediator";
|
||||
import { VariantDecoder } from "./variables/variant_decoder";
|
||||
import {
|
||||
GodotBreakpoint,
|
||||
GodotStackFrame,
|
||||
GodotDebugData,
|
||||
} from "./debug_runtime";
|
||||
import { window } from "vscode";
|
||||
const TERMINATE = require("terminate");
|
||||
import net = require("net");
|
||||
import utils = require("../utils");
|
||||
import cp = require("child_process");
|
||||
import path = require("path");
|
||||
|
||||
export class ServerController {
|
||||
private command_buffer: Buffer[] = [];
|
||||
private commands = new CommandParser();
|
||||
private debug_data: GodotDebugData;
|
||||
private decoder = new VariantDecoder();
|
||||
private draining = false;
|
||||
private exception = "";
|
||||
private godot_pid: number;
|
||||
private server?: net.Server;
|
||||
private socket?: net.Socket;
|
||||
private stepping_out = false;
|
||||
private terminated = false;
|
||||
|
||||
public break() {
|
||||
this.add_and_send(this.commands.make_break_command());
|
||||
}
|
||||
|
||||
public continue() {
|
||||
this.add_and_send(this.commands.make_continue_command());
|
||||
}
|
||||
|
||||
public next() {
|
||||
this.add_and_send(this.commands.make_next_command());
|
||||
}
|
||||
|
||||
public remove_breakpoint(path_to: string, line: number) {
|
||||
this.debug_data.remove_breakpoint(path_to, line);
|
||||
this.add_and_send(
|
||||
this.commands.make_remove_breakpoint_command(path_to, line)
|
||||
);
|
||||
}
|
||||
|
||||
public send_inspect_object_request(object_id: bigint) {
|
||||
this.add_and_send(this.commands.make_inspect_object_command(object_id));
|
||||
}
|
||||
|
||||
public send_request_scene_tree_command() {
|
||||
this.add_and_send(this.commands.make_request_scene_tree_command());
|
||||
}
|
||||
|
||||
public send_scope_request(frame_id: number) {
|
||||
this.add_and_send(this.commands.make_stack_frame_vars_command(frame_id));
|
||||
}
|
||||
|
||||
public set_breakpoint(path_to: string, line: number) {
|
||||
this.add_and_send(
|
||||
this.commands.make_send_breakpoint_command(path_to, line)
|
||||
);
|
||||
}
|
||||
|
||||
public set_exception(exception: string) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public set_object_property(
|
||||
object_id: bigint,
|
||||
label: string,
|
||||
new_parsed_value: any
|
||||
) {
|
||||
this.add_and_send(
|
||||
this.commands.make_set_object_value_command(
|
||||
BigInt(object_id),
|
||||
label,
|
||||
new_parsed_value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public stack_dump() {
|
||||
this.add_and_send(this.commands.make_stack_dump_command());
|
||||
}
|
||||
|
||||
public start(
|
||||
project_path: string,
|
||||
address: string,
|
||||
port: number,
|
||||
launch_instance: boolean,
|
||||
launch_scene: boolean,
|
||||
scene_file: string | undefined,
|
||||
additional_options: string | undefined,
|
||||
debug_data: GodotDebugData
|
||||
) {
|
||||
this.debug_data = debug_data;
|
||||
|
||||
if (launch_instance) {
|
||||
let godot_path: string = utils.get_configuration("editorPath", "godot");
|
||||
const force_visible_collision_shapes = utils.get_configuration("forceVisibleCollisionShapes", false);
|
||||
const force_visible_nav_mesh = utils.get_configuration("forceVisibleNavMesh", false);
|
||||
|
||||
let executable_line = `"${godot_path}" --path "${project_path}" --remote-debug ${address}:${port}`;
|
||||
|
||||
if (force_visible_collision_shapes) {
|
||||
executable_line += " --debug-collisions";
|
||||
}
|
||||
if (force_visible_nav_mesh) {
|
||||
executable_line += " --debug-navigation";
|
||||
}
|
||||
if (launch_scene) {
|
||||
let filename = "";
|
||||
if (scene_file) {
|
||||
filename = scene_file;
|
||||
} else {
|
||||
filename = window.activeTextEditor.document.fileName;
|
||||
}
|
||||
executable_line += ` "${filename}"`;
|
||||
}
|
||||
if(additional_options){
|
||||
executable_line += " " + additional_options;
|
||||
}
|
||||
executable_line += this.breakpoint_string(
|
||||
debug_data.get_all_breakpoints(),
|
||||
project_path
|
||||
);
|
||||
let godot_exec = cp.exec(executable_line, (error) => {
|
||||
if (!this.terminated) {
|
||||
window.showErrorMessage(`Failed to launch Godot instance: ${error}`);
|
||||
}
|
||||
});
|
||||
this.godot_pid = godot_exec.pid;
|
||||
}
|
||||
|
||||
this.server = net.createServer((socket) => {
|
||||
this.socket = socket;
|
||||
|
||||
if (!launch_instance) {
|
||||
let breakpoints = this.debug_data.get_all_breakpoints();
|
||||
breakpoints.forEach((bp) => {
|
||||
this.set_breakpoint(
|
||||
this.breakpoint_path(project_path, bp.file),
|
||||
bp.line
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
socket.on("data", (buffer) => {
|
||||
let buffers = this.split_buffers(buffer);
|
||||
while (buffers.length > 0) {
|
||||
let sub_buffer = buffers.shift();
|
||||
let data = this.decoder.get_dataset(sub_buffer, 0).slice(1);
|
||||
this.commands.parse_message(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("close", (had_error) => {
|
||||
Mediator.notify("stop");
|
||||
});
|
||||
|
||||
socket.on("end", () => {
|
||||
Mediator.notify("stop");
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
Mediator.notify("error", [error]);
|
||||
});
|
||||
|
||||
socket.on("drain", () => {
|
||||
socket.resume();
|
||||
this.draining = false;
|
||||
this.send_buffer();
|
||||
});
|
||||
});
|
||||
|
||||
this.server.listen(port, address);
|
||||
}
|
||||
|
||||
public step() {
|
||||
this.add_and_send(this.commands.make_step_command());
|
||||
}
|
||||
|
||||
public step_out() {
|
||||
this.stepping_out = true;
|
||||
this.add_and_send(this.commands.make_next_command());
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.socket?.destroy();
|
||||
this.server?.close((error) => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
this.server.unref();
|
||||
this.server = undefined;
|
||||
});
|
||||
|
||||
if (this.godot_pid) {
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
private terminate() {
|
||||
this.terminated = true;
|
||||
TERMINATE(this.godot_pid);
|
||||
this.godot_pid = undefined;
|
||||
}
|
||||
|
||||
public trigger_breakpoint(stack_frames: GodotStackFrame[]) {
|
||||
let continue_stepping = false;
|
||||
let stack_count = stack_frames.length;
|
||||
|
||||
let file = stack_frames[0].file.replace(
|
||||
"res://",
|
||||
`${this.debug_data.project_path}/`
|
||||
);
|
||||
let line = stack_frames[0].line;
|
||||
|
||||
if (this.stepping_out) {
|
||||
let breakpoint = this.debug_data
|
||||
.get_breakpoints(file)
|
||||
.find((bp) => bp.line === line);
|
||||
if (!breakpoint) {
|
||||
if (this.debug_data.stack_count > 1) {
|
||||
continue_stepping = this.debug_data.stack_count === stack_count;
|
||||
} else {
|
||||
let file_same =
|
||||
stack_frames[0].file === this.debug_data.last_frame.file;
|
||||
let func_same =
|
||||
stack_frames[0].function === this.debug_data.last_frame.function;
|
||||
let line_greater =
|
||||
stack_frames[0].line >= this.debug_data.last_frame.line;
|
||||
|
||||
continue_stepping = file_same && func_same && line_greater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.debug_data.stack_count = stack_count;
|
||||
this.debug_data.last_frame = stack_frames[0];
|
||||
|
||||
if (continue_stepping) {
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
|
||||
this.stepping_out = false;
|
||||
|
||||
this.debug_data.stack_files = stack_frames.map((sf) => {
|
||||
return sf.file;
|
||||
});
|
||||
|
||||
if (this.exception.length === 0) {
|
||||
Mediator.notify("stopped_on_breakpoint", [stack_frames]);
|
||||
} else {
|
||||
Mediator.notify("stopped_on_exception", [stack_frames, this.exception]);
|
||||
}
|
||||
}
|
||||
|
||||
private add_and_send(buffer: Buffer) {
|
||||
this.command_buffer.push(buffer);
|
||||
this.send_buffer();
|
||||
}
|
||||
|
||||
private breakpoint_path(project_path: string, file: string) {
|
||||
let relative_path = path.relative(project_path, file).replace(/\\/g, "/");
|
||||
if (relative_path.length !== 0) {
|
||||
return `res://${relative_path}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private breakpoint_string(
|
||||
breakpoints: GodotBreakpoint[],
|
||||
project_path: string
|
||||
) {
|
||||
let output = "";
|
||||
if (breakpoints.length > 0) {
|
||||
output += " --breakpoints ";
|
||||
breakpoints.forEach((bp, i) => {
|
||||
output += `${this.breakpoint_path(project_path, bp.file)}:${bp.line}${i < breakpoints.length - 1 ? "," : ""
|
||||
}`;
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private send_buffer() {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!this.draining && this.command_buffer.length > 0) {
|
||||
this.draining = !this.socket.write(this.command_buffer.shift());
|
||||
}
|
||||
}
|
||||
|
||||
private split_buffers(buffer: Buffer) {
|
||||
let len = buffer.byteLength;
|
||||
let offset = 0;
|
||||
let buffers: Buffer[] = [];
|
||||
while (len > 0) {
|
||||
let sub_len = buffer.readUInt32LE(offset) + 4;
|
||||
buffers.push(buffer.slice(offset, offset + sub_len));
|
||||
offset += sub_len;
|
||||
len -= sub_len;
|
||||
}
|
||||
|
||||
return buffers;
|
||||
}
|
||||
}
|
||||
164
src/extension.ts
164
src/extension.ts
@@ -1,24 +1,27 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
import { attemptSettingsUpdate } from "./settings_updater";
|
||||
import { register_debugger } from "./debugger/debugger_context";
|
||||
import { GDDocumentLinkProvider } from "./document_link_provider";
|
||||
import { ClientConnectionManager } from "./lsp/ClientConnectionManager";
|
||||
import { ScenePreviewProvider } from "./scene_preview_provider";
|
||||
import { GodotDebugger } from "./debugger/debugger";
|
||||
import { exec, execSync } from "child_process";
|
||||
import {
|
||||
get_configuration,
|
||||
set_configuration,
|
||||
find_file,
|
||||
find_project_file,
|
||||
register_command
|
||||
register_command,
|
||||
get_project_version,
|
||||
set_context,
|
||||
projectDir,
|
||||
projectVersion,
|
||||
} from "./utils";
|
||||
|
||||
const TOOL_NAME = "GodotTools";
|
||||
import { prompt_for_godot_executable } from "./utils/prompts";
|
||||
|
||||
let lspClientManager: ClientConnectionManager = null;
|
||||
let linkProvider: GDDocumentLinkProvider = null;
|
||||
let scenePreviewManager: ScenePreviewProvider = null;
|
||||
let godotDebugger: GodotDebugger = null;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
attemptSettingsUpdate(context);
|
||||
@@ -26,24 +29,19 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
lspClientManager = new ClientConnectionManager(context);
|
||||
linkProvider = new GDDocumentLinkProvider(context);
|
||||
scenePreviewManager = new ScenePreviewProvider();
|
||||
|
||||
register_debugger(context);
|
||||
godotDebugger = new GodotDebugger(context);
|
||||
|
||||
context.subscriptions.push(
|
||||
register_command("openEditor", () => {
|
||||
open_workspace_with_editor("-e").catch(err => vscode.window.showErrorMessage(err));
|
||||
}),
|
||||
register_command("runProject", () => {
|
||||
open_workspace_with_editor().catch(err => vscode.window.showErrorMessage(err));
|
||||
}),
|
||||
register_command("runProjectDebug", () => {
|
||||
open_workspace_with_editor("--debug-collisions --debug-navigation").catch(err => vscode.window.showErrorMessage(err));
|
||||
}),
|
||||
register_command("copyResourcePathContext", copy_resource_path),
|
||||
register_command("openEditor", open_workspace_with_editor),
|
||||
register_command("copyResourcePath", copy_resource_path),
|
||||
register_command("openTypeDocumentation", open_type_documentation),
|
||||
register_command("switchSceneScript", switch_scene_script),
|
||||
)
|
||||
);
|
||||
|
||||
set_context("godotFiles", ["gdscript", "gdscene", "gdresource", "gdshader",]);
|
||||
set_context("sceneLikeFiles", ["gdscript", "gdscene"]);
|
||||
|
||||
get_project_version();
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> {
|
||||
@@ -89,113 +87,29 @@ async function switch_scene_script() {
|
||||
}
|
||||
}
|
||||
|
||||
function open_workspace_with_editor(params = "") {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
let valid = false;
|
||||
let project_dir = '';
|
||||
let project_file = '';
|
||||
function open_workspace_with_editor() {
|
||||
const settingName = `editorPath.godot${projectVersion[0]}`;
|
||||
const godotPath = get_configuration(settingName);
|
||||
|
||||
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());
|
||||
}
|
||||
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 Godot editor: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
return;
|
||||
}
|
||||
if (valid) {
|
||||
run_editor(`--path "${project_dir}" ${params}`).then(() => resolve()).catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject("Current workspace is not a Godot project");
|
||||
if (match[1] !== settingName.slice(-1)) {
|
||||
const message = `Cannot launch Godot editor: The current project uses Godot v${projectVersion}, but the specified Godot executable is version ${match[0]}`;
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function 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) => {
|
||||
const POWERSHELL = "powershell.exe";
|
||||
const POWERSHELL_CORE = "pwsh.exe";
|
||||
return path && (path.endsWith(POWERSHELL) || path.endsWith(POWERSHELL_CORE));
|
||||
};
|
||||
const escape_command = (cmd: string) => {
|
||||
const cmdEsc = `"${cmd}"`;
|
||||
if (process.platform === "win32") {
|
||||
const shell_plugin = vscode.workspace.getConfiguration("terminal.integrated.shell");
|
||||
|
||||
if (shell_plugin) {
|
||||
const shell = shell_plugin.get<string>("windows");
|
||||
if (shell) {
|
||||
if (is_powershell_path(shell)) {
|
||||
return `&${cmdEsc}`;
|
||||
} else {
|
||||
return cmdEsc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const POWERSHELL_SOURCE = "PowerShell";
|
||||
const default_profile = vscode.workspace.getConfiguration("terminal.integrated.defaultProfile");
|
||||
if (default_profile) {
|
||||
const profile_name = default_profile.get<string>("windows");
|
||||
if (profile_name) {
|
||||
if (POWERSHELL_SOURCE === profile_name) {
|
||||
return `&${cmdEsc}`;
|
||||
}
|
||||
const profiles = vscode.workspace.getConfiguration("terminal.integrated.profiles.windows");
|
||||
const profile = profiles.get<{ source?: string, path?: string }>(profile_name);
|
||||
if (profile) {
|
||||
if (POWERSHELL_SOURCE === profile.source || is_powershell_path(profile.path)) {
|
||||
return `&${cmdEsc}`;
|
||||
} else {
|
||||
return cmdEsc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// default for Windows if nothing is set is PowerShell
|
||||
return `&${cmdEsc}`;
|
||||
|
||||
}
|
||||
return cmdEsc;
|
||||
};
|
||||
let existingTerminal = vscode.window.terminals.find(t => t.name === TOOL_NAME);
|
||||
if (existingTerminal) {
|
||||
existingTerminal.dispose();
|
||||
}
|
||||
let terminal = vscode.window.createTerminal(TOOL_NAME);
|
||||
let editorPath = escape_command(path);
|
||||
let cmmand = `${editorPath} ${params}`;
|
||||
terminal.sendText(cmmand, true);
|
||||
terminal.show();
|
||||
resolve();
|
||||
};
|
||||
|
||||
// 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",
|
||||
filters: process.platform === "win32" ? { "Godot Editor Binary": ["exe", "EXE"] } : undefined
|
||||
}).then((uris: vscode.Uri[]) => {
|
||||
if (!uris) {
|
||||
return;
|
||||
}
|
||||
let path = uris[0].fsPath;
|
||||
if (!fs.existsSync(path) || !fs.statSync(path).isFile()) {
|
||||
reject("Invalid editor path to run the project");
|
||||
} else {
|
||||
run_godot(path, params);
|
||||
set_configuration("editorPath", path);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
run_godot(editorPath, params);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
const message = `Cannot launch Godot editor: ${settingName} of ${godotPath} is not a valid Godot executable`;
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
return;
|
||||
}
|
||||
|
||||
exec(`${godotPath} --path "${projectDir}" -e`);
|
||||
}
|
||||
|
||||
128
src/logger.ts
128
src/logger.ts
@@ -1,48 +1,5 @@
|
||||
|
||||
export class Logger {
|
||||
protected buffer: 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 = "";
|
||||
if (this.tag) {
|
||||
line += `[${this.tag}]`;
|
||||
}
|
||||
if (this.time) {
|
||||
line += `[${new Date().toISOString()}]`;
|
||||
}
|
||||
if (line) {
|
||||
line += " ";
|
||||
}
|
||||
|
||||
for (let index = 0; index < messages.length; index++) {
|
||||
line += messages[index];
|
||||
if (index < messages.length) {
|
||||
line += " ";
|
||||
} else {
|
||||
line += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
this.buffer += line;
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
get_buffer(): string {
|
||||
return this.buffer;
|
||||
}
|
||||
}
|
||||
import { LogOutputChannel, window } from "vscode";
|
||||
import { is_debug_mode } from "./utils";
|
||||
|
||||
export enum LOG_LEVEL {
|
||||
SILENT,
|
||||
@@ -58,9 +15,9 @@ const LOG_LEVEL_NAMES = [
|
||||
"WARN ",
|
||||
"INFO ",
|
||||
"DEBUG",
|
||||
]
|
||||
];
|
||||
|
||||
const RESET = "\u001b[0m"
|
||||
const RESET = "\u001b[0m";
|
||||
|
||||
const LOG_COLORS = [
|
||||
RESET, // SILENT, normal
|
||||
@@ -68,39 +25,67 @@ const LOG_COLORS = [
|
||||
"\u001b[1;33m", // WARNING, yellow
|
||||
"\u001b[1;36m", // INFO, cyan
|
||||
"\u001b[1;32m", // DEBUG, green
|
||||
]
|
||||
];
|
||||
|
||||
export class Logger2 {
|
||||
export interface LoggerOptions {
|
||||
level?: LOG_LEVEL
|
||||
time?: boolean;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private level: LOG_LEVEL = LOG_LEVEL.DEBUG;
|
||||
private show_tag: boolean = true;
|
||||
private show_time: boolean;
|
||||
private show_label: boolean;
|
||||
private show_level: boolean = false;
|
||||
private output?: LogOutputChannel;
|
||||
|
||||
constructor(
|
||||
private tag: string,
|
||||
private level: LOG_LEVEL = LOG_LEVEL.DEBUG,
|
||||
{ time = false, label = false }: { time?: boolean, label?: boolean } = {},
|
||||
{ level = LOG_LEVEL.DEBUG, time = false, output = "" }: LoggerOptions = {},
|
||||
) {
|
||||
this.level = level;
|
||||
this.show_time = time;
|
||||
this.show_label = label;
|
||||
if (output) {
|
||||
this.output = window.createOutputChannel(output, { log: true });
|
||||
}
|
||||
}
|
||||
|
||||
private log(level: LOG_LEVEL, ...messages) {
|
||||
let prefix = "";
|
||||
if (this.show_label) {
|
||||
prefix += "[godotTools]";
|
||||
}
|
||||
if (this.show_time) {
|
||||
prefix += `[${new Date().toISOString()}]`;
|
||||
}
|
||||
if (this.show_level) {
|
||||
prefix += "[" + LOG_COLORS[level] + LOG_LEVEL_NAMES[level] + RESET + "]";
|
||||
}
|
||||
if (this.show_tag) {
|
||||
prefix += "[" + LOG_COLORS[level] + this.tag + RESET + "]";
|
||||
if (is_debug_mode()) {
|
||||
let prefix = "";
|
||||
if (this.show_time) {
|
||||
prefix += `[${new Date().toISOString()}]`;
|
||||
}
|
||||
if (this.show_level) {
|
||||
prefix += "[" + LOG_COLORS[level] + LOG_LEVEL_NAMES[level] + RESET + "]";
|
||||
}
|
||||
if (this.show_tag) {
|
||||
prefix += "[" + LOG_COLORS[level] + this.tag + RESET + "]";
|
||||
}
|
||||
|
||||
console.log(prefix, ...messages);
|
||||
}
|
||||
|
||||
console.log(prefix, ...messages);
|
||||
if (this.output) {
|
||||
const line = `${messages[0]}`;
|
||||
switch (level) {
|
||||
case LOG_LEVEL.ERROR:
|
||||
this.output.error(line);
|
||||
break;
|
||||
case LOG_LEVEL.WARNING:
|
||||
this.output.warn(line);
|
||||
break;
|
||||
case LOG_LEVEL.INFO:
|
||||
this.output.info(line);
|
||||
break;
|
||||
case LOG_LEVEL.DEBUG:
|
||||
this.output.debug(line);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info(...messages) {
|
||||
@@ -125,9 +110,10 @@ export class Logger2 {
|
||||
}
|
||||
}
|
||||
|
||||
export function createLogger(tag, level: LOG_LEVEL = LOG_LEVEL.DEBUG) {
|
||||
return new Logger2(tag, level);
|
||||
}
|
||||
const loggers: Map<string, Logger> = new Map();
|
||||
|
||||
const logger = new Logger("godot-tools", true);
|
||||
export default logger;
|
||||
export function createLogger(tag, options?: LoggerOptions) {
|
||||
const logger = new Logger(tag, options);
|
||||
loggers.set(tag, logger);
|
||||
return logger;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
register_command,
|
||||
set_configuration,
|
||||
} from "../utils";
|
||||
import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
|
||||
import { createLogger } from "../logger";
|
||||
import { execSync } from "child_process";
|
||||
import { subProcess, killSubProcesses } from '../utils/subspawn';
|
||||
import { subProcess, killSubProcesses } from "../utils/subspawn";
|
||||
|
||||
const log = createLogger("lsp.manager");
|
||||
|
||||
@@ -83,7 +84,7 @@ export class ClientConnectionManager {
|
||||
}
|
||||
|
||||
private stop_language_server() {
|
||||
killSubProcesses('LSP');
|
||||
killSubProcesses("LSP");
|
||||
}
|
||||
|
||||
private async start_language_server() {
|
||||
@@ -98,11 +99,11 @@ export class ClientConnectionManager {
|
||||
|
||||
const projectVersion = await get_project_version();
|
||||
|
||||
let minimumVersion = '6';
|
||||
let targetVersion = '3.6';
|
||||
if (projectVersion.startsWith('4')) {
|
||||
minimumVersion = '2';
|
||||
targetVersion = '4.2';
|
||||
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);
|
||||
@@ -113,21 +114,13 @@ export class ClientConnectionManager {
|
||||
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);
|
||||
}
|
||||
});
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
return;
|
||||
}
|
||||
this.connectedVersion = output;
|
||||
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);
|
||||
}
|
||||
});
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,21 +128,17 @@ export class ClientConnectionManager {
|
||||
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);
|
||||
select_godot_executable(settingName);
|
||||
} else if (item == "Disable Headless LSP") {
|
||||
set_configuration("lsp.headless", false);
|
||||
this.prompt_for_reload();
|
||||
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);
|
||||
}
|
||||
});
|
||||
prompt_for_godot_executable(message, settingName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,10 +148,10 @@ export class ClientConnectionManager {
|
||||
|
||||
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 lspProcess = subProcess("LSP", command, { shell: true, detached: true });
|
||||
|
||||
const lspStdout = createLogger("lsp.stdout");
|
||||
lspProcess.stdout.on('data', (data) => {
|
||||
lspProcess.stdout.on("data", (data) => {
|
||||
const out = data.toString().trim();
|
||||
if (out) {
|
||||
lspStdout.debug(out);
|
||||
@@ -170,41 +159,18 @@ export class ClientConnectionManager {
|
||||
});
|
||||
|
||||
// const lspStderr = createLogger("lsp.stderr");
|
||||
lspProcess.stderr.on('data', (data) => {
|
||||
lspProcess.stderr.on("data", (data) => {
|
||||
// const out = data.toString().trim();
|
||||
// if (out) {
|
||||
// lspStderr.debug(out);
|
||||
// }
|
||||
});
|
||||
|
||||
lspProcess.on('close', (code) => {
|
||||
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");
|
||||
@@ -250,13 +216,13 @@ export class ClientConnectionManager {
|
||||
|
||||
private update_status_widget() {
|
||||
const lspTarget = this.get_lsp_connection_string();
|
||||
const maxAttempts = get_configuration("lsp.autoReconnect.attempts")
|
||||
const maxAttempts = get_configuration("lsp.autoReconnect.attempts");
|
||||
let text = "";
|
||||
let tooltip = "";
|
||||
switch (this.status) {
|
||||
case ManagerStatus.INITIALIZING:
|
||||
text = `$(sync~spin) Initializing`;
|
||||
tooltip = `Initializing extension...`;
|
||||
text = "$(sync~spin) Initializing";
|
||||
tooltip = "Initializing extension...";
|
||||
break;
|
||||
case ManagerStatus.INITIALIZING_LSP:
|
||||
text = `$(sync~spin) Initializing LSP ${this.reconnectionAttempts}/${maxAttempts}`;
|
||||
@@ -266,19 +232,19 @@ export class ClientConnectionManager {
|
||||
}
|
||||
break;
|
||||
case ManagerStatus.PENDING:
|
||||
text = `$(sync~spin) Connecting`;
|
||||
text = "$(sync~spin) Connecting";
|
||||
tooltip = `Connecting to the GDScript language server at ${lspTarget}`;
|
||||
break;
|
||||
case ManagerStatus.CONNECTED:
|
||||
text = `$(check) Connected`;
|
||||
text = "$(check) Connected";
|
||||
tooltip = `Connected to the GDScript language server.\n${lspTarget}`;
|
||||
if (this.connectedVersion) {
|
||||
tooltip += `\n${this.connectedVersion}`;
|
||||
}
|
||||
break;
|
||||
case ManagerStatus.DISCONNECTED:
|
||||
text = `$(x) Disconnected`;
|
||||
tooltip = `Disconnected from the GDScript language server.`;
|
||||
text = "$(x) Disconnected";
|
||||
tooltip = "Disconnected from the GDScript language server.";
|
||||
break;
|
||||
case ManagerStatus.RETRYING:
|
||||
text = `$(sync~spin) Connecting ${this.reconnectionAttempts}/${maxAttempts}`;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EventEmitter } from "events";
|
||||
import * as vscode from 'vscode';
|
||||
import { LanguageClient, RequestMessage, ResponseMessage, integer } from "vscode-languageclient/node";
|
||||
import { createLogger } from "../logger";
|
||||
import { createLogger, LOG_LEVEL } 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");
|
||||
const log = createLogger("lsp.client", {level: LOG_LEVEL.SILENT});
|
||||
|
||||
export enum ClientStatus {
|
||||
PENDING,
|
||||
@@ -19,7 +19,7 @@ export enum TargetLSP {
|
||||
EDITOR,
|
||||
}
|
||||
|
||||
const CUSTOM_MESSAGE = "gdscrip_client/";
|
||||
const CUSTOM_MESSAGE = "gdscript_client/";
|
||||
|
||||
export default class GDScriptLanguageClient extends LanguageClient {
|
||||
public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") == "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
|
||||
@@ -153,8 +153,8 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
// 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');
|
||||
const value: string = message.result["contents"]?.value;
|
||||
message.result["contents"].value = value?.replace(/\n[#]+/g, '\n');
|
||||
}
|
||||
|
||||
this.message_handler.on_message(message);
|
||||
@@ -164,8 +164,11 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
this.lastSymbolHovered = "";
|
||||
set_context("typeFound", false);
|
||||
|
||||
let decl: string = message.result["contents"].value;
|
||||
decl = decl.split('\n')[0].trim();
|
||||
let decl: string = message?.result["contents"]?.value;
|
||||
if (!decl) {
|
||||
return;
|
||||
}
|
||||
decl = decl.split("\n")[0].trim();
|
||||
|
||||
// strip off the value
|
||||
if (decl.includes("=")) {
|
||||
|
||||
@@ -86,6 +86,7 @@ export class TCPMessageIO extends MessageIO {
|
||||
socket.on('data', this.on_message.bind(this));
|
||||
socket.on('end', this.on_disconnected.bind(this));
|
||||
socket.on('close', this.on_disconnected.bind(this));
|
||||
socket.on('error', this.on_error.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,6 +99,10 @@ export class TCPMessageIO extends MessageIO {
|
||||
this.socket = null;
|
||||
this.emit('disconnected');
|
||||
}
|
||||
|
||||
protected on_error(error) {
|
||||
// TODO: handle errors?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +116,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
|
||||
private partialMessageTimer: NodeJS.Timeout | undefined;
|
||||
private _partialMessageTimeout: number;
|
||||
|
||||
public constructor(io: MessageIO, encoding: string = 'utf8') {
|
||||
public constructor(io: MessageIO, encoding: BufferEncoding = 'utf8') {
|
||||
super();
|
||||
this.io = io;
|
||||
this.io.reader = this;
|
||||
@@ -207,7 +212,7 @@ export class MessageIOWriter extends AbstractMessageWriter implements MessageWri
|
||||
private encoding: BufferEncoding;
|
||||
private errorCount: number;
|
||||
|
||||
public constructor(io: MessageIO, encoding: string = 'utf8') {
|
||||
public constructor(io: MessageIO, encoding: BufferEncoding = 'utf8') {
|
||||
super();
|
||||
this.io = io;
|
||||
this.io.writer = this;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { EventEmitter } from "events";
|
||||
import { MessageIO } from "./MessageIO";
|
||||
import { NotificationMessage } from "vscode-jsonrpc";
|
||||
import * as Prism from "prismjs";
|
||||
import * as marked from "marked";
|
||||
import { marked } from "marked";
|
||||
import { get_configuration, register_command } from "../utils";
|
||||
import {
|
||||
Methods,
|
||||
@@ -127,7 +127,7 @@ export class NativeDocumentManager extends EventEmitter {
|
||||
* configuration and previously opened native symbols.
|
||||
*/
|
||||
private get_new_native_symbol_column(): vscode.ViewColumn {
|
||||
const config_placement = get_configuration("nativeSymbolPlacement");
|
||||
const config_placement = get_configuration("documentation.newTabPlacement");
|
||||
|
||||
if (config_placement == "active") {
|
||||
return vscode.ViewColumn.Active;
|
||||
|
||||
@@ -109,12 +109,12 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode> {
|
||||
|
||||
private pin_preview() {
|
||||
this.scenePreviewPinned = true;
|
||||
set_context("godotTools.context.scenePreviewPinned", true);
|
||||
set_context("scenePreview.pinned", true);
|
||||
}
|
||||
|
||||
private unpin_preview() {
|
||||
this.scenePreviewPinned = false;
|
||||
set_context("godotTools.context.scenePreviewPinned", false);
|
||||
set_context("scenePreview.pinned", false);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import * as vscode from "vscode";
|
||||
|
||||
const OLD_SETTINGS_CONVERSIONS = [
|
||||
["godot_tools.editor_path", "godotTools.editorPath.godot3"],
|
||||
["godot_tools.gdscript_lsp_server_protocol", "godotTools.lsp.serverProtocol"],
|
||||
["godot_tools.gdscript_lsp_server_host", "godotTools.lsp.serverHost"],
|
||||
["godot_tools.gdscript_lsp_server_port", "godotTools.lsp.serverPort"],
|
||||
["godot_tools.editor_path", "godotTools.editorPath"],
|
||||
["godot_tools.scene_file_config", "godotTools.sceneFileConfig"],
|
||||
["godot_tools.reconnect_automatically", "godotTools.lsp.autoReconnect.enabled"],
|
||||
["godot_tools.reconnect_cooldown", "godotTools.lsp.autoReconnect.cooldown"],
|
||||
["godot_tools.reconnect_attempts", "godotTools.lsp.autoReconnect.attempts"],
|
||||
["godot_tools.force_visible_collision_shapes", "godotTools.forceVisibleCollisionShapes"],
|
||||
["godot_tools.force_visible_nav_mesh", "godotTools.forceVisibleNavMesh"],
|
||||
["godot_tools.native_symbol_placement", "godotTools.nativeSymbolPlacement"],
|
||||
["godot_tools.force_visible_collision_shapes", "godotTools.debugger.forceVisibleCollisionShapes"],
|
||||
["godot_tools.force_visible_nav_mesh", "godotTools.debugger.forceVisibleNavMesh"],
|
||||
["godot_tools.native_symbol_placement", "godotTools.documentation.newTabPlacement"],
|
||||
["godot_tools.scenePreview.previewRelatedScenes", "godotTools.scenePreview.previewRelatedScenes"]
|
||||
];
|
||||
|
||||
@@ -23,7 +22,6 @@ export function updateOldStyleSettings() {
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
configuration.update(old_style_key, undefined, true);
|
||||
configuration.update(new_style_key, value, true);
|
||||
settings_changed = true;
|
||||
}
|
||||
|
||||
56
src/utils.ts
56
src/utils.ts
@@ -5,12 +5,12 @@ import { AddressInfo, createServer } from "net";
|
||||
|
||||
const EXTENSION_PREFIX = "godotTools";
|
||||
|
||||
export function get_configuration(name: string, default_value?: any) {
|
||||
let config_value = vscode.workspace.getConfiguration(EXTENSION_PREFIX).get(name, null);
|
||||
if (default_value && config_value === null) {
|
||||
return default_value;
|
||||
export function get_configuration(name: string, defaultValue?: any) {
|
||||
const configValue = vscode.workspace.getConfiguration(EXTENSION_PREFIX).get(name, null);
|
||||
if (defaultValue && configValue === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return config_value;
|
||||
return configValue;
|
||||
}
|
||||
|
||||
export function set_configuration(name: string, value: any) {
|
||||
@@ -40,16 +40,19 @@ export function get_word_under_cursor(): string {
|
||||
return symbolName;
|
||||
}
|
||||
|
||||
export async function get_project_version(): Promise<string | undefined> {
|
||||
const project_dir = await get_project_dir();
|
||||
export let projectVersion = undefined;
|
||||
|
||||
if (!project_dir) {
|
||||
export async function get_project_version(): Promise<string | undefined> {
|
||||
const dir = await get_project_dir();
|
||||
|
||||
if (!dir) {
|
||||
projectVersion = undefined;
|
||||
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);
|
||||
let godotVersion = "3.x";
|
||||
const projectFile = vscode.Uri.file(path.join(dir, "project.godot"));
|
||||
const document = await vscode.workspace.openTextDocument(projectFile);
|
||||
const text = document.getText();
|
||||
|
||||
const match = text.match(/config\/features=PackedStringArray\((.*)\)/);
|
||||
@@ -57,25 +60,30 @@ export async function get_project_version(): Promise<string | undefined> {
|
||||
const line = match[0];
|
||||
const version = line.match(/\"(4.[0-9]+)\"/);
|
||||
if (version) {
|
||||
godot_version = version[1];
|
||||
godotVersion = version[1];
|
||||
}
|
||||
}
|
||||
return godot_version;
|
||||
|
||||
projectVersion = godotVersion;
|
||||
return godotVersion;
|
||||
}
|
||||
|
||||
export let projectDir = undefined;
|
||||
|
||||
export async function get_project_dir() {
|
||||
let project_dir = undefined;
|
||||
let project_file = '';
|
||||
let dir = undefined;
|
||||
let projectFile = "";
|
||||
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);
|
||||
projectFile = files[0].fsPath;
|
||||
if (fs.existsSync(projectFile) && fs.statSync(projectFile).isFile()) {
|
||||
dir = path.dirname(projectFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
return project_dir;
|
||||
projectDir = dir;
|
||||
return dir;
|
||||
}
|
||||
|
||||
export function find_project_file(start: string, depth: number = 20) {
|
||||
@@ -86,10 +94,10 @@ export function find_project_file(start: string, depth: number = 20) {
|
||||
if (start == folder) {
|
||||
return null;
|
||||
}
|
||||
const project_file = path.join(folder, "project.godot");
|
||||
const projectFile = path.join(folder, "project.godot");
|
||||
|
||||
if (fs.existsSync(project_file) && fs.statSync(project_file).isFile()) {
|
||||
return project_file;
|
||||
if (fs.existsSync(projectFile) && fs.statSync(projectFile).isFile()) {
|
||||
return projectFile;
|
||||
} else {
|
||||
if (depth === 0) {
|
||||
return null;
|
||||
@@ -116,8 +124,8 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
|
||||
if (!files) {
|
||||
return null;
|
||||
}
|
||||
const project_dir = files[0].fsPath.replace("project.godot", "");
|
||||
return vscode.Uri.joinPath(vscode.Uri.file(project_dir), resPath.substring(6));
|
||||
const dir = files[0].fsPath.replace("project.godot", "");
|
||||
return vscode.Uri.joinPath(vscode.Uri.file(dir), resPath.substring(6));
|
||||
}
|
||||
|
||||
export async function get_free_port(): Promise<number> {
|
||||
|
||||
33
src/utils/prompts.ts
Normal file
33
src/utils/prompts.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as vscode from "vscode";
|
||||
import { set_configuration } from "../utils";
|
||||
|
||||
export function 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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function 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);
|
||||
prompt_for_reload();
|
||||
});
|
||||
}
|
||||
|
||||
export function prompt_for_godot_executable(message: string, settingName: string) {
|
||||
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
|
||||
if (item == "Select Godot executable") {
|
||||
select_godot_executable(settingName);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,10 @@ 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';
|
||||
import { ChildProcess, execSync, spawn, SpawnOptions } from "child_process";
|
||||
import { createLogger } from "../logger";
|
||||
|
||||
const log = createLogger("subspawn");
|
||||
|
||||
interface DictionaryOfStringChildProcessArray {
|
||||
[key: string]: ChildProcess[];
|
||||
@@ -20,17 +23,23 @@ export function killSubProcesses(owner: string) {
|
||||
children[owner].forEach((c) => {
|
||||
try {
|
||||
if (c.pid) {
|
||||
if (process.platform === 'win32') {
|
||||
if (process.platform === "win32") {
|
||||
execSync(`taskkill /pid ${c.pid} /T /F`);
|
||||
} else if (process.platform === "darwin") {
|
||||
execSync(`kill -9 ${c.pid}`);
|
||||
} else {
|
||||
process.kill(-c.pid);
|
||||
process.kill(c.pid);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
} catch {
|
||||
log.error(`couldn't kill task ${owner}`);
|
||||
}
|
||||
});
|
||||
|
||||
children[owner] = [];
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
process.on("exit", () => {
|
||||
Object.keys(children).forEach((owner) => killSubProcesses(owner));
|
||||
});
|
||||
|
||||
@@ -38,9 +47,9 @@ function gracefulExitHandler() {
|
||||
process.exit();
|
||||
}
|
||||
|
||||
process.on('SIGINT', gracefulExitHandler);
|
||||
process.on('SIGTERM', gracefulExitHandler);
|
||||
process.on('SIGQUIT', gracefulExitHandler);
|
||||
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);
|
||||
|
||||
@@ -86,7 +86,7 @@ function get_class_list(modules) {
|
||||
}
|
||||
|
||||
function discover_modules() {
|
||||
const modules = []
|
||||
const modules = [];
|
||||
|
||||
// a valid module is a subdir of modulesPath, and contains a subdir 'icons'
|
||||
fs.readdirSync(modulesPath, {withFileTypes:true}).forEach(mod => {
|
||||
@@ -106,7 +106,7 @@ function get_icons() {
|
||||
const modules = discover_modules();
|
||||
const classes = get_class_list(modules);
|
||||
|
||||
const searchPaths = [iconsPath]
|
||||
const searchPaths = [iconsPath];
|
||||
modules.forEach(mod => {
|
||||
searchPaths.push(join(mod, 'icons'));
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"target": "es2020",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"es2022",
|
||||
"dom"
|
||||
],
|
||||
"sourceMap": true,
|
||||
|
||||
Reference in New Issue
Block a user