Bladeren bron

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 <[email protected]>
Co-authored-by: Zachary Gardner <[email protected]>
Daelon Suzuka 1 jaar geleden
bovenliggende
commit
a4c1181894
48 gewijzigde bestanden met toevoegingen van 5347 en 4102 verwijderingen
  1. 55 0
      .eslintrc.json
  2. 22 24
      README.md
  3. 512 2104
      package-lock.json
  4. 251 102
      package.json
  5. 0 7
      src/debugger/commands/command.ts
  6. 0 170
      src/debugger/commands/command_parser.ts
  7. 0 9
      src/debugger/commands/commands/command_debug_enter.ts
  8. 0 8
      src/debugger/commands/commands/command_debug_exit.ts
  9. 0 18
      src/debugger/commands/commands/command_message_inspect_object.ts
  10. 0 25
      src/debugger/commands/commands/command_message_scene_tree.ts
  11. 0 5
      src/debugger/commands/commands/command_null.ts
  12. 0 9
      src/debugger/commands/commands/command_output.ts
  13. 0 17
      src/debugger/commands/commands/command_stack_dump.ts
  14. 0 31
      src/debugger/commands/commands/command_stack_frame_vars.ts
  15. 0 3
      src/debugger/debug_adapter.ts
  16. 119 37
      src/debugger/debug_runtime.ts
  17. 318 0
      src/debugger/debugger.ts
  18. 0 223
      src/debugger/debugger_context.ts
  19. 304 254
      src/debugger/godot3/debug_session.ts
  20. 116 0
      src/debugger/godot3/helpers.ts
  21. 523 0
      src/debugger/godot3/server_controller.ts
  22. 80 84
      src/debugger/godot3/variables/variant_decoder.ts
  23. 11 14
      src/debugger/godot3/variables/variant_encoder.ts
  24. 1 1
      src/debugger/godot3/variables/variants.ts
  25. 550 0
      src/debugger/godot4/debug_session.ts
  26. 123 0
      src/debugger/godot4/helpers.ts
  27. 529 0
      src/debugger/godot4/server_controller.ts
  28. 652 0
      src/debugger/godot4/variables/variant_decoder.ts
  29. 446 0
      src/debugger/godot4/variables/variant_encoder.ts
  30. 475 0
      src/debugger/godot4/variables/variants.ts
  31. 11 12
      src/debugger/inspector_provider.ts
  32. 0 263
      src/debugger/mediator.ts
  33. 20 61
      src/debugger/scene_tree_provider.ts
  34. 0 314
      src/debugger/server_controller.ts
  35. 39 125
      src/extension.ts
  36. 57 71
      src/logger.ts
  37. 24 58
      src/lsp/ClientConnectionManager.ts
  38. 10 7
      src/lsp/GDScriptLanguageClient.ts
  39. 7 2
      src/lsp/MessageIO.ts
  40. 2 2
      src/lsp/NativeDocumentManager.ts
  41. 2 2
      src/scene_preview_provider.ts
  42. 4 6
      src/settings_updater.ts
  43. 31 23
      src/utils.ts
  44. 33 0
      src/utils/prompts.ts
  45. 17 8
      src/utils/subspawn.ts
  46. 0 0
      syntaxes/examples/project.godot
  47. 2 2
      tools/generate_icons.ts
  48. 1 1
      tsconfig.json

+ 55 - 0
.eslintrc.json

@@ -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"
+        ]
+    }
+}

+ 22 - 24
README.md

@@ -3,12 +3,6 @@
 A complete set of tools to code games with
 A complete set of tools to code games with
 [Godot Engine](http://www.godotengine.org/) in Visual Studio Code.
 [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
 **IMPORTANT NOTE:** Versions 1.0.0 and later of this extension only support
 Godot 3.2 or later.
 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 GDScript (`.gd`) language
 - Syntax highlighting for the `.tscn` and `.tres` scene formats
 - Syntax highlighting for the `.tscn` and `.tres` scene formats
+- Syntax highlighting for the `.gdshader` shader format
 - Full typed GDScript support
 - Full typed GDScript support
 - Optional "Smart Mode" to improve productivity with dynamically typed scripts
 - Optional "Smart Mode" to improve productivity with dynamically typed scripts
 - Function definitions and documentation display on hover (see image below)
 - Function definitions and documentation display on hover (see image below)
 - Rich autocompletion
 - 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
 - Display script warnings and errors
 - Ctrl + click on a variable or method call to jump to its definition
 - 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)
 - 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:
 You can use the following settings to configure Godot Tools:
 
 
-##### `editor_path`
-
-The absolute path to the Godot editor executable. _Under Mac OS, this is the executable inside of Godot.app._
-
-##### `gdscript_lsp_server_port`
+- `godotTools.editorPath.godot3`
+- `godotTools.editorPath.godot4`
 
 
-The WebSocket server port of the GDScript language server.
+The path to the Godot editor executable. _Under Mac OS, this is the executable inside of Godot.app._
 
 
-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
 #### GDScript Debugger
 
 
@@ -102,19 +95,24 @@ To configure the GDScript debugger:
 5. Change any relevant settings.
 5. Change any relevant settings.
 6. Press F5 to launch.
 6. Press F5 to launch.
 
 
-*Configurations*
+### *Configurations*
 
 
 _Required_
 _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_
 _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*
 *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
 - GDScript is a dynamically typed script language. The language server can't
   infer all variable types.
   infer all variable types.
 - To increase the number of results displayed, open the **Editor Settings**,
 - 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**.

File diff suppressed because it is too large
+ 512 - 2104
package-lock.json


+ 251 - 102
package.json

@@ -31,7 +31,7 @@
 	"main": "./out/extension.js",
 	"main": "./out/extension.js",
 	"scripts": {
 	"scripts": {
 		"compile": "tsc -p ./",
 		"compile": "tsc -p ./",
-		"lint": "tslint -p ./",
+		"lint": "eslint ./src --quiet",
 		"watch": "tsc -watch -p ./",
 		"watch": "tsc -watch -p ./",
 		"package": "vsce package",
 		"package": "vsce package",
 		"vscode:prepublish": "npm run esbuild-base -- --minify",
 		"vscode:prepublish": "npm run esbuild-base -- --minify",
@@ -43,34 +43,32 @@
 	"contributes": {
 	"contributes": {
 		"commands": [
 		"commands": [
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.openEditor",
 				"command": "godotTools.openEditor",
-				"title": "Godot Tools: Open workspace with Godot editor"
+				"title": "Open workspace with Godot editor"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.startLanguageServer",
 				"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",
 				"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",
 				"command": "godotTools.listNativeClasses",
-				"title": "Godot Tools: List native classes of godot"
+				"title": "List native classes of godot"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.openTypeDocumentation",
 				"command": "godotTools.openTypeDocumentation",
-				"title": "Godot Tools: Open Type Documentation"
+				"title": "Open Type Documentation"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.debugger.inspectNode",
 				"command": "godotTools.debugger.inspectNode",
 				"title": "Inspect Remote Node",
 				"title": "Inspect Remote Node",
 				"icon": {
 				"icon": {
@@ -79,6 +77,7 @@
 				}
 				}
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.debugger.refreshSceneTree",
 				"command": "godotTools.debugger.refreshSceneTree",
 				"title": "Refresh",
 				"title": "Refresh",
 				"icon": {
 				"icon": {
@@ -87,6 +86,7 @@
 				}
 				}
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.debugger.refreshInspector",
 				"command": "godotTools.debugger.refreshInspector",
 				"title": "Refresh",
 				"title": "Refresh",
 				"icon": {
 				"icon": {
@@ -95,6 +95,7 @@
 				}
 				}
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.debugger.editValue",
 				"command": "godotTools.debugger.editValue",
 				"title": "Edit value",
 				"title": "Edit value",
 				"icon": {
 				"icon": {
@@ -103,54 +104,85 @@
 				}
 				}
 			},
 			},
 			{
 			{
+				"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",
 				"command": "godotTools.scenePreview.refresh",
-				"title": "Godot Tools: Refresh Scene Preview"
+				"title": "Refresh Scene Preview"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.scenePreview.pin",
 				"command": "godotTools.scenePreview.pin",
 				"title": "Pin Scene Preview",
 				"title": "Pin Scene Preview",
 				"icon": "resources/pin_off.svg"
 				"icon": "resources/pin_off.svg"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.scenePreview.unpin",
 				"command": "godotTools.scenePreview.unpin",
 				"title": "Unpin Scene Preview",
 				"title": "Unpin Scene Preview",
 				"icon": "resources/pin_on.svg"
 				"icon": "resources/pin_on.svg"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.scenePreview.goToDefinition",
 				"command": "godotTools.scenePreview.goToDefinition",
 				"title": "Go to Definition"
 				"title": "Go to Definition"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.scenePreview.copyNodePath",
 				"command": "godotTools.scenePreview.copyNodePath",
 				"title": "Copy Node Path"
 				"title": "Copy Node Path"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.scenePreview.copyResourcePath",
 				"command": "godotTools.scenePreview.copyResourcePath",
 				"title": "Copy Resource Path"
 				"title": "Copy Resource Path"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.scenePreview.openScene",
 				"command": "godotTools.scenePreview.openScene",
 				"title": "Open Scene"
 				"title": "Open Scene"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.scenePreview.openScript",
 				"command": "godotTools.scenePreview.openScript",
 				"title": "Open Script"
 				"title": "Open Script"
 			},
 			},
 			{
 			{
+				"category": "Godot Tools",
 				"command": "godotTools.switchSceneScript",
 				"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",
 				"command": "godotTools.copyResourcePath",
-				"title": "Godot Tools: Copy Resource Path"
+				"title": "Copy Resource Path"
 			}
 			}
 		],
 		],
 		"keybindings": [
 		"keybindings": [
@@ -164,6 +196,16 @@
 			"type": "object",
 			"type": "object",
 			"title": "Godot Tools",
 			"title": "Godot Tools",
 			"properties": {
 			"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": {
 				"godotTools.lsp.serverProtocol": {
 					"type": [
 					"type": [
 						"string"
 						"string"
@@ -194,21 +236,6 @@
 					"default": false,
 					"default": false,
 					"description": "Whether to launch the LSP as a headless child process"
 					"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": {
 				"godotTools.lsp.autoReconnect.enabled": {
 					"type": "boolean",
 					"type": "boolean",
 					"default": true,
 					"default": true,
@@ -224,27 +251,27 @@
 					"default": 10,
 					"default": 10,
 					"description": "How many times the client will attempt to reconnect"
 					"description": "How many times the client will attempt to reconnect"
 				},
 				},
-				"godotTools.forceVisibleCollisionShapes": {
+				"godotTools.debugger.forceVisibleCollisionShapes": {
 					"type": "boolean",
 					"type": "boolean",
 					"default": false,
 					"default": false,
 					"description": "Force the project to run with visible collision shapes"
 					"description": "Force the project to run with visible collision shapes"
 				},
 				},
-				"godotTools.forceVisibleNavMesh": {
+				"godotTools.debugger.forceVisibleNavMesh": {
 					"type": "boolean",
 					"type": "boolean",
 					"default": false,
 					"default": false,
 					"description": "Force the project to run with visible navigation meshes"
 					"description": "Force the project to run with visible navigation meshes"
 				},
 				},
-				"godotTools.nativeSymbolPlacement": {
+				"godotTools.documentation.newTabPlacement": {
 					"enum": [
 					"enum": [
 						"active",
 						"active",
 						"beside"
 						"beside"
 					],
 					],
 					"enumDescriptions": [
 					"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",
 					"default": "beside",
-					"description": "Where to place the native symbol windows"
+					"description": "Where to place new documentation views"
 				},
 				},
 				"godotTools.scenePreview.previewRelatedScenes": {
 				"godotTools.scenePreview.previewRelatedScenes": {
 					"enum": [
 					"enum": [
@@ -274,6 +301,17 @@
 				],
 				],
 				"configuration": "./configurations/gdscript-configuration.json"
 				"configuration": "./configurations/gdscript-configuration.json"
 			},
 			},
+			{
+				"id": "gdscene",
+				"aliases": [
+					"GDScene",
+					"gdscene"
+				],
+				"extensions": [
+					"tscn"
+				],
+				"configuration": "./configurations/gdresource-configuration.json"
+			},
 			{
 			{
 				"id": "gdresource",
 				"id": "gdresource",
 				"aliases": [
 				"aliases": [
@@ -283,7 +321,6 @@
 				"extensions": [
 				"extensions": [
 					"godot",
 					"godot",
 					"tres",
 					"tres",
-					"tscn",
 					"import",
 					"import",
 					"gdns",
 					"gdns",
 					"gdnlib"
 					"gdnlib"
@@ -328,64 +365,74 @@
 			{
 			{
 				"type": "godot",
 				"type": "godot",
 				"label": "GDScript Godot Debug",
 				"label": "GDScript Godot Debug",
-				"program": "./out/debugger/debug_adapter.js",
 				"runtime": "node",
 				"runtime": "node",
 				"configurationAttributes": {
 				"configurationAttributes": {
 					"launch": {
 					"launch": {
-						"required": [
-							"project",
-							"port",
-							"address"
-						],
+						"required": [],
 						"properties": {
 						"properties": {
 							"project": {
 							"project": {
 								"type": "string",
 								"type": "string",
 								"description": "Absolute path to a directory with a project.godot file.",
 								"description": "Absolute path to a directory with a project.godot file.",
 								"default": "${workspaceFolder}"
 								"default": "${workspaceFolder}"
 							},
 							},
-							"port": {
-								"type": "number",
-								"description": "The port number for the Godot remote debugger to use.",
-								"default": 6007
-							},
 							"address": {
 							"address": {
 								"type": "string",
 								"type": "string",
 								"description": "The IP address for the Godot remote debugger to use.",
 								"description": "The IP address for the Godot remote debugger to use.",
 								"default": "127.0.0.1"
 								"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
-							},
-							"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
+							"port": {
+								"type": "number",
+								"description": "The port number for the Godot remote debugger to use.",
+								"default": 6007
 							},
 							},
-							"scene_file": {
+							"scene": {
 								"type": "string",
 								"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": ""
 								"default": ""
 							},
 							},
+							"editor_path": {
+								"type": "string",
+								"description": "Absolute path to the Godot executable to be used for this debug profile."
+							},
 							"additional_options": {
 							"additional_options": {
 								"type": "string",
 								"type": "string",
 								"description": "Additional command line arguments.",
 								"description": "Additional command line arguments.",
 								"default": ""
 								"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": [
 				"initialConfigurations": [
 					{
 					{
-						"name": "GDScript Godot",
+						"name": "GDScript: Launch Godot",
 						"type": "godot",
 						"type": "godot",
 						"request": "launch",
 						"request": "launch",
 						"project": "${workspaceFolder}",
 						"project": "${workspaceFolder}",
-						"port": 6007,
-						"address": "127.0.0.1",
-						"launch_game_instance": true,
-						"launch_scene": false,
 						"additional_options": ""
 						"additional_options": ""
 					}
 					}
 				],
 				],
@@ -394,15 +441,47 @@
 						"label": "GDScript Godot Debug: Launch",
 						"label": "GDScript Godot Debug: Launch",
 						"description": "A new configuration for debugging a Godot project.",
 						"description": "A new configuration for debugging a Godot project.",
 						"body": {
 						"body": {
+							"name": "GDScript: Launch Project",
 							"type": "godot",
 							"type": "godot",
 							"request": "launch",
 							"request": "launch",
 							"project": "${workspaceFolder}",
 							"project": "${workspaceFolder}",
-							"port": 6007,
-							"address": "127.0.0.1",
-							"launch_game_instance": true,
-							"launch_scene": false,
 							"additional_options": ""
 							"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": {
 		"views": {
 			"debug": [
 			"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": [
 			"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": {
 		"menus": {
 			"commandPalette": [
 			"commandPalette": [
 				{
 				{
@@ -476,46 +567,58 @@
 					"when": "false"
 					"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"
 					"when": "false"
 				}
 				}
 			],
 			],
 			"view/title": [
 			"view/title": [
 				{
 				{
 					"command": "godotTools.debugger.refreshSceneTree",
 					"command": "godotTools.debugger.refreshSceneTree",
-					"when": "view == active-scene-tree",
+					"when": "view == activeSceneTree",
 					"group": "navigation"
 					"group": "navigation"
 				},
 				},
 				{
 				{
 					"command": "godotTools.debugger.refreshInspector",
 					"command": "godotTools.debugger.refreshInspector",
-					"when": "view == inspect-node",
+					"when": "view == inspectNode",
 					"group": "navigation"
 					"group": "navigation"
 				},
 				},
 				{
 				{
 					"command": "godotTools.scenePreview.pin",
 					"command": "godotTools.scenePreview.pin",
-					"when": "view == scenePreview && !godotTools.context.scenePreviewPinned",
+					"when": "view == scenePreview && !godotTools.context.scenePreview.pinned",
 					"group": "navigation"
 					"group": "navigation"
 				},
 				},
 				{
 				{
 					"command": "godotTools.scenePreview.unpin",
 					"command": "godotTools.scenePreview.unpin",
-					"when": "view == scenePreview && godotTools.context.scenePreviewPinned",
+					"when": "view == scenePreview && godotTools.context.scenePreview.pinned",
 					"group": "navigation"
 					"group": "navigation"
 				}
 				}
 			],
 			],
 			"view/item/context": [
 			"view/item/context": [
 				{
 				{
 					"command": "godotTools.debugger.inspectNode",
 					"command": "godotTools.debugger.inspectNode",
-					"when": "view == active-scene-tree",
+					"when": "view == activeSceneTree",
 					"group": "inline"
 					"group": "inline"
 				},
 				},
 				{
 				{
 					"command": "godotTools.debugger.inspectNode",
 					"command": "godotTools.debugger.inspectNode",
-					"when": "view == inspect-node && viewItem == remote_object",
+					"when": "view == inspectNode && viewItem == remote_object",
 					"group": "inline"
 					"group": "inline"
 				},
 				},
 				{
 				{
 					"command": "godotTools.debugger.editValue",
 					"command": "godotTools.debugger.editValue",
-					"when": "view == inspect-node && viewItem == editable_value",
+					"when": "view == inspectNode && viewItem == editable_value",
 					"group": "inline"
 					"group": "inline"
 				},
 				},
 				{
 				{
@@ -544,18 +647,58 @@
 			],
 			],
 			"explorer/context": [
 			"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"
 					"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": [
 			"editor/title/context": [
 				{
 				{
-					"command": "godotTools.copyResourcePathContext",
+					"command": "godotTools.copyResourcePath",
 					"group": "1_godot"
 					"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": [
 			"editor/context": [
@@ -566,34 +709,40 @@
 				},
 				},
 				{
 				{
 					"command": "godotTools.switchSceneScript",
 					"command": "godotTools.switchSceneScript",
-					"when": "editorLangId == 'gdscript' || editorLangId == 'gdresource'",
+					"when": "editorLangId in godotTools.context.sceneLikeFiles",
 					"group": "custom1@1"
 					"group": "custom1@1"
 				}
 				}
 			]
 			]
 		}
 		}
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
-		"@types/marked": "^0.6.5",
+		"@types/marked": "^4.0.8",
 		"@types/mocha": "^9.1.0",
 		"@types/mocha": "^9.1.0",
 		"@types/node": "^18.15.0",
 		"@types/node": "^18.15.0",
 		"@types/prismjs": "^1.16.8",
 		"@types/prismjs": "^1.16.8",
 		"@types/vscode": "^1.80.0",
 		"@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",
 		"@vscode/vsce": "^2.21.0",
-		"esbuild": "^0.15.2",
+		"esbuild": "^0.17.15",
+		"eslint": "^8.37.0",
 		"ts-node": "^10.9.1",
 		"ts-node": "^10.9.1",
+		"tsconfig-paths": "^4.2.0",
 		"tslint": "^5.20.1",
 		"tslint": "^5.20.1",
 		"typescript": "^5.2.2"
 		"typescript": "^5.2.2"
 	},
 	},
 	"dependencies": {
 	"dependencies": {
+		"@vscode/debugadapter": "^1.64.0",
+		"@vscode/debugprotocol": "^1.64.0",
 		"await-notify": "^1.0.1",
 		"await-notify": "^1.0.1",
 		"global": "^4.4.0",
 		"global": "^4.4.0",
 		"marked": "^4.0.11",
 		"marked": "^4.0.11",
 		"net": "^1.0.2",
 		"net": "^1.0.2",
 		"prismjs": "^1.17.1",
 		"prismjs": "^1.17.1",
 		"terminate": "^2.5.0",
 		"terminate": "^2.5.0",
-		"vscode-debugadapter": "^1.38.0",
 		"vscode-languageclient": "^7.0.0",
 		"vscode-languageclient": "^7.0.0",
-		"ws": "^8.4.2"
+		"ws": "^8.13.0"
 	}
 	}
 }
 }

+ 0 - 7
src/debugger/commands/command.ts

@@ -1,7 +0,0 @@
-import { Mediator } from "../mediator";
-
-export abstract class Command {
-	public param_count: number = -1;
-
-	public abstract trigger(parameters: any[]): void;
-}

+ 0 - 170
src/debugger/commands/command_parser.ts

@@ -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;
-	}
-}

+ 0 - 9
src/debugger/commands/commands/command_debug_enter.ts

@@ -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]);
-	}
-}

+ 0 - 8
src/debugger/commands/commands/command_debug_exit.ts

@@ -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");
-	}
-}

+ 0 - 18
src/debugger/commands/commands/command_message_inspect_object.ts

@@ -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]);
-	}
-}

+ 0 - 25
src/debugger/commands/commands/command_message_scene_tree.ts

@@ -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);
-	}
-}

+ 0 - 5
src/debugger/commands/commands/command_null.ts

@@ -1,5 +0,0 @@
-import { Command } from "../command";
-
-export class CommandNull extends Command {
-	public trigger(parameters: any[]) {}
-}

+ 0 - 9
src/debugger/commands/commands/command_output.ts

@@ -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);
-	}
-}

+ 0 - 17
src/debugger/commands/commands/command_stack_dump.ts

@@ -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);
-	}
-}

+ 0 - 31
src/debugger/commands/commands/command_stack_frame_vars.ts

@@ -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]);
-	}
-}

+ 0 - 3
src/debugger/debug_adapter.ts

@@ -1,3 +0,0 @@
-import { GodotDebugSession } from "./debug_session";
-
-GodotDebugSession.run(GodotDebugSession);

+ 119 - 37
src/debugger/debug_runtime.ts

@@ -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 {
 export interface GodotBreakpoint {
 	file: string;
 	file: string;
@@ -15,11 +17,64 @@ export interface GodotStackFrame {
 	line: number;
 	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 {
 export interface GodotVariable {
 	name: string;
 	name: string;
 	scope_path?: string;
 	scope_path?: string;
 	sub_values?: GodotVariable[];
 	sub_values?: GodotVariable[];
 	value: any;
 	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 {
 export class GodotDebugData {
@@ -28,63 +83,90 @@ export class GodotDebugData {
 
 
 	public last_frame: GodotStackFrame;
 	public last_frame: GodotStackFrame;
 	public last_frames: GodotStackFrame[] = [];
 	public last_frames: GodotStackFrame[] = [];
-	public project_path: string;
+	public projectPath: string;
 	public scene_tree?: SceneTreeProvider;
 	public scene_tree?: SceneTreeProvider;
 	public stack_count: number = 0;
 	public stack_count: number = 0;
 	public stack_files: string[] = [];
 	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 constructor(session) {
+		this.session = session;
 	}
 	}
 
 
-	public get_breakpoints(path: string) {
-		return this.breakpoints.get(path) || [];
+	public set_breakpoint(path_to: string, line: number) {
+		const bp = {
+			file: path_to.replace(/\\/g, "/"),
+			line: line,
+			id: this.breakpoint_id++,
+		};
+
+		let bps: GodotBreakpoint[] = this.breakpoints.get(bp.file);
+		if (!bps) {
+			bps = [];
+			this.breakpoints.set(bp.file, bps);
+		}
+
+		bps.push(bp);
+
+		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(path_to: string, line: number) {
-		let bps = this.breakpoints.get(path_to);
+	public remove_breakpoint(pathTo: string, line: number) {
+		const bps = this.breakpoints.get(pathTo);
 
 
 		if (bps) {
 		if (bps) {
-			let index = bps.findIndex((bp) => {
+			const index = bps.findIndex((bp) => {
 				return bp.line === line;
 				return bp.line === line;
 			});
 			});
 			if (index !== -1) {
 			if (index !== -1) {
-				let bp = bps[index];
+				const bp = bps[index];
 				bps.splice(index, 1);
 				bps.splice(index, 1);
-				this.breakpoints.set(path_to, bps);
-				let file = `res://${path.relative(this.project_path, bp.file)}`;
-				Mediator.notify("remove_breakpoint", [
+				this.breakpoints.set(pathTo, bps);
+				const file = `res://${path.relative(this.projectPath, bp.file)}`;
+				this.session?.controller.remove_breakpoint(
 					file.replace(/\\/g, "/"),
 					file.replace(/\\/g, "/"),
 					bp.line,
 					bp.line,
-				]);
+				);
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	public set_breakpoint(path_to: string, line: number) {
-		let bp = {
-			file: path_to.replace(/\\/g, "/"),
-			line: line,
-			id: this.breakpoint_id++,
-		};
+	public get_all_breakpoints(): GodotBreakpoint[] {
+		const output: GodotBreakpoint[] = [];
+		Array.from(this.breakpoints.values()).forEach((bp_array) => {
+			output.push(...bp_array);
+		});
+		return output;
+	}
 
 
-		let bps: GodotBreakpoint[] = this.breakpoints.get(bp.file);
-		if (!bps) {
-			bps = [];
-			this.breakpoints.set(bp.file, bps);
-		}
+	public get_breakpoints(path: string) {
+		return this.breakpoints.get(path) || [];
+	}
 
 
-		bps.push(bp);
+	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;
+	}
 
 
-		if (this.project_path) {
-			let out_file = `res://${path.relative(this.project_path, bp.file)}`;
-			Mediator.notify("set_breakpoint", [out_file.replace(/\\/g, "/"), line]);
+	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 - 0
src/debugger/debugger.ts

@@ -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
+						);
+					},
+				);
+			});
+	}
+}

+ 0 - 223
src/debugger/debugger_context.ts

@@ -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;
-	}
-}

+ 304 - 254
src/debugger/debug_session.ts → src/debugger/godot3/debug_session.ts

@@ -1,84 +1,111 @@
+import * as fs from "fs";
 import {
 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 { ServerController } from "./server_controller";
-import { ObjectId, RawObject } from "./variables/variants";
-const { Subject } = require("await-notify");
-import fs = require("fs");
-
-interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
-	address: string;
-	launch_game_instance: boolean;
-	launch_scene: boolean;
-	port: number;
-	project: string;
-	scene_file: string;
-	additional_options: string;
-}
+import { createLogger } from "../../logger";
+
+const log = createLogger("debugger.session", { output: "Godot Debugger" });
 
 
 export class GodotDebugSession extends LoggingDebugSession {
 export class GodotDebugSession extends LoggingDebugSession {
 	private all_scopes: GodotVariable[];
 	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 exception = false;
-	private got_scope = new Subject();
+	private got_scope: Subject = new Subject();
 	private ongoing_inspections: bigint[] = [];
 	private ongoing_inspections: bigint[] = [];
 	private previous_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() {
 	public constructor() {
 		super();
 		super();
 
 
 		this.setDebuggerLinesStartAt1(false);
 		this.setDebuggerLinesStartAt1(false);
 		this.setDebuggerColumnsStartAt1(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() {
+		this.controller.stop();
 	}
 	}
 
 
-	public dispose() {}
+	protected initializeRequest(
+		response: DebugProtocol.InitializeResponse,
+		args: DebugProtocol.InitializeRequestArguments
+	) {
+		response.body = response.body || {};
 
 
-	public set_exception(exception: boolean) {
-		this.exception = true;
+		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());
 	}
 	}
 
 
-	public set_inspection(id: bigint, replacement: GodotVariable) {
-		let variables = this.all_scopes.filter(
-			(va) => va && va.value instanceof ObjectId && va.value.id === id
-		);
+	protected async launchRequest(
+		response: DebugProtocol.LaunchResponse,
+		args: LaunchRequestArguments
+	) {
+		await this.configuration_done.wait(1000);
 
 
-		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);
-		});
+		this.mode = "launch";
 
 
-		this.ongoing_inspections.splice(
-			this.ongoing_inspections.findIndex((va_id) => va_id === id),
-			1
-		);
+		this.debug_data.projectPath = args.project;
+		this.exception = false;
+		await this.controller.launch(args);
 
 
-		this.previous_inspections.push(id);
+		this.sendResponse(response);
+	}
 
 
-		this.add_to_inspections();
+	protected async attachRequest(
+		response: DebugProtocol.AttachResponse,
+		args: AttachRequestArguments
+	) {
+		await this.configuration_done.wait(1000);
 
 
-		if (this.ongoing_inspections.length === 0) {
-			this.previous_inspections = [];
-			this.got_scope.notify();
-		}
-	}
+		this.mode = "attach";
+
+		this.exception = false;
+		await this.controller.attach(args);
 
 
-	public set_scene_tree(scene_tree_provider: SceneTreeProvider) {
-		this.debug_data.scene_tree = scene_tree_provider;
+		this.sendResponse(response);
 	}
 	}
 
 
 	public configurationDoneRequest(
 	public configurationDoneRequest(
@@ -89,80 +116,35 @@ export class GodotDebugSession extends LoggingDebugSession {
 		this.sendResponse(response);
 		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(
 	protected continueRequest(
 		response: DebugProtocol.ContinueResponse,
 		response: DebugProtocol.ContinueResponse,
 		args: DebugProtocol.ContinueArguments
 		args: DebugProtocol.ContinueArguments
 	) {
 	) {
 		if (!this.exception) {
 		if (!this.exception) {
 			response.body = { allThreadsContinued: true };
 			response.body = { allThreadsContinued: true };
-			Mediator.notify("continue");
+			this.controller.continue();
 			this.sendResponse(response);
 			this.sendResponse(response);
 		}
 		}
 	}
 	}
 
 
-	protected evaluateRequest(
+	protected async evaluateRequest(
 		response: DebugProtocol.EvaluateResponse,
 		response: DebugProtocol.EvaluateResponse,
 		args: DebugProtocol.EvaluateArguments
 		args: DebugProtocol.EvaluateArguments
 	) {
 	) {
+		await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
+
 		if (this.all_scopes) {
 		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);
 		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(
 	protected nextRequest(
 		response: DebugProtocol.NextResponse,
 		response: DebugProtocol.NextResponse,
 		args: DebugProtocol.NextArguments
 		args: DebugProtocol.NextArguments
 	) {
 	) {
 		if (!this.exception) {
 		if (!this.exception) {
-			Mediator.notify("next");
+			this.controller.next();
 			this.sendResponse(response);
 			this.sendResponse(response);
 		}
 		}
 	}
 	}
@@ -255,7 +173,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 		args: DebugProtocol.PauseArguments
 		args: DebugProtocol.PauseArguments
 	) {
 	) {
 		if (!this.exception) {
 		if (!this.exception) {
-			Mediator.notify("break");
+			this.controller.break();
 			this.sendResponse(response);
 			this.sendResponse(response);
 		}
 		}
 	}
 	}
@@ -264,10 +182,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.ScopesResponse,
 		response: DebugProtocol.ScopesResponse,
 		args: DebugProtocol.ScopesArguments
 		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);
 		await this.got_scope.wait(2000);
 
 
 		response.body = {
 		response.body = {
@@ -284,12 +199,12 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.SetBreakpointsResponse,
 		response: DebugProtocol.SetBreakpointsResponse,
 		args: DebugProtocol.SetBreakpointsArguments
 		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)) {
 		if (fs.existsSync(path)) {
 			let bps = this.debug_data.get_breakpoints(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) => {
 			bps.forEach((bp) => {
 				if (client_lines.indexOf(bp.line) === -1) {
 				if (client_lines.indexOf(bp.line) === -1) {
@@ -298,7 +213,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 			});
 			});
 			client_lines.forEach((l) => {
 			client_lines.forEach((l) => {
 				if (bp_lines.indexOf(l) === -1) {
 				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) {
 					if (!bp.condition) {
 						this.debug_data.set_breakpoint(path, l);
 						this.debug_data.set_breakpoint(path, l);
 					}
 					}
@@ -339,7 +254,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 						column: 1,
 						column: 1,
 						source: new Source(
 						source: new Source(
 							sf.file,
 							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
 		args: DebugProtocol.StepInArguments
 	) {
 	) {
 		if (!this.exception) {
 		if (!this.exception) {
-			Mediator.notify("step");
+			this.controller.step();
 			this.sendResponse(response);
 			this.sendResponse(response);
 		}
 		}
 	}
 	}
@@ -363,7 +278,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 		args: DebugProtocol.StepOutArguments
 		args: DebugProtocol.StepOutArguments
 	) {
 	) {
 		if (!this.exception) {
 		if (!this.exception) {
-			Mediator.notify("step_out");
+			this.controller.step_out();
 			this.sendResponse(response);
 			this.sendResponse(response);
 		}
 		}
 	}
 	}
@@ -372,7 +287,10 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.TerminateResponse,
 		response: DebugProtocol.TerminateResponse,
 		args: DebugProtocol.TerminateArguments
 		args: DebugProtocol.TerminateArguments
 	) {
 	) {
-		Mediator.notify("stop");
+		if (this.mode === "launch") {
+			this.controller.stop();
+			this.sendEvent(new TerminatedEvent());
+		}
 		this.sendResponse(response);
 		this.sendResponse(response);
 	}
 	}
 
 
@@ -385,25 +303,32 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.VariablesResponse,
 		response: DebugProtocol.VariablesResponse,
 		args: DebugProtocol.VariablesArguments
 		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[];
 		let variables: DebugProtocol.Variable[];
 
 
 		if (!reference.sub_values) {
 		if (!reference.sub_values) {
 			variables = [];
 			variables = [];
 		} else {
 		} else {
 			variables = reference.sub_values.map((va) => {
 			variables = reference.sub_values.map((va) => {
-				let sva = this.all_scopes.find(
+				const sva = this.all_scopes.find(
 					(sva) =>
 					(sva) =>
 						sva && sva.scope_path === va.scope_path && sva.name === va.name
 						sva && sva.scope_path === va.scope_path && sva.name === va.name
 				);
 				);
 				if (sva) {
 				if (sva) {
-					return this.parse_variable(
+					return parse_variable(
 						sva,
 						sva,
 						this.all_scopes.findIndex(
 						this.all_scopes.findIndex(
 							(va_idx) =>
 							(va_idx) =>
 								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
 								va_idx.name === va.name
 						)
 						)
 					);
 					);
@@ -418,83 +343,208 @@ export class GodotDebugSession extends LoggingDebugSession {
 		this.sendResponse(response);
 		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() {
 	private add_to_inspections() {
 		this.all_scopes.forEach((va) => {
 		this.all_scopes.forEach((va) => {
 			if (va && va.value instanceof ObjectId) {
 			if (va && va.value instanceof ObjectId) {
 				if (
 				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);
 					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) {
 	private append_variable(variable: GodotVariable, index?: number) {
 		if (index) {
 		if (index) {
 			this.all_scopes.splice(index, 0, variable);
 			this.all_scopes.splice(index, 0, variable);
 		} else {
 		} else {
 			this.all_scopes.push(variable);
 			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) {
 		if (variable.sub_values) {
 			variable.sub_values.forEach((va, i) => {
 			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);
 				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 - 0
src/debugger/godot3/helpers.ts

@@ -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 - 0
src/debugger/godot3/server_controller.ts

@@ -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);
+	}
+}

+ 80 - 84
src/debugger/variables/variant_decoder.ts → src/debugger/godot3/variables/variant_decoder.ts

@@ -18,7 +18,7 @@ import {
 
 
 export class VariantDecoder {
 export class VariantDecoder {
 	public decode_variant(model: BufferModel) {
 	public decode_variant(model: BufferModel) {
-		let type = this.decode_UInt32(model);
+		const type = this.decode_UInt32(model);
 		switch (type & 0xff) {
 		switch (type & 0xff) {
 			case GDScriptTypes.BOOL:
 			case GDScriptTypes.BOOL:
 				return this.decode_UInt32(model) !== 0;
 				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,
 			buffer: buffer,
-			offset: offset + 4,
+			offset: 4, // data starts after the initial length
 			len: len,
 			len: len,
 		};
 		};
 
 
-		let output = [];
+		const output = [];
 		output.push(len + 4);
 		output.push(len + 4);
 		do {
 		do {
-			let value = this.decode_variant(model);
+			const value = this.decode_variant(model);
 			output.push(value);
 			output.push(value);
 		} while (model.len > 0);
 		} while (model.len > 0);
 
 
@@ -110,12 +113,12 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Array(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
-			let value = this.decode_variant(model);
+			const value = this.decode_variant(model);
 			output.push(value);
 			output.push(value);
 		}
 		}
 
 
@@ -131,19 +134,19 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Color(model: BufferModel) {
 	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);
 		return new Color(rgb.x, rgb.y, rgb.z, a);
 	}
 	}
 
 
 	private decode_Dictionary(model: BufferModel) {
 	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++) {
 		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);
 			output.set(key, value);
 		}
 		}
 
 
@@ -151,7 +154,7 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Double(model: BufferModel) {
 	private decode_Double(model: BufferModel) {
-		let d = model.buffer.readDoubleLE(model.offset);
+		const d = model.buffer.readDoubleLE(model.offset);
 
 
 		model.offset += 8;
 		model.offset += 8;
 		model.len -= 8;
 		model.len -= 8;
@@ -160,7 +163,7 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Float(model: BufferModel) {
 	private decode_Float(model: BufferModel) {
-		let f = model.buffer.readFloatLE(model.offset);
+		const f = model.buffer.readFloatLE(model.offset);
 
 
 		model.offset += 4;
 		model.offset += 4;
 		model.len -= 4;
 		model.len -= 4;
@@ -169,41 +172,53 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Int32(model: BufferModel) {
 	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 result;
+	}
 
 
+	private decode_UInt32(model: BufferModel) {
+		const result = model.buffer.readUInt32LE(model.offset);
 		model.len -= 4;
 		model.len -= 4;
 		model.offset += 4;
 		model.offset += 4;
 
 
-		return u;
+		return result;
 	}
 	}
 
 
 	private decode_Int64(model: BufferModel) {
 	private decode_Int64(model: BufferModel) {
-		let hi = model.buffer.readInt32LE(model.offset);
-		let lo = model.buffer.readInt32LE(model.offset + 4);
+		const result = model.buffer.readBigInt64LE(model.offset);
+		model.len -= 8;
+		model.offset += 8;
 
 
-		let u: BigInt = BigInt((hi << 32) | lo);
+		return result;
+	}
 
 
+	private decode_UInt64(model: BufferModel) {
+		const result = model.buffer.readBigUInt64LE(model.offset);
 		model.len -= 8;
 		model.len -= 8;
 		model.offset += 8;
 		model.offset += 8;
 
 
-		return u;
+		return result;
 	}
 	}
 
 
 	private decode_NodePath(model: BufferModel) {
 	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 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) {
 		if (flags & 2) {
 			//Obsolete format with property separate from subPath
 			//Obsolete format with property separate from subPath
 			subname_count++;
 			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++) {
 		for (let i = 0; i < total; i++) {
-			let str = this.decode_String(model);
+			const str = this.decode_String(model);
 			if (i < name_count) {
 			if (i < name_count) {
 				names.push(str);
 				names.push(str);
 			} else {
 			} else {
@@ -215,13 +230,13 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Object(model: BufferModel) {
 	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++) {
 		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);
 			output.set(name, value);
 		}
 		}
 
 
@@ -229,23 +244,23 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Object_id(model: BufferModel) {
 	private decode_Object_id(model: BufferModel) {
-		let id = this.decode_UInt64(model);
+		const id = this.decode_UInt64(model);
 
 
 		return new ObjectId(id);
 		return new ObjectId(id);
 	}
 	}
 
 
 	private decode_Plane(model: BufferModel) {
 	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);
 		return new Plane(x, y, z, d);
 	}
 	}
 
 
 	private decode_PoolByteArray(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
 			output.push(model.buffer.readUInt8(model.offset));
 			output.push(model.buffer.readUInt8(model.offset));
 			model.offset++;
 			model.offset++;
@@ -256,8 +271,8 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_PoolColorArray(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
 			output.push(this.decode_Color(model));
 			output.push(this.decode_Color(model));
 		}
 		}
@@ -266,8 +281,8 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_PoolFloatArray(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
 			output.push(this.decode_Float(model));
 			output.push(this.decode_Float(model));
 		}
 		}
@@ -276,8 +291,8 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_PoolIntArray(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
 			output.push(this.decode_Int32(model));
 			output.push(this.decode_Int32(model));
 		}
 		}
@@ -286,8 +301,8 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_PoolStringArray(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
 			output.push(this.decode_String(model));
 			output.push(this.decode_String(model));
 		}
 		}
@@ -296,8 +311,8 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_PoolVector2Array(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
 			output.push(this.decode_Vector2(model));
 			output.push(this.decode_Vector2(model));
 		}
 		}
@@ -306,8 +321,8 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_PoolVector3Array(model: BufferModel) {
 	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++) {
 		for (let i = 0; i < count; i++) {
 			output.push(this.decode_Vector3(model));
 			output.push(this.decode_Vector3(model));
 		}
 		}
@@ -316,10 +331,10 @@ export class VariantDecoder {
 	}
 	}
 
 
 	private decode_Quat(model: BufferModel) {
 	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);
 		return new Quat(x, y, z, w);
 	}
 	}
@@ -335,7 +350,7 @@ export class VariantDecoder {
 			pad = 4 - (len % 4);
 			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;
 		len += pad;
 
 
 		model.offset += len;
 		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) {
 	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);
 		return new Vector2(x, y);
 	}
 	}
 
 
 	private decode_Vector3(model: BufferModel) {
 	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);
 		return new Vector3(x, y, z);
 	}
 	}

+ 11 - 14
src/debugger/variables/variant_encoder.ts → src/debugger/godot3/variables/variant_encoder.ts

@@ -35,8 +35,8 @@ export class VariantEncoder {
 		}
 		}
 
 
 		if (!model) {
 		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 = {
 			model = {
 				buffer: buffer,
 				buffer: buffer,
 				offset: 0,
 				offset: 0,
@@ -48,7 +48,7 @@ export class VariantEncoder {
 		switch (typeof value) {
 		switch (typeof value) {
 			case "number":
 			case "number":
 				{
 				{
-					let is_integer = Number.isInteger(value);
+					const is_integer = Number.isInteger(value);
 					if (is_integer) {
 					if (is_integer) {
 						this.encode_UInt32(GDScriptTypes.INT, model);
 						this.encode_UInt32(GDScriptTypes.INT, model);
 						this.encode_UInt32(value, model);
 						this.encode_UInt32(value, model);
@@ -123,7 +123,7 @@ export class VariantEncoder {
 	}
 	}
 
 
 	private encode_Array(arr: any[], model: BufferModel) {
 	private encode_Array(arr: any[], model: BufferModel) {
-		let size = arr.length;
+		const size = arr.length;
 		this.encode_UInt32(size, model);
 		this.encode_UInt32(size, model);
 		arr.forEach((e) => {
 		arr.forEach((e) => {
 			this.encode_variant(e, model);
 			this.encode_variant(e, model);
@@ -148,11 +148,11 @@ export class VariantEncoder {
 	}
 	}
 
 
 	private encode_Dictionary(dict: Map<any, any>, model: BufferModel) {
 	private encode_Dictionary(dict: Map<any, any>, model: BufferModel) {
-		let size = dict.size;
+		const size = dict.size;
 		this.encode_UInt32(size, model);
 		this.encode_UInt32(size, model);
-		let keys = Array.from(dict.keys());
+		const keys = Array.from(dict.keys());
 		keys.forEach((key) => {
 		keys.forEach((key) => {
-			let value = dict.get(key);
+			const value = dict.get(key);
 			this.encode_variant(key, model);
 			this.encode_variant(key, model);
 			this.encode_variant(value, model);
 			this.encode_variant(value, model);
 		});
 		});
@@ -217,11 +217,8 @@ export class VariantEncoder {
 	}
 	}
 
 
 	private encode_UInt64(value: bigint, model: BufferModel) {
 	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) {
 	private encode_Vector2(value: Vector2, model: BufferModel) {
@@ -241,9 +238,9 @@ export class VariantEncoder {
 
 
 	private size_Dictionary(dict: Map<any, any>): number {
 	private size_Dictionary(dict: Map<any, any>): number {
 		let size = this.size_UInt32();
 		let size = this.size_UInt32();
-		let keys = Array.from(dict.keys());
+		const keys = Array.from(dict.keys());
 		keys.forEach((key) => {
 		keys.forEach((key) => {
-			let value = dict.get(key);
+			const value = dict.get(key);
 			size += this.size_variant(key);
 			size += this.size_variant(key);
 			size += this.size_variant(value);
 			size += this.size_variant(value);
 		});
 		});

+ 1 - 1
src/debugger/variables/variants.ts → src/debugger/godot3/variables/variants.ts

@@ -1,4 +1,4 @@
-import { GodotVariable } from "../debug_runtime";
+import { GodotVariable } from "../../debug_runtime";
 
 
 export enum GDScriptTypes {
 export enum GDScriptTypes {
 	NIL,
 	NIL,

+ 550 - 0
src/debugger/godot4/debug_session.ts

@@ -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 - 0
src/debugger/godot4/helpers.ts

@@ -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 - 0
src/debugger/godot4/server_controller.ts

@@ -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 - 0
src/debugger/godot4/variables/variant_decoder.ts

@@ -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 - 0
src/debugger/godot4/variables/variant_encoder.ts

@@ -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 - 0
src/debugger/godot4/variables/variants.ts

@@ -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";
+	}
+}

+ 11 - 12
src/debugger/scene_tree/inspector_provider.ts → src/debugger/inspector_provider.ts

@@ -6,8 +6,7 @@ import {
 	TreeItem,
 	TreeItem,
 	TreeItemCollapsibleState,
 	TreeItemCollapsibleState,
 } from "vscode";
 } 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> {
 export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
 	private _on_did_change_tree_data: EventEmitter<
 	private _on_did_change_tree_data: EventEmitter<
@@ -63,10 +62,10 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
 		property: RemoteProperty,
 		property: RemoteProperty,
 		new_parsed_value: any
 		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)) {
 		if (Array.isArray(value)) {
-			let idx = parseInt(property.label);
+			const idx = parseInt(property.label);
 			if (idx < value.length) {
 			if (idx < value.length) {
 				value[idx] = new_parsed_value;
 				value[idx] = new_parsed_value;
 			}
 			}
@@ -98,7 +97,7 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
 	}
 	}
 
 
 	private parse_variable(va: GodotVariable, object_id?: number) {
 	private parse_variable(va: GodotVariable, object_id?: number) {
-		let value = va.value;
+		const value = va.value;
 		let rendered_value = "";
 		let rendered_value = "";
 
 
 		if (typeof value === "number") {
 		if (typeof value === "number") {
@@ -132,31 +131,31 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
 		let child_props: RemoteProperty[] = [];
 		let child_props: RemoteProperty[] = [];
 
 
 		if (value) {
 		if (value) {
-			let sub_variables =
+			const sub_variables =
 				typeof value["sub_values"] === "function" &&
 				typeof value["sub_values"] === "function" &&
 				value instanceof ObjectId === false
 				value instanceof ObjectId === false
 					? value.sub_values()
 					? value.sub_values()
 					: Array.isArray(value)
 					: Array.isArray(value)
 					? value.map((va, i) => {
 					? value.map((va, i) => {
 							return { name: `${i}`, value: va };
 							return { name: `${i}`, value: va };
-					  })
+					})
 					: value instanceof Map
 					: value instanceof Map
 					? Array.from(value.keys()).map((va) => {
 					? Array.from(value.keys()).map((va) => {
-							let name =
+							const name =
 								typeof va["rendered_value"] === "function"
 								typeof va["rendered_value"] === "function"
 									? va.rendered_value()
 									? va.rendered_value()
 									: `${va}`;
 									: `${va}`;
-							let map_value = value.get(va);
+							const map_value = value.get(va);
 
 
 							return { name: name, value: map_value };
 							return { name: name, value: map_value };
-					  })
+					})
 					: [];
 					: [];
 			child_props = sub_variables?.map((va) => {
 			child_props = sub_variables?.map((va) => {
 				return this.parse_variable(va, object_id);
 				return this.parse_variable(va, object_id);
 			});
 			});
 		}
 		}
 
 
-		let out_prop = new RemoteProperty(
+		const out_prop = new RemoteProperty(
 			va.name,
 			va.name,
 			value,
 			value,
 			object_id,
 			object_id,

+ 0 - 263
src/debugger/mediator.ts

@@ -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);
-	}
-}

+ 20 - 61
src/debugger/scene_tree/scene_tree_provider.ts → src/debugger/scene_tree_provider.ts

@@ -7,7 +7,6 @@ import {
 	TreeItemCollapsibleState,
 	TreeItemCollapsibleState,
 } from "vscode";
 } from "vscode";
 import path = require("path");
 import path = require("path");
-import fs = require("fs");
 
 
 export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
 export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
 	private _on_did_change_tree_data: EventEmitter<
 	private _on_did_change_tree_data: EventEmitter<
@@ -18,7 +17,7 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
 	public readonly onDidChangeTreeData: Event<SceneNode> | undefined = this
 	public readonly onDidChangeTreeData: Event<SceneNode> | undefined = this
 		._on_did_change_tree_data.event;
 		._on_did_change_tree_data.event;
 
 
-	constructor() {}
+	constructor() { }
 
 
 	public fill_tree(tree: SceneNode) {
 	public fill_tree(tree: SceneNode) {
 		this.tree = tree;
 		this.tree = tree;
@@ -38,9 +37,8 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
 	}
 	}
 
 
 	public getTreeItem(element: SceneNode): TreeItem | Thenable<TreeItem> {
 	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,
 			element.label,
 			has_children
 			has_children
 				? element === this.tree
 				? element === this.tree
@@ -51,77 +49,38 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
 
 
 		tree_item.description = element.class_name;
 		tree_item.description = element.class_name;
 		tree_item.iconPath = element.iconPath;
 		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;
 		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 {
 export class SceneNode extends TreeItem {
 	constructor(
 	constructor(
 		public label: string,
 		public label: string,
 		public class_name: string,
 		public class_name: string,
 		public object_id: number,
 		public object_id: number,
 		public children: SceneNode[],
 		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 = {
 		this.iconPath = {
-			light: light,
-			dark: dark,
+			light: path.join(iconDir, "light", iconName),
+			dark: path.join(iconDir, "dark", iconName),
 		};
 		};
 	}
 	}
 }
 }

+ 0 - 314
src/debugger/server_controller.ts

@@ -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;
-	}
-}

+ 39 - 125
src/extension.ts

@@ -1,24 +1,27 @@
-import * as fs from "fs";
 import * as path from "path";
 import * as path from "path";
 import * as vscode from "vscode";
 import * as vscode from "vscode";
 import { attemptSettingsUpdate } from "./settings_updater";
 import { attemptSettingsUpdate } from "./settings_updater";
-import { register_debugger } from "./debugger/debugger_context";
 import { GDDocumentLinkProvider } from "./document_link_provider";
 import { GDDocumentLinkProvider } from "./document_link_provider";
 import { ClientConnectionManager } from "./lsp/ClientConnectionManager";
 import { ClientConnectionManager } from "./lsp/ClientConnectionManager";
 import { ScenePreviewProvider } from "./scene_preview_provider";
 import { ScenePreviewProvider } from "./scene_preview_provider";
+import { GodotDebugger } from "./debugger/debugger";
+import { exec, execSync } from "child_process";
 import {
 import {
 	get_configuration,
 	get_configuration,
-	set_configuration,
 	find_file,
 	find_file,
 	find_project_file,
 	find_project_file,
-	register_command
+	register_command,
+	get_project_version,
+	set_context,
+	projectDir,
+	projectVersion,
 } from "./utils";
 } from "./utils";
-
-const TOOL_NAME = "GodotTools";
+import { prompt_for_godot_executable } from "./utils/prompts";
 
 
 let lspClientManager: ClientConnectionManager = null;
 let lspClientManager: ClientConnectionManager = null;
 let linkProvider: GDDocumentLinkProvider = null;
 let linkProvider: GDDocumentLinkProvider = null;
 let scenePreviewManager: ScenePreviewProvider = null;
 let scenePreviewManager: ScenePreviewProvider = null;
+let godotDebugger: GodotDebugger = null;
 
 
 export function activate(context: vscode.ExtensionContext) {
 export function activate(context: vscode.ExtensionContext) {
 	attemptSettingsUpdate(context);
 	attemptSettingsUpdate(context);
@@ -26,24 +29,19 @@ export function activate(context: vscode.ExtensionContext) {
 	lspClientManager = new ClientConnectionManager(context);
 	lspClientManager = new ClientConnectionManager(context);
 	linkProvider = new GDDocumentLinkProvider(context);
 	linkProvider = new GDDocumentLinkProvider(context);
 	scenePreviewManager = new ScenePreviewProvider();
 	scenePreviewManager = new ScenePreviewProvider();
-
-	register_debugger(context);
+	godotDebugger = new GodotDebugger(context);
 
 
 	context.subscriptions.push(
 	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("copyResourcePath", copy_resource_path),
 		register_command("openTypeDocumentation", open_type_documentation),
 		register_command("openTypeDocumentation", open_type_documentation),
 		register_command("switchSceneScript", switch_scene_script),
 		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> {
 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 = '';
-
-		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());
-			}
+function open_workspace_with_editor() {
+	const settingName = `editorPath.godot${projectVersion[0]}`;
+	const godotPath = get_configuration(settingName);
+
+	try {
+		const output = execSync(`${godotPath} --version`).toString().trim();
+		const pattern = /([34])\.([0-9]+)\.(?:[0-9]+\.)?\w+.\w+.[0-9a-f]{9}/;
+		const match = output.match(pattern);
+		if (!match) {
+			const message = `Cannot launch 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;
 		}
 		}
-	});
-}
+	} catch {
+		const message = `Cannot launch Godot editor: ${settingName} of ${godotPath} is not a valid Godot executable`;
+		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);
-		}
-	});
+	exec(`${godotPath} --path "${projectDir}" -e`);
 }
 }

+ 57 - 71
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 {
 export enum LOG_LEVEL {
 	SILENT,
 	SILENT,
@@ -58,9 +15,9 @@ const LOG_LEVEL_NAMES = [
 	"WARN ",
 	"WARN ",
 	"INFO ",
 	"INFO ",
 	"DEBUG",
 	"DEBUG",
-]
+];
 
 
-const RESET = "\u001b[0m"
+const RESET = "\u001b[0m";
 
 
 const LOG_COLORS = [
 const LOG_COLORS = [
 	RESET, // SILENT, normal
 	RESET, // SILENT, normal
@@ -68,39 +25,67 @@ const LOG_COLORS = [
 	"\u001b[1;33m", // WARNING, yellow
 	"\u001b[1;33m", // WARNING, yellow
 	"\u001b[1;36m", // INFO, cyan
 	"\u001b[1;36m", // INFO, cyan
 	"\u001b[1;32m", // DEBUG, green
 	"\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_tag: boolean = true;
 	private show_time: boolean;
 	private show_time: boolean;
-	private show_label: boolean;
 	private show_level: boolean = false;
 	private show_level: boolean = false;
+	private output?: LogOutputChannel;
 
 
 	constructor(
 	constructor(
 		private tag: string,
 		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_time = time;
-		this.show_label = label;
+		if (output) {
+			this.output = window.createOutputChannel(output, { log: true });
+		}
 	}
 	}
 
 
 	private log(level: LOG_LEVEL, ...messages) {
 	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) {
 	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;
+}

+ 24 - 58
src/lsp/ClientConnectionManager.ts

@@ -10,9 +10,10 @@ import {
 	register_command,
 	register_command,
 	set_configuration,
 	set_configuration,
 } from "../utils";
 } from "../utils";
+import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
 import { createLogger } from "../logger";
 import { createLogger } from "../logger";
 import { execSync } from "child_process";
 import { execSync } from "child_process";
-import { subProcess, killSubProcesses } from '../utils/subspawn';
+import { subProcess, killSubProcesses } from "../utils/subspawn";
 
 
 const log = createLogger("lsp.manager");
 const log = createLogger("lsp.manager");
 
 
@@ -83,7 +84,7 @@ export class ClientConnectionManager {
 	}
 	}
 
 
 	private stop_language_server() {
 	private stop_language_server() {
-		killSubProcesses('LSP');
+		killSubProcesses("LSP");
 	}
 	}
 
 
 	private async start_language_server() {
 	private async start_language_server() {
@@ -98,11 +99,11 @@ export class ClientConnectionManager {
 
 
 		const projectVersion = await get_project_version();
 		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 settingName = `editorPath.godot${projectVersion[0]}`;
 		const godotPath = get_configuration(settingName);
 		const godotPath = get_configuration(settingName);
@@ -113,21 +114,13 @@ export class ClientConnectionManager {
 			const match = output.match(pattern);
 			const match = output.match(pattern);
 			if (!match) {
 			if (!match) {
 				const message = `Cannot launch headless LSP: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
 				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;
 				return;
 			}
 			}
 			this.connectedVersion = output;
 			this.connectedVersion = output;
 			if (match[1] !== projectVersion[0]) {
 			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]}`;
 				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;
 				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]}.`;
 				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 => {
 				vscode.window.showErrorMessage(message, "Select Godot executable", "Disable Headless LSP", "Ignore").then(item => {
 					if (item == "Select Godot executable") {
 					if (item == "Select Godot executable") {
-						this.select_godot_executable(settingName);
+						select_godot_executable(settingName);
 					} else if (item == "Disable Headless LSP") {
 					} else if (item == "Disable Headless LSP") {
 						set_configuration("lsp.headless", false);
 						set_configuration("lsp.headless", false);
-						this.prompt_for_reload();
+						prompt_for_reload();
 					}
 					}
 				});
 				});
 				return;
 				return;
 			}
 			}
 		} catch (e) {
 		} catch (e) {
 			const message = `Cannot launch headless LSP: ${settingName} of ${godotPath} is not a valid Godot executable`;
 			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;
 			return;
 		}
 		}
 
 
@@ -159,10 +148,10 @@ export class ClientConnectionManager {
 
 
 		const headlessFlags = "--headless --no-window";
 		const headlessFlags = "--headless --no-window";
 		const command = `${godotPath} --path "${projectDir}" --editor ${headlessFlags} --lsp-port ${this.client.port}`;
 		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");
 		const lspStdout = createLogger("lsp.stdout");
-		lspProcess.stdout.on('data', (data) => {
+		lspProcess.stdout.on("data", (data) => {
 			const out = data.toString().trim();
 			const out = data.toString().trim();
 			if (out) {
 			if (out) {
 				lspStdout.debug(out);
 				lspStdout.debug(out);
@@ -170,41 +159,18 @@ export class ClientConnectionManager {
 		});
 		});
 
 
 		// const lspStderr = createLogger("lsp.stderr");
 		// const lspStderr = createLogger("lsp.stderr");
-		lspProcess.stderr.on('data', (data) => {
+		lspProcess.stderr.on("data", (data) => {
 			// const out = data.toString().trim();
 			// const out = data.toString().trim();
 			// if (out) {
 			// if (out) {
 			// 	lspStderr.debug(out);
 			// 	lspStderr.debug(out);
 			// }
 			// }
 		});
 		});
 
 
-		lspProcess.on('close', (code) => {
+		lspProcess.on("close", (code) => {
 			log.info(`LSP process exited with code ${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() {
 	private get_lsp_connection_string() {
 		let host = get_configuration("lsp.serverHost");
 		let host = get_configuration("lsp.serverHost");
 		let port = get_configuration("lsp.serverPort");
 		let port = get_configuration("lsp.serverPort");
@@ -250,13 +216,13 @@ export class ClientConnectionManager {
 
 
 	private update_status_widget() {
 	private update_status_widget() {
 		const lspTarget = this.get_lsp_connection_string();
 		const lspTarget = this.get_lsp_connection_string();
-		const maxAttempts = get_configuration("lsp.autoReconnect.attempts")
+		const maxAttempts = get_configuration("lsp.autoReconnect.attempts");
 		let text = "";
 		let text = "";
 		let tooltip = "";
 		let tooltip = "";
 		switch (this.status) {
 		switch (this.status) {
 			case ManagerStatus.INITIALIZING:
 			case ManagerStatus.INITIALIZING:
-				text = `$(sync~spin) Initializing`;
-				tooltip = `Initializing extension...`;
+				text = "$(sync~spin) Initializing";
+				tooltip = "Initializing extension...";
 				break;
 				break;
 			case ManagerStatus.INITIALIZING_LSP:
 			case ManagerStatus.INITIALIZING_LSP:
 				text = `$(sync~spin) Initializing LSP ${this.reconnectionAttempts}/${maxAttempts}`;
 				text = `$(sync~spin) Initializing LSP ${this.reconnectionAttempts}/${maxAttempts}`;
@@ -266,19 +232,19 @@ export class ClientConnectionManager {
 				}
 				}
 				break;
 				break;
 			case ManagerStatus.PENDING:
 			case ManagerStatus.PENDING:
-				text = `$(sync~spin) Connecting`;
+				text = "$(sync~spin) Connecting";
 				tooltip = `Connecting to the GDScript language server at ${lspTarget}`;
 				tooltip = `Connecting to the GDScript language server at ${lspTarget}`;
 				break;
 				break;
 			case ManagerStatus.CONNECTED:
 			case ManagerStatus.CONNECTED:
-				text = `$(check) Connected`;
+				text = "$(check) Connected";
 				tooltip = `Connected to the GDScript language server.\n${lspTarget}`;
 				tooltip = `Connected to the GDScript language server.\n${lspTarget}`;
 				if (this.connectedVersion) {
 				if (this.connectedVersion) {
 					tooltip += `\n${this.connectedVersion}`;
 					tooltip += `\n${this.connectedVersion}`;
 				}
 				}
 				break;
 				break;
 			case ManagerStatus.DISCONNECTED:
 			case ManagerStatus.DISCONNECTED:
-				text = `$(x) Disconnected`;
-				tooltip = `Disconnected from the GDScript language server.`;
+				text = "$(x) Disconnected";
+				tooltip = "Disconnected from the GDScript language server.";
 				break;
 				break;
 			case ManagerStatus.RETRYING:
 			case ManagerStatus.RETRYING:
 				text = `$(sync~spin) Connecting ${this.reconnectionAttempts}/${maxAttempts}`;
 				text = `$(sync~spin) Connecting ${this.reconnectionAttempts}/${maxAttempts}`;

+ 10 - 7
src/lsp/GDScriptLanguageClient.ts

@@ -1,12 +1,12 @@
 import { EventEmitter } from "events";
 import { EventEmitter } from "events";
 import * as vscode from 'vscode';
 import * as vscode from 'vscode';
 import { LanguageClient, RequestMessage, ResponseMessage, integer } from "vscode-languageclient/node";
 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 { get_configuration, set_context } from "../utils";
 import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
 import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
 import { NativeDocumentManager } from './NativeDocumentManager';
 import { NativeDocumentManager } from './NativeDocumentManager';
 
 
-const log = createLogger("lsp.client");
+const log = createLogger("lsp.client", {level: LOG_LEVEL.SILENT});
 
 
 export enum ClientStatus {
 export enum ClientStatus {
 	PENDING,
 	PENDING,
@@ -19,7 +19,7 @@ export enum TargetLSP {
 	EDITOR,
 	EDITOR,
 }
 }
 
 
-const CUSTOM_MESSAGE = "gdscrip_client/";
+const CUSTOM_MESSAGE = "gdscript_client/";
 
 
 export default class GDScriptLanguageClient extends LanguageClient {
 export default class GDScriptLanguageClient extends LanguageClient {
 	public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") == "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
 	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
 			// this is a dirty hack to fix language server sending us prerendered
 			// markdown but not correctly stripping leading #'s, leading to 
 			// markdown but not correctly stripping leading #'s, leading to 
 			// docstrings being displayed as titles
 			// 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);
 		this.message_handler.on_message(message);
@@ -164,8 +164,11 @@ export default class GDScriptLanguageClient extends LanguageClient {
 		this.lastSymbolHovered = "";
 		this.lastSymbolHovered = "";
 		set_context("typeFound", false);
 		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
 		// strip off the value
 		if (decl.includes("=")) {
 		if (decl.includes("=")) {

+ 7 - 2
src/lsp/MessageIO.ts

@@ -86,6 +86,7 @@ export class TCPMessageIO extends MessageIO {
 			socket.on('data', this.on_message.bind(this));
 			socket.on('data', this.on_message.bind(this));
 			socket.on('end', this.on_disconnected.bind(this));
 			socket.on('end', this.on_disconnected.bind(this));
 			socket.on('close', 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.socket = null;
 		this.emit('disconnected');
 		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 partialMessageTimer: NodeJS.Timeout | undefined;
 	private _partialMessageTimeout: number;
 	private _partialMessageTimeout: number;
 
 
-	public constructor(io: MessageIO, encoding: string = 'utf8') {
+	public constructor(io: MessageIO, encoding: BufferEncoding = 'utf8') {
 		super();
 		super();
 		this.io = io;
 		this.io = io;
 		this.io.reader = this;
 		this.io.reader = this;
@@ -207,7 +212,7 @@ export class MessageIOWriter extends AbstractMessageWriter implements MessageWri
 	private encoding: BufferEncoding;
 	private encoding: BufferEncoding;
 	private errorCount: number;
 	private errorCount: number;
 
 
-	public constructor(io: MessageIO, encoding: string = 'utf8') {
+	public constructor(io: MessageIO, encoding: BufferEncoding = 'utf8') {
 		super();
 		super();
 		this.io = io;
 		this.io = io;
 		this.io.writer = this;
 		this.io.writer = this;

+ 2 - 2
src/lsp/NativeDocumentManager.ts

@@ -4,7 +4,7 @@ import { EventEmitter } from "events";
 import { MessageIO } from "./MessageIO";
 import { MessageIO } from "./MessageIO";
 import { NotificationMessage } from "vscode-jsonrpc";
 import { NotificationMessage } from "vscode-jsonrpc";
 import * as Prism from "prismjs";
 import * as Prism from "prismjs";
-import * as marked from "marked";
+import { marked } from "marked";
 import { get_configuration, register_command } from "../utils";
 import { get_configuration, register_command } from "../utils";
 import {
 import {
 	Methods,
 	Methods,
@@ -127,7 +127,7 @@ export class NativeDocumentManager extends EventEmitter {
 	 * configuration and previously opened native symbols.
 	 * configuration and previously opened native symbols.
 	 */
 	 */
 	private get_new_native_symbol_column(): vscode.ViewColumn {
 	private get_new_native_symbol_column(): vscode.ViewColumn {
-		const config_placement = get_configuration("nativeSymbolPlacement");
+		const config_placement = get_configuration("documentation.newTabPlacement");
 
 
 		if (config_placement == "active") {
 		if (config_placement == "active") {
 			return vscode.ViewColumn.Active;
 			return vscode.ViewColumn.Active;

+ 2 - 2
src/scene_preview_provider.ts

@@ -109,12 +109,12 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode> {
 
 
 	private pin_preview() {
 	private pin_preview() {
 		this.scenePreviewPinned = true;
 		this.scenePreviewPinned = true;
-		set_context("godotTools.context.scenePreviewPinned", true);
+		set_context("scenePreview.pinned", true);
 	}
 	}
 
 
 	private unpin_preview() {
 	private unpin_preview() {
 		this.scenePreviewPinned = false;
 		this.scenePreviewPinned = false;
-		set_context("godotTools.context.scenePreviewPinned", false);
+		set_context("scenePreview.pinned", false);
 		this.refresh();
 		this.refresh();
 	}
 	}
 
 

+ 4 - 6
src/settings_updater.ts

@@ -1,17 +1,16 @@
 import * as vscode from "vscode";
 import * as vscode from "vscode";
 
 
 const OLD_SETTINGS_CONVERSIONS = [
 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_protocol", "godotTools.lsp.serverProtocol"],
 	["godot_tools.gdscript_lsp_server_host", "godotTools.lsp.serverHost"],
 	["godot_tools.gdscript_lsp_server_host", "godotTools.lsp.serverHost"],
 	["godot_tools.gdscript_lsp_server_port", "godotTools.lsp.serverPort"],
 	["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_automatically", "godotTools.lsp.autoReconnect.enabled"],
 	["godot_tools.reconnect_cooldown", "godotTools.lsp.autoReconnect.cooldown"],
 	["godot_tools.reconnect_cooldown", "godotTools.lsp.autoReconnect.cooldown"],
 	["godot_tools.reconnect_attempts", "godotTools.lsp.autoReconnect.attempts"],
 	["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"]
 	["godot_tools.scenePreview.previewRelatedScenes", "godotTools.scenePreview.previewRelatedScenes"]
 ];
 ];
 
 
@@ -23,7 +22,6 @@ export function updateOldStyleSettings() {
 		if (value === undefined) {
 		if (value === undefined) {
 			continue;
 			continue;
 		}
 		}
-		configuration.update(old_style_key, undefined, true);
 		configuration.update(new_style_key, value, true);
 		configuration.update(new_style_key, value, true);
 		settings_changed = true;
 		settings_changed = true;
 	}
 	}

+ 31 - 23
src/utils.ts

@@ -5,12 +5,12 @@ import { AddressInfo, createServer } from "net";
 
 
 const EXTENSION_PREFIX = "godotTools";
 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) {
 export function set_configuration(name: string, value: any) {
@@ -40,16 +40,19 @@ export function get_word_under_cursor(): string {
 	return symbolName;
 	return symbolName;
 }
 }
 
 
+export let projectVersion = undefined;
+
 export async function get_project_version(): Promise<string | undefined> {
 export async function get_project_version(): Promise<string | undefined> {
-	const project_dir = await get_project_dir();
+	const dir = await get_project_dir();
 
 
-	if (!project_dir) {
+	if (!dir) {
+		projectVersion = undefined;
 		return 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 text = document.getText();
 
 
 	const match = text.match(/config\/features=PackedStringArray\((.*)\)/);
 	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 line = match[0];
 		const version = line.match(/\"(4.[0-9]+)\"/);
 		const version = line.match(/\"(4.[0-9]+)\"/);
 		if (version) {
 		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() {
 export async function get_project_dir() {
-	let project_dir = undefined;
-	let project_file = '';
+	let dir = undefined;
+	let projectFile = "";
 	if (vscode.workspace.workspaceFolders != undefined) {
 	if (vscode.workspace.workspaceFolders != undefined) {
 		const files = await vscode.workspace.findFiles("**/project.godot");
 		const files = await vscode.workspace.findFiles("**/project.godot");
 		if (files) {
 		if (files) {
-			project_file = files[0].fsPath;
-			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) {
 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) {
 	if (start == folder) {
 		return null;
 		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 {
 	} else {
 		if (depth === 0) {
 		if (depth === 0) {
 			return null;
 			return null;
@@ -116,8 +124,8 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
 	if (!files) {
 	if (!files) {
 		return null;
 		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> {
 export async function get_free_port(): Promise<number> {

+ 33 - 0
src/utils/prompts.ts

@@ -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);
+		}
+	});
+}

+ 17 - 8
src/utils/subspawn.ts

@@ -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.
 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 {
 interface DictionaryOfStringChildProcessArray {
 	[key: string]: ChildProcess[];
 	[key: string]: ChildProcess[];
@@ -20,17 +23,23 @@ export function killSubProcesses(owner: string) {
 	children[owner].forEach((c) => {
 	children[owner].forEach((c) => {
 		try {
 		try {
 			if (c.pid) {
 			if (c.pid) {
-				if (process.platform === 'win32') {
+				if (process.platform === "win32") {
 					execSync(`taskkill /pid ${c.pid} /T /F`);
 					execSync(`taskkill /pid ${c.pid} /T /F`);
+				} else if (process.platform === "darwin") {
+					execSync(`kill -9 ${c.pid}`);
 				} else {
 				} 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));
 	Object.keys(children).forEach((owner) => killSubProcesses(owner));
 });
 });
 
 
@@ -38,9 +47,9 @@ function gracefulExitHandler() {
 	process.exit();
 	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) {
 export function subProcess(owner: string, command: string, options?: SpawnOptions) {
 	const childProcess = spawn(command, options);
 	const childProcess = spawn(command, options);

+ 0 - 0
syntaxes/examples/project.godot


+ 2 - 2
tools/generate_icons.ts

@@ -86,7 +86,7 @@ function get_class_list(modules) {
 }
 }
 
 
 function discover_modules() {
 function discover_modules() {
-	const modules = []
+	const modules = [];
 
 
 	// a valid module is a subdir of modulesPath, and contains a subdir 'icons'
 	// a valid module is a subdir of modulesPath, and contains a subdir 'icons'
 	fs.readdirSync(modulesPath, {withFileTypes:true}).forEach(mod => {
 	fs.readdirSync(modulesPath, {withFileTypes:true}).forEach(mod => {
@@ -106,7 +106,7 @@ function get_icons() {
 	const modules = discover_modules();
 	const modules = discover_modules();
 	const classes = get_class_list(modules);
 	const classes = get_class_list(modules);
 
 
-	const searchPaths = [iconsPath]
+	const searchPaths = [iconsPath];
 	modules.forEach(mod => {
 	modules.forEach(mod => {
 		searchPaths.push(join(mod, 'icons'));
 		searchPaths.push(join(mod, 'icons'));
 	});
 	});

+ 1 - 1
tsconfig.json

@@ -4,7 +4,7 @@
 		"target": "es2020",
 		"target": "es2020",
 		"outDir": "out",
 		"outDir": "out",
 		"lib": [
 		"lib": [
-			"es2020",
+			"es2022",
 			"dom"
 			"dom"
 		],
 		],
 		"sourceMap": true,
 		"sourceMap": true,

Some files were not shown because too many files changed in this diff