Bläddra i källkod

Implement Godot-in-the-loop test suite and fix debugger errors (#788)

Fixes for get variables issues
1. Same reference but different variable override fix, which resulted in variables being lost
** Now different GodotVariable instances are used for different variables with same reference
** const replacement = {value: rawObject, sub_values: sub_values } as GodotVariable;
2. 'Signal' type handling crash and string representation fix
** value.sub_values()?.map
3. Empty scopes return fix
** if (this.ongoing_inspections.length === 0  && stackVars.remaining == 0)
4. Various splice from findIndex fixes (where findIndex can return -1)
5. Added variables tests
**  updated vscode types to version 1.96 to use `onDidChangeActiveStackItem` for breakpoint hit detection in tests
1 & 3 should fix https://github.com/godotengine/godot-vscode-plugin/issues/779
MichaelXt 5 månader sedan
förälder
incheckning
2490d0cdad
30 ändrade filer med 856 tillägg och 162 borttagningar
  1. 27 0
      .github/workflows/ci.yml
  2. 2 0
      .gitignore
  3. 2 0
      .vscode-test.js
  4. 5 0
      .vscode/extensions.json
  5. 4 3
      .vscode/launch.json
  6. 114 97
      package-lock.json
  7. 6 4
      package.json
  8. 46 16
      src/debugger/godot4/debug_session.ts
  9. 359 0
      src/debugger/godot4/debugger_variables.test.ts
  10. 34 28
      src/debugger/godot4/helpers.ts
  11. 10 8
      src/debugger/godot4/server_controller.ts
  12. 1 1
      src/debugger/godot4/variables/variants.ts
  13. 3 2
      src/debugger/scene_tree_provider.ts
  14. 4 0
      src/formatter/formatter.test.ts
  15. 3 2
      src/scene_tools/types.ts
  16. 40 0
      test_projects/test-dap-project-godot4/.vscode/launch.json
  17. 39 0
      test_projects/test-dap-project-godot4/BuiltInTypes.gd
  18. 11 0
      test_projects/test-dap-project-godot4/BuiltInTypes.tscn
  19. 38 0
      test_projects/test-dap-project-godot4/ExtensiveVars.gd
  20. 12 0
      test_projects/test-dap-project-godot4/ExtensiveVars.tscn
  21. 14 0
      test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd
  22. 3 0
      test_projects/test-dap-project-godot4/GlobalScript.gd
  23. 8 0
      test_projects/test-dap-project-godot4/Node1.gd
  24. 9 0
      test_projects/test-dap-project-godot4/NodeVars.gd
  25. 17 0
      test_projects/test-dap-project-godot4/NodeVars.tscn
  26. 8 0
      test_projects/test-dap-project-godot4/ScopeVars.gd
  27. 11 0
      test_projects/test-dap-project-godot4/ScopeVars.tscn
  28. 5 0
      test_projects/test-dap-project-godot4/TestClassA.gd
  29. 19 0
      test_projects/test-dap-project-godot4/project.godot
  30. 2 1
      tsconfig.json

+ 27 - 0
.github/workflows/ci.yml

@@ -5,6 +5,7 @@ jobs:
   test:
   test:
     name: Test
     name: Test
     strategy:
     strategy:
+      fail-fast: false
       matrix:
       matrix:
         os: [macos-latest, ubuntu-latest, windows-latest]
         os: [macos-latest, ubuntu-latest, windows-latest]
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
@@ -17,9 +18,35 @@ jobs:
         with:
         with:
           node-version: 16.x
           node-version: 16.x
 
 
+      - name: Install Godot (Ubuntu)
+        if: matrix.os == 'ubuntu-latest'
+        run: |
+          wget https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_linux.x86_64.zip
+          unzip Godot_v4.3-stable_linux.x86_64.zip
+          sudo mv Godot_v4.3-stable_linux.x86_64 /usr/local/bin/godot
+          chmod +x /usr/local/bin/godot
+
+      - name: Install Godot (macOS)
+        if: matrix.os == 'macos-latest'
+        run: |
+          curl -L -o Godot.zip https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_macos.universal.zip
+          unzip Godot.zip
+          sudo mv Godot.app /Applications/Godot.app
+          sudo ln -s /Applications/Godot.app/Contents/MacOS/Godot /usr/local/bin/godot
+
+      - name: Install Godot (Windows)
+        if: matrix.os == 'windows-latest'
+        run: |
+          Invoke-WebRequest -Uri "https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_win64.exe.zip" -OutFile "Godot.zip"
+          Expand-Archive -Path "Godot.zip" -DestinationPath "C:\Godot43"
+          "C:\Godot43\Godot_v4.3-stable_win64.exe %*" | Out-File -Encoding ascii -FilePath ([Environment]::SystemDirectory+"\godot.cmd")
+
       - name: Install dependencies
       - name: Install dependencies
         run: npm install
         run: npm install
 
 
+      - name: Godot init project
+        run: godot --import test_projects/test-dap-project-godot4/project.godot --headless
+
       - name: Run headless test
       - name: Run headless test
         uses: coactions/setup-xvfb@v1
         uses: coactions/setup-xvfb@v1
         with:
         with:

+ 2 - 0
.gitignore

@@ -4,3 +4,5 @@ node_modules
 .vscode-test
 .vscode-test
 workspace.code-workspace
 workspace.code-workspace
 .history
 .history
+.godot
+*.tmp

+ 2 - 0
.vscode-test.js

@@ -5,5 +5,7 @@ module.exports = defineConfig(
 		// version: '1.84.0',
 		// version: '1.84.0',
 		label: 'unitTests',
 		label: 'unitTests',
 		files: 'out/**/*.test.js',
 		files: 'out/**/*.test.js',
+		launchArgs: ['--disable-extensions'],
+		workspaceFolder: './test_projects/test-dap-project-godot4',
 	}
 	}
 );
 );

+ 5 - 0
.vscode/extensions.json

@@ -0,0 +1,5 @@
+{
+  "recommendations": [
+    "ms-vscode.extension-test-runner"
+  ]
+}

+ 4 - 3
.vscode/launch.json

@@ -5,6 +5,7 @@
 {
 {
 	"version": "0.2.0",
 	"version": "0.2.0",
 	"configurations": [
 	"configurations": [
+		
 		{
 		{
 			"name": "Run Extension",
 			"name": "Run Extension",
 			"type": "extensionHost",
 			"type": "extensionHost",
@@ -22,7 +23,7 @@
 			],
 			],
 			"preLaunchTask": "npm: watch",
 			"preLaunchTask": "npm: watch",
 			"env": {
 			"env": {
-				"VSCODE_DEBUG_MODE": true
+				"VSCODE_DEBUG_MODE": "true"
 			}
 			}
 		},
 		},
 		{
 		{
@@ -33,7 +34,7 @@
 			"args": [
 			"args": [
 				"--profile=temp",
 				"--profile=temp",
 				"--extensionDevelopmentPath=${workspaceFolder}",
 				"--extensionDevelopmentPath=${workspaceFolder}",
-				"${workspaceFolder}/workspace.code-workspace"
+				"${workspaceFolder}/test_projects/test-dap-project-godot4"
 			],
 			],
 			"outFiles": [
 			"outFiles": [
 				"${workspaceFolder}/out/**/*.js"
 				"${workspaceFolder}/out/**/*.js"
@@ -44,7 +45,7 @@
 			],
 			],
 			"preLaunchTask": "npm: watch",
 			"preLaunchTask": "npm: watch",
 			"env": {
 			"env": {
-				"VSCODE_DEBUG_MODE": true
+				"VSCODE_DEBUG_MODE": "true"
 			}
 			}
 		},
 		},
 	]
 	]

+ 114 - 97
package-lock.json

@@ -9,8 +9,8 @@
 			"version": "2.3.0",
 			"version": "2.3.0",
 			"license": "MIT",
 			"license": "MIT",
 			"dependencies": {
 			"dependencies": {
-				"@vscode/debugadapter": "^1.64.0",
-				"@vscode/debugprotocol": "^1.64.0",
+				"@vscode/debugadapter": "^1.68.0",
+				"@vscode/debugprotocol": "^1.68.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",
@@ -25,11 +25,12 @@
 			},
 			},
 			"devDependencies": {
 			"devDependencies": {
 				"@types/chai": "^4.3.11",
 				"@types/chai": "^4.3.11",
+				"@types/chai-subset": "^1.3.5",
 				"@types/marked": "^4.0.8",
 				"@types/marked": "^4.0.8",
 				"@types/mocha": "^10.0.6",
 				"@types/mocha": "^10.0.6",
 				"@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.96.0",
 				"@types/ws": "^8.5.4",
 				"@types/ws": "^8.5.4",
 				"@typescript-eslint/eslint-plugin": "^5.57.1",
 				"@typescript-eslint/eslint-plugin": "^5.57.1",
 				"@typescript-eslint/eslint-plugin-tslint": "^5.57.1",
 				"@typescript-eslint/eslint-plugin-tslint": "^5.57.1",
@@ -38,6 +39,7 @@
 				"@vscode/test-electron": "^2.3.8",
 				"@vscode/test-electron": "^2.3.8",
 				"@vscode/vsce": "^2.29.0",
 				"@vscode/vsce": "^2.29.0",
 				"chai": "^4.3.10",
 				"chai": "^4.3.10",
+				"chai-subset": "^1.6.0",
 				"esbuild": "^0.17.15",
 				"esbuild": "^0.17.15",
 				"eslint": "^8.37.0",
 				"eslint": "^8.37.0",
 				"mocha": "^10.2.0",
 				"mocha": "^10.2.0",
@@ -47,7 +49,7 @@
 				"typescript": "^5.2.2"
 				"typescript": "^5.2.2"
 			},
 			},
 			"engines": {
 			"engines": {
-				"vscode": "^1.80.0"
+				"vscode": "^1.96.0"
 			}
 			}
 		},
 		},
 		"node_modules/@aashutoshrathi/word-wrap": {
 		"node_modules/@aashutoshrathi/word-wrap": {
@@ -1067,6 +1069,15 @@
 			"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
 			"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/@types/chai-subset": {
+			"version": "1.3.5",
+			"resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz",
+			"integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==",
+			"dev": true,
+			"dependencies": {
+				"@types/chai": "*"
+			}
+		},
 		"node_modules/@types/json-schema": {
 		"node_modules/@types/json-schema": {
 			"version": "7.0.13",
 			"version": "7.0.13",
 			"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
 			"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
@@ -1104,9 +1115,9 @@
 			"dev": true
 			"dev": true
 		},
 		},
 		"node_modules/@types/vscode": {
 		"node_modules/@types/vscode": {
-			"version": "1.82.0",
-			"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.82.0.tgz",
-			"integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==",
+			"version": "1.96.0",
+			"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz",
+			"integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==",
 			"dev": true
 			"dev": true
 		},
 		},
 		"node_modules/@types/ws": {
 		"node_modules/@types/ws": {
@@ -1414,20 +1425,20 @@
 			}
 			}
 		},
 		},
 		"node_modules/@vscode/debugadapter": {
 		"node_modules/@vscode/debugadapter": {
-			"version": "1.64.0",
-			"resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.64.0.tgz",
-			"integrity": "sha512-XygE985qmNCzJExDnam4bErK6FG9Ck8S5TRPDNESwkt7i3OXqw5a3vYb7Dteyhz9YMEf7hwhFoT46Mjc45nJUg==",
+			"version": "1.68.0",
+			"resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.68.0.tgz",
+			"integrity": "sha512-D6gk5Fw2y4FV8oYmltoXpj+VAZexxJFopN/mcZ6YcgzQE9dgq2L45Aj3GLxScJOD6GeLILcxJIaA8l3v11esGg==",
 			"dependencies": {
 			"dependencies": {
-				"@vscode/debugprotocol": "1.64.0"
+				"@vscode/debugprotocol": "1.68.0"
 			},
 			},
 			"engines": {
 			"engines": {
 				"node": ">=14"
 				"node": ">=14"
 			}
 			}
 		},
 		},
 		"node_modules/@vscode/debugprotocol": {
 		"node_modules/@vscode/debugprotocol": {
-			"version": "1.64.0",
-			"resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.64.0.tgz",
-			"integrity": "sha512-Zhf3KvB+J04M4HPE2yCvEILGVtPixXUQMLBvx4QcAtjhc5lnwlZbbt80LCsZO2B+2BH8RMgVXk3QQ5DEzEne2Q=="
+			"version": "1.68.0",
+			"resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz",
+			"integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg=="
 		},
 		},
 		"node_modules/@vscode/test-cli": {
 		"node_modules/@vscode/test-cli": {
 			"version": "0.0.4",
 			"version": "0.0.4",
@@ -1892,9 +1903,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/ansi-colors": {
 		"node_modules/ansi-colors": {
-			"version": "4.1.1",
-			"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
-			"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+			"version": "4.1.3",
+			"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+			"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
 			"dev": true,
 			"dev": true,
 			"engines": {
 			"engines": {
 				"node": ">=6"
 				"node": ">=6"
@@ -2192,9 +2203,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/chai": {
 		"node_modules/chai": {
-			"version": "4.3.10",
-			"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
-			"integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
+			"integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
 			"dev": true,
 			"dev": true,
 			"dependencies": {
 			"dependencies": {
 				"assertion-error": "^1.1.0",
 				"assertion-error": "^1.1.0",
@@ -2203,12 +2214,21 @@
 				"get-func-name": "^2.0.2",
 				"get-func-name": "^2.0.2",
 				"loupe": "^2.3.6",
 				"loupe": "^2.3.6",
 				"pathval": "^1.1.1",
 				"pathval": "^1.1.1",
-				"type-detect": "^4.0.8"
+				"type-detect": "^4.1.0"
 			},
 			},
 			"engines": {
 			"engines": {
 				"node": ">=4"
 				"node": ">=4"
 			}
 			}
 		},
 		},
+		"node_modules/chai-subset": {
+			"version": "1.6.0",
+			"resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz",
+			"integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==",
+			"dev": true,
+			"engines": {
+				"node": ">=4"
+			}
+		},
 		"node_modules/chalk": {
 		"node_modules/chalk": {
 			"version": "2.4.2",
 			"version": "2.4.2",
 			"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
 			"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -2498,12 +2518,12 @@
 			}
 			}
 		},
 		},
 		"node_modules/debug": {
 		"node_modules/debug": {
-			"version": "4.3.4",
-			"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
-			"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+			"version": "4.4.0",
+			"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+			"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
 			"dev": true,
 			"dev": true,
 			"dependencies": {
 			"dependencies": {
-				"ms": "2.1.2"
+				"ms": "^2.1.3"
 			},
 			},
 			"engines": {
 			"engines": {
 				"node": ">=6.0"
 				"node": ">=6.0"
@@ -4466,32 +4486,31 @@
 			"optional": true
 			"optional": true
 		},
 		},
 		"node_modules/mocha": {
 		"node_modules/mocha": {
-			"version": "10.2.0",
-			"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
-			"integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
-			"dev": true,
-			"dependencies": {
-				"ansi-colors": "4.1.1",
-				"browser-stdout": "1.3.1",
-				"chokidar": "3.5.3",
-				"debug": "4.3.4",
-				"diff": "5.0.0",
-				"escape-string-regexp": "4.0.0",
-				"find-up": "5.0.0",
-				"glob": "7.2.0",
-				"he": "1.2.0",
-				"js-yaml": "4.1.0",
-				"log-symbols": "4.1.0",
-				"minimatch": "5.0.1",
-				"ms": "2.1.3",
-				"nanoid": "3.3.3",
-				"serialize-javascript": "6.0.0",
-				"strip-json-comments": "3.1.1",
-				"supports-color": "8.1.1",
-				"workerpool": "6.2.1",
-				"yargs": "16.2.0",
-				"yargs-parser": "20.2.4",
-				"yargs-unparser": "2.0.0"
+			"version": "10.8.2",
+			"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
+			"integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==",
+			"dev": true,
+			"dependencies": {
+				"ansi-colors": "^4.1.3",
+				"browser-stdout": "^1.3.1",
+				"chokidar": "^3.5.3",
+				"debug": "^4.3.5",
+				"diff": "^5.2.0",
+				"escape-string-regexp": "^4.0.0",
+				"find-up": "^5.0.0",
+				"glob": "^8.1.0",
+				"he": "^1.2.0",
+				"js-yaml": "^4.1.0",
+				"log-symbols": "^4.1.0",
+				"minimatch": "^5.1.6",
+				"ms": "^2.1.3",
+				"serialize-javascript": "^6.0.2",
+				"strip-json-comments": "^3.1.1",
+				"supports-color": "^8.1.1",
+				"workerpool": "^6.5.1",
+				"yargs": "^16.2.0",
+				"yargs-parser": "^20.2.9",
+				"yargs-unparser": "^2.0.0"
 			},
 			},
 			"bin": {
 			"bin": {
 				"_mocha": "bin/_mocha",
 				"_mocha": "bin/_mocha",
@@ -4499,10 +4518,6 @@
 			},
 			},
 			"engines": {
 			"engines": {
 				"node": ">= 14.0.0"
 				"node": ">= 14.0.0"
-			},
-			"funding": {
-				"type": "opencollective",
-				"url": "https://opencollective.com/mochajs"
 			}
 			}
 		},
 		},
 		"node_modules/mocha/node_modules/argparse": {
 		"node_modules/mocha/node_modules/argparse": {
@@ -4521,9 +4536,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/mocha/node_modules/diff": {
 		"node_modules/mocha/node_modules/diff": {
-			"version": "5.0.0",
-			"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
-			"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+			"version": "5.2.0",
+			"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+			"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
 			"dev": true,
 			"dev": true,
 			"engines": {
 			"engines": {
 				"node": ">=0.3.1"
 				"node": ">=0.3.1"
@@ -4541,6 +4556,26 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 			}
 		},
 		},
+		"node_modules/mocha/node_modules/glob": {
+			"version": "8.1.0",
+			"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+			"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+			"deprecated": "Glob versions prior to v9 are no longer supported",
+			"dev": true,
+			"dependencies": {
+				"fs.realpath": "^1.0.0",
+				"inflight": "^1.0.4",
+				"inherits": "2",
+				"minimatch": "^5.0.1",
+				"once": "^1.3.0"
+			},
+			"engines": {
+				"node": ">=12"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/isaacs"
+			}
+		},
 		"node_modules/mocha/node_modules/has-flag": {
 		"node_modules/mocha/node_modules/has-flag": {
 			"version": "4.0.0",
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4563,9 +4598,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/mocha/node_modules/minimatch": {
 		"node_modules/mocha/node_modules/minimatch": {
-			"version": "5.0.1",
-			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
-			"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+			"version": "5.1.6",
+			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+			"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
 			"dev": true,
 			"dev": true,
 			"dependencies": {
 			"dependencies": {
 				"brace-expansion": "^2.0.1"
 				"brace-expansion": "^2.0.1"
@@ -4574,12 +4609,6 @@
 				"node": ">=10"
 				"node": ">=10"
 			}
 			}
 		},
 		},
-		"node_modules/mocha/node_modules/ms": {
-			"version": "2.1.3",
-			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-			"dev": true
-		},
 		"node_modules/mocha/node_modules/strip-json-comments": {
 		"node_modules/mocha/node_modules/strip-json-comments": {
 			"version": "3.1.1",
 			"version": "3.1.1",
 			"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
 			"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -4608,9 +4637,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/ms": {
 		"node_modules/ms": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-			"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
 			"dev": true
 			"dev": true
 		},
 		},
 		"node_modules/mute-stream": {
 		"node_modules/mute-stream": {
@@ -4619,18 +4648,6 @@
 			"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
 			"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
 			"dev": true
 			"dev": true
 		},
 		},
-		"node_modules/nanoid": {
-			"version": "3.3.3",
-			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
-			"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
-			"dev": true,
-			"bin": {
-				"nanoid": "bin/nanoid.cjs"
-			},
-			"engines": {
-				"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-			}
-		},
 		"node_modules/napi-build-utils": {
 		"node_modules/napi-build-utils": {
 			"version": "1.0.2",
 			"version": "1.0.2",
 			"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
 			"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
@@ -5247,9 +5264,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/serialize-javascript": {
 		"node_modules/serialize-javascript": {
-			"version": "6.0.0",
-			"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
-			"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+			"version": "6.0.2",
+			"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+			"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
 			"dev": true,
 			"dev": true,
 			"dependencies": {
 			"dependencies": {
 				"randombytes": "^2.1.0"
 				"randombytes": "^2.1.0"
@@ -5635,9 +5652,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/ts-node": {
 		"node_modules/ts-node": {
-			"version": "10.9.1",
-			"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
-			"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+			"version": "10.9.2",
+			"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+			"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
 			"dev": true,
 			"dev": true,
 			"dependencies": {
 			"dependencies": {
 				"@cspotcode/source-map-support": "^0.8.0",
 				"@cspotcode/source-map-support": "^0.8.0",
@@ -5774,9 +5791,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/type-detect": {
 		"node_modules/type-detect": {
-			"version": "4.0.8",
-			"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
-			"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+			"integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
 			"dev": true,
 			"dev": true,
 			"engines": {
 			"engines": {
 				"node": ">=4"
 				"node": ">=4"
@@ -5951,9 +5968,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/workerpool": {
 		"node_modules/workerpool": {
-			"version": "6.2.1",
-			"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
-			"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+			"version": "6.5.1",
+			"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
+			"integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
 			"dev": true
 			"dev": true
 		},
 		},
 		"node_modules/wrap-ansi": {
 		"node_modules/wrap-ansi": {
@@ -6222,9 +6239,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/yargs-parser": {
 		"node_modules/yargs-parser": {
-			"version": "20.2.4",
-			"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
-			"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+			"version": "20.2.9",
+			"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+			"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
 			"dev": true,
 			"dev": true,
 			"engines": {
 			"engines": {
 				"node": ">=10"
 				"node": ">=10"

+ 6 - 4
package.json

@@ -15,7 +15,7 @@
 	"author": "The Godot Engine community",
 	"author": "The Godot Engine community",
 	"publisher": "geequlim",
 	"publisher": "geequlim",
 	"engines": {
 	"engines": {
-		"vscode": "^1.80.0"
+		"vscode": "^1.96.0"
 	},
 	},
 	"categories": [
 	"categories": [
 		"Programming Languages",
 		"Programming Languages",
@@ -866,11 +866,12 @@
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
 		"@types/chai": "^4.3.11",
 		"@types/chai": "^4.3.11",
+		"@types/chai-subset": "^1.3.5",
 		"@types/marked": "^4.0.8",
 		"@types/marked": "^4.0.8",
 		"@types/mocha": "^10.0.6",
 		"@types/mocha": "^10.0.6",
 		"@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.96.0",
 		"@types/ws": "^8.5.4",
 		"@types/ws": "^8.5.4",
 		"@typescript-eslint/eslint-plugin": "^5.57.1",
 		"@typescript-eslint/eslint-plugin": "^5.57.1",
 		"@typescript-eslint/eslint-plugin-tslint": "^5.57.1",
 		"@typescript-eslint/eslint-plugin-tslint": "^5.57.1",
@@ -879,6 +880,7 @@
 		"@vscode/test-electron": "^2.3.8",
 		"@vscode/test-electron": "^2.3.8",
 		"@vscode/vsce": "^2.29.0",
 		"@vscode/vsce": "^2.29.0",
 		"chai": "^4.3.10",
 		"chai": "^4.3.10",
+		"chai-subset": "^1.6.0",
 		"esbuild": "^0.17.15",
 		"esbuild": "^0.17.15",
 		"eslint": "^8.37.0",
 		"eslint": "^8.37.0",
 		"mocha": "^10.2.0",
 		"mocha": "^10.2.0",
@@ -888,8 +890,8 @@
 		"typescript": "^5.2.2"
 		"typescript": "^5.2.2"
 	},
 	},
 	"dependencies": {
 	"dependencies": {
-		"@vscode/debugadapter": "^1.64.0",
-		"@vscode/debugprotocol": "^1.64.0",
+		"@vscode/debugadapter": "^1.68.0",
+		"@vscode/debugprotocol": "^1.68.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",

+ 46 - 16
src/debugger/godot4/debug_session.ts

@@ -17,7 +17,7 @@ import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
 import { SceneTreeProvider } from "../scene_tree_provider";
 import { SceneTreeProvider } from "../scene_tree_provider";
 import { is_variable_built_in_type, parse_variable } from "./helpers";
 import { is_variable_built_in_type, parse_variable } from "./helpers";
 import { ServerController } from "./server_controller";
 import { ServerController } from "./server_controller";
-import { ObjectId } from "./variables/variants";
+import { ObjectId, RawObject } from "./variables/variants";
 
 
 const log = createLogger("debugger.session", { output: "Godot Debugger" });
 const log = createLogger("debugger.session", { output: "Godot Debugger" });
 
 
@@ -55,6 +55,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.InitializeResponse,
 		response: DebugProtocol.InitializeResponse,
 		args: DebugProtocol.InitializeRequestArguments,
 		args: DebugProtocol.InitializeRequestArguments,
 	) {
 	) {
+		log.info("initializeRequest", args);
 		response.body = response.body || {};
 		response.body = response.body || {};
 
 
 		response.body.supportsConfigurationDoneRequest = true;
 		response.body.supportsConfigurationDoneRequest = true;
@@ -83,6 +84,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) {
 	protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) {
+		log.info("launchRequest", args);
 		await this.configuration_done.wait(1000);
 		await this.configuration_done.wait(1000);
 
 
 		this.mode = "launch";
 		this.mode = "launch";
@@ -95,6 +97,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) {
 	protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) {
+		log.info("attachRequest", args);
 		await this.configuration_done.wait(1000);
 		await this.configuration_done.wait(1000);
 
 
 		this.mode = "attach";
 		this.mode = "attach";
@@ -109,11 +112,13 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.ConfigurationDoneResponse,
 		response: DebugProtocol.ConfigurationDoneResponse,
 		args: DebugProtocol.ConfigurationDoneArguments,
 		args: DebugProtocol.ConfigurationDoneArguments,
 	) {
 	) {
+		log.info("configurationDoneRequest", args);
 		this.configuration_done.notify();
 		this.configuration_done.notify();
 		this.sendResponse(response);
 		this.sendResponse(response);
 	}
 	}
 
 
 	protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
 	protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
+		log.info("continueRequest", args);
 		if (!this.exception) {
 		if (!this.exception) {
 			response.body = { allThreadsContinued: true };
 			response.body = { allThreadsContinued: true };
 			this.controller.continue();
 			this.controller.continue();
@@ -122,6 +127,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
 	protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
+		log.info("evaluateRequest", args);
 		await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
 		await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
 
 
 		if (this.all_scopes) {
 		if (this.all_scopes) {
@@ -149,6 +155,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
 	protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
+		log.info("nextRequest", args);
 		if (!this.exception) {
 		if (!this.exception) {
 			this.controller.next();
 			this.controller.next();
 			this.sendResponse(response);
 			this.sendResponse(response);
@@ -156,6 +163,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
 	protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
+		log.info("pauseRequest", args);
 		if (!this.exception) {
 		if (!this.exception) {
 			this.controller.break();
 			this.controller.break();
 			this.sendResponse(response);
 			this.sendResponse(response);
@@ -163,6 +171,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
 	protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
+		log.info("scopesRequest", args);
 		this.controller.request_stack_frame_vars(args.frameId);
 		this.controller.request_stack_frame_vars(args.frameId);
 		await this.got_scope.wait(2000);
 		await this.got_scope.wait(2000);
 
 
@@ -180,6 +189,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.SetBreakpointsResponse,
 		response: DebugProtocol.SetBreakpointsResponse,
 		args: DebugProtocol.SetBreakpointsArguments,
 		args: DebugProtocol.SetBreakpointsArguments,
 	) {
 	) {
+		log.info("setBreakPointsRequest", args);
 		const path = (args.source.path as string).replace(/\\/g, "/");
 		const path = (args.source.path as string).replace(/\\/g, "/");
 		const client_lines = args.lines || [];
 		const client_lines = args.lines || [];
 
 
@@ -216,6 +226,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
 	protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
+		log.info("stackTraceRequest", args);
 		if (this.debug_data.last_frame) {
 		if (this.debug_data.last_frame) {
 			response.body = {
 			response.body = {
 				totalFrames: this.debug_data.last_frames.length,
 				totalFrames: this.debug_data.last_frames.length,
@@ -234,6 +245,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
 	protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
+		log.info("stepInRequest", args);
 		if (!this.exception) {
 		if (!this.exception) {
 			this.controller.step();
 			this.controller.step();
 			this.sendResponse(response);
 			this.sendResponse(response);
@@ -241,6 +253,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
 	protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
+		log.info("stepOutRequest", args);
 		if (!this.exception) {
 		if (!this.exception) {
 			this.controller.step_out();
 			this.controller.step_out();
 			this.sendResponse(response);
 			this.sendResponse(response);
@@ -248,6 +261,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
 	protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
+		log.info("terminateRequest", args);
 		if (this.mode === "launch") {
 		if (this.mode === "launch") {
 			this.controller.stop();
 			this.controller.stop();
 			this.sendEvent(new TerminatedEvent());
 			this.sendEvent(new TerminatedEvent());
@@ -256,6 +270,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 	}
 	}
 
 
 	protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
 	protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
+		log.info("threadsRequest");
 		response.body = { threads: [new Thread(0, "thread_1")] };
 		response.body = { threads: [new Thread(0, "thread_1")] };
 		this.sendResponse(response);
 		this.sendResponse(response);
 	}
 	}
@@ -264,6 +279,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 		response: DebugProtocol.VariablesResponse,
 		response: DebugProtocol.VariablesResponse,
 		args: DebugProtocol.VariablesArguments,
 		args: DebugProtocol.VariablesArguments,
 	) {
 	) {
+		log.info("variablesRequest", args);
 		if (!this.all_scopes) {
 		if (!this.all_scopes) {
 			response.body = {
 			response.body = {
 				variables: [],
 				variables: [],
@@ -347,48 +363,62 @@ export class GodotDebugSession extends LoggingDebugSession {
 
 
 		this.add_to_inspections();
 		this.add_to_inspections();
 
 
-		if (this.ongoing_inspections.length === 0) {
+		if (this.ongoing_inspections.length === 0  && stackVars.remaining == 0) {
+			// in case if stackVars are empty, the this.ongoing_inspections will be empty also
+			// godot 4.3 generates empty stackVars with remaining > 0 on a breakpoint stop
+			// godot will continue sending `stack_frame_vars` until all `stackVars.remaining` are sent
+			// at this moment `stack_frame_vars` will call `set_scopes` again with cumulated stackVars
+			// TODO: godot won't send the recursive variable, see related https://github.com/godotengine/godot/issues/76019
+			//   in that case the vscode extension fails to call this.got_scope.notify();
+			//   hence the extension needs to be refactored to handle missing `stack_frame_vars` messages
 			this.previous_inspections = [];
 			this.previous_inspections = [];
 			this.got_scope.notify();
 			this.got_scope.notify();
 		}
 		}
 	}
 	}
 
 
-	public set_inspection(id: bigint, replacement: GodotVariable) {
+	public set_inspection(id: bigint, rawObject: RawObject, sub_values: GodotVariable[]) {
 		const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id);
 		const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id);
 
 
 		for (const va of variables) {
 		for (const va of variables) {
 			const index = this.all_scopes.findIndex((va_id) => va_id === va);
 			const index = this.all_scopes.findIndex((va_id) => va_id === va);
+			if (index < 0) {
+				continue;
+			}
 			const old = this.all_scopes.splice(index, 1);
 			const old = this.all_scopes.splice(index, 1);
+			// GodotVariable instance will be different for different variables, even if the referenced object id is the same:
+			const replacement = {value: rawObject, sub_values: sub_values } as GodotVariable;
 			replacement.name = old[0].name;
 			replacement.name = old[0].name;
 			replacement.scope_path = old[0].scope_path;
 			replacement.scope_path = old[0].scope_path;
 			this.append_variable(replacement, index);
 			this.append_variable(replacement, index);
 		}
 		}
 
 
-		this.ongoing_inspections.splice(
-			this.ongoing_inspections.findIndex((va_id) => va_id === id),
-			1,
-		);
+		const ongoing_inspections_index = this.ongoing_inspections.findIndex((va_id) => va_id === id);
+		if (ongoing_inspections_index >= 0) {
+			this.ongoing_inspections.splice(ongoing_inspections_index, 1);
+		}
+		
 
 
 		this.previous_inspections.push(id);
 		this.previous_inspections.push(id);
 
 
 		// this.add_to_inspections();
 		// this.add_to_inspections();
 
 
 		if (this.ongoing_inspections.length === 0) {
 		if (this.ongoing_inspections.length === 0) {
+			// the `ongoing_inspections` is not empty, until all scopes are fully resolved
+			// once last inspection is resolved: Notify that we got full scope
 			this.previous_inspections = [];
 			this.previous_inspections = [];
 			this.got_scope.notify();
 			this.got_scope.notify();
 		}
 		}
 	}
 	}
 
 
 	private add_to_inspections() {
 	private add_to_inspections() {
-		for (const va of this.all_scopes) {
-			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);
-				}
+		const scopes_to_check = this.all_scopes.filter((va) => va && va.value instanceof ObjectId);
+		for (const va of scopes_to_check) {
+			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);
 			}
 			}
 		}
 		}
 	}
 	}

+ 359 - 0
src/debugger/godot4/debugger_variables.test.ts

@@ -0,0 +1,359 @@
+import { promises as fs } from "fs";
+import * as path from "path";
+import * as vscode from "vscode";
+import { DebugProtocol } from "@vscode/debugprotocol";
+import chai from "chai";
+import chaiSubset from "chai-subset";
+
+import { promisify } from "util";
+import { execFile } from "child_process";
+const execFileAsync = promisify(execFile);
+
+chai.use(chaiSubset);
+const { expect } = chai;
+
+async function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve,  ms));
+}
+
+/**
+ * Given a path to a script, returns an object where each key is the name of a
+ * breakpoint (delimited by `breakpoint::`) and each value is the line number
+ * where the breakpoint appears in the script.
+ *
+ * @param scriptPath The path to the script to scan.
+ * @returns An object of breakpoint names to line numbers.
+ */
+async function getBreakpointLocations(scriptPath: string): Promise<{ [key: string]: vscode.Location }> {
+  const script_content = await fs.readFile(scriptPath, "utf-8");
+  const breakpoints: { [key: string]: vscode.Location } = {};
+  const breakpointRegex = /\b(breakpoint::.*)\b/g;
+  let match: RegExpExecArray | null;
+  while ((match = breakpointRegex.exec(script_content)) !== null) {
+    const breakpointName = match[1];
+    const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1;
+    breakpoints[breakpointName] = new vscode.Location(vscode.Uri.file(scriptPath), new vscode.Position(line - 1, 0));
+  }
+  return breakpoints;
+}
+
+async function waitForActiveStackItemChange(ms: number = 10000): Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
+  const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((resolve, reject) => {
+      const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => {
+        debugListener.dispose();
+        resolve(vscode.debug.activeStackItem);
+      });
+
+      // Timeout fallback in case stack item never changes
+      setTimeout(() => {
+          debugListener.dispose();
+          console.warn();
+          reject(new Error(`The ActiveStackItem eventwas not changed within the timeout period of '${ms}'`));
+      }, ms);
+  });
+
+  return res;
+}
+
+async function getStackFrames(threadId: number = 1): Promise<DebugProtocol.StackFrame[]> {
+  // Ensure there is an active debug session
+  if (!vscode.debug.activeDebugSession) {
+      throw new Error("No active debug session found");
+  }
+
+  // corresponds to file://./debug_session.ts stackTraceRequest(...)
+  const stackTraceResponse = await vscode.debug.activeDebugSession.customRequest("stackTrace", {
+      threadId: threadId,
+  });
+
+  // Extract and return the stack frames
+  return stackTraceResponse.stackFrames || [];
+}
+
+async function waitForBreakpoint(breakpoint: vscode.SourceBreakpoint, timeoutMs: number, ctx?: Mocha.Context): Promise<void> {
+  const t0 = performance.now();
+  console.log(fmt(`Waiting for breakpoint ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}`));
+  const res = await waitForActiveStackItemChange(timeoutMs);
+  const t1 = performance.now();
+  console.log(fmt(`Waiting for breakpoint completed ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}, took ${t1 - t0}ms`));
+  const stackFrames = await getStackFrames();
+  if (stackFrames[0].source.path !== breakpoint.location.uri.fsPath || stackFrames[0].line != breakpoint.location.range.start.line+1) {
+    throw new Error(`Wrong breakpoint was hit. Expected: ${breakpoint.location.uri.fsPath}:${breakpoint.location.range.start.line+1}, Got: ${stackFrames[0].source.path}:${stackFrames[0].line}`);
+  }
+}
+
+enum VariableScope {
+  Locals  = 1,
+  Members = 2,
+  Globals = 3
+}
+
+async function getVariablesForScope(scope: VariableScope): Promise<DebugProtocol.Variable[]> {
+  // corresponds to file://./debug_session.ts protected async variablesRequest
+  const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
+    variablesReference: scope
+  });
+  return variablesResponse?.variables || [];
+}
+
+async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise<any> {
+  // corresponds to file://./debug_session.ts protected async evaluateRequest
+  const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest("evaluate", {
+    context,
+    expression,
+    frameId
+  });
+  return evaluateResponse.body;
+}
+
+function formatMs(ms: number): string {
+  const seconds = Math.floor((ms / 1000) % 60);
+  const minutes = Math.floor((ms / (1000 * 60)) % 60);
+  return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${(Math.round(ms) % 1000).toString().padStart(3, "0")}`;
+}
+
+function formatMessage(this: Mocha.Context, msg: string): string {
+  return `[${formatMs(performance.now()-this.testStart)}] ${msg}`;
+}
+
+var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
+
+async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn"): Promise<void> {
+  const t0 = performance.now();
+  const debugConfig: vscode.DebugConfiguration = {
+    type: "godot",
+    request: "launch",
+    name: "Godot Debug",
+    scene: scene,
+    additional_options: "--headless"
+  };
+  console.log(fmt(`Starting debugger for scene ${scene}`));
+  const res = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfig);
+  const t1 = performance.now();
+  console.log(fmt(`Starting debugger for scene ${scene} completed, took ${t1 - t0}ms`));
+  if (!res) {
+    throw new Error(`Failed to start debugging for scene ${scene}`);
+  }
+}
+
+suite("DAP Integration Tests - Variable Scopes", () => {
+  // workspaceFolder should match `.vscode-test.js`::workspaceFolder
+  const workspaceFolder = path.resolve(__dirname, "../../../test_projects/test-dap-project-godot4");
+
+  suiteSetup(async function() {
+    this.timeout(20000); // enough time to do `godot --import`
+    console.log("Environment Variables:");
+    for (const [key, value] of Object.entries(process.env)) {
+      console.log(`${key}: ${value}`);
+    }
+
+    // init the godot project by importing it in godot engine:
+    const config = vscode.workspace.getConfiguration("godotTools");
+    var godot4_path = config.get<string>("editorPath.godot4");
+    // get the path for currently opened project in vscode test instance:
+    var project_path = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
+    console.log("Executing", [godot4_path, "--headless", "--import", project_path]);
+    const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", project_path], {shell: true, cwd: project_path});
+    if (exec_res.stderr !== "") {
+      throw new Error(exec_res.stderr);
+    }
+    console.log(exec_res.stdout);
+  });
+
+  setup(async function() {
+    console.log(`➤ Test '${this?.currentTest.title}' starting`);
+    await vscode.commands.executeCommand("workbench.action.closeAllEditors");
+    if (vscode.debug.breakpoints) {
+      await vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
+    }
+    this.testStart = performance.now();
+    fmt = formatMessage.bind(this);
+  });
+
+
+  teardown(async function() {
+    await sleep(1000);
+    if (vscode.debug.activeDebugSession !== undefined) {
+      console.log("Closing debug session");
+      await vscode.debug.stopDebugging();
+    }
+    console.log(`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`);
+  });
+
+
+  test("sample test", async function() {
+    // await sleep(1000);
+    await startDebugging("ScopeVars.tscn");
+  });
+
+  
+  test("should return correct scopes", async function() {
+    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+    vscode.debug.addBreakpoints([breakpoint]);
+
+    await startDebugging("ScopeVars.tscn");
+    await waitForBreakpoint(breakpoint, 2000);
+
+    // TODO: current DAP needs a delay before it will return variables
+    console.log("Sleeping for 2 seconds");
+    await sleep(2000);
+
+    // corresponds to file://./debug_session.ts async scopesRequest
+    const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: 1});
+
+    expect(res_scopes).to.exist;
+    expect(res_scopes.scopes).to.exist;
+
+    const scopes = res_scopes.scopes;
+    expect(scopes.length).to.equal(3, "Expected 3 scopes");
+    expect(scopes[0].name).to.equal(VariableScope[VariableScope.Locals], "Expected Locals scope");
+    expect(scopes[0].variablesReference).to.equal(VariableScope.Locals, "Expected Locals variablesReference");
+    expect(scopes[1].name).to.equal(VariableScope[VariableScope.Members], "Expected Members scope");
+    expect(scopes[1].variablesReference).to.equal(VariableScope.Members, "Expected Members variablesReference");
+    expect(scopes[2].name).to.equal(VariableScope[VariableScope.Globals], "Expected Globals scope");
+    expect(scopes[2].variablesReference).to.equal(VariableScope.Globals, "Expected Globals variablesReference");
+
+    await sleep(1000);
+    await vscode.debug.stopDebugging();
+  })?.timeout(5000);
+
+  test("should return global variables", async function() {
+    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+    vscode.debug.addBreakpoints([breakpoint]);
+
+    await startDebugging("ScopeVars.tscn");
+    await waitForBreakpoint(breakpoint, 2000);
+    
+    // TODO: current DAP needs a delay before it will return variables
+    console.log("Sleeping for 2 seconds");
+    await sleep(2000);
+
+    const variables = await getVariablesForScope(VariableScope.Globals);
+    expect(variables).to.containSubset([{name: "GlobalScript"}]);
+    
+    await sleep(1000);
+    await vscode.debug.stopDebugging();
+  })?.timeout(7000);
+
+  test("should return local variables", async function() {
+    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+    vscode.debug.addBreakpoints([breakpoint]);
+
+    await startDebugging("ScopeVars.tscn");
+    await waitForBreakpoint(breakpoint, 2000);
+
+    // TODO: current DAP needs a delay before it will return variables
+    console.log("Sleeping for 2 seconds");
+    await sleep(2000);
+
+    const variables = await getVariablesForScope(VariableScope.Locals);
+    expect(variables.length).to.equal(2);
+    expect(variables).to.containSubset([{name: "local1"}]);
+    expect(variables).to.containSubset([{name: "local2"}]);
+    
+    await sleep(1000);
+    await vscode.debug.stopDebugging();
+  })?.timeout(5000);
+
+  test("should return member variables", async function() {
+    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+    vscode.debug.addBreakpoints([breakpoint]);
+
+    await startDebugging("ScopeVars.tscn");
+    await waitForBreakpoint(breakpoint, 2000);
+
+    // TODO: current DAP needs a delay before it will return variables
+    console.log("Sleeping for 2 seconds");
+    await sleep(2000);
+
+    const variables = await getVariablesForScope(VariableScope.Members);
+    expect(variables.length).to.equal(2);
+    expect(variables).to.containSubset([{name: "self"}]);
+    expect(variables).to.containSubset([{name: "member1"}]);
+
+    await sleep(1000);
+    await vscode.debug.stopDebugging();
+  })?.timeout(5000);
+
+  test("should retrieve all built-in types correctly", async function() {
+    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
+    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]);
+    vscode.debug.addBreakpoints([breakpoint]);
+
+    await startDebugging("BuiltInTypes.tscn");
+    await waitForBreakpoint(breakpoint, 2000);
+    
+    // TODO: current DAP needs a delay before it will return variables
+    console.log("Sleeping for 2 seconds");
+    await sleep(2000);
+    
+    const variables = await getVariablesForScope(VariableScope.Locals);
+
+    expect(variables).to.containSubset([{ name: "int_var", value: "42" }]);
+    expect(variables).to.containSubset([{ name: "float_var", value: "3.14" }]);
+    expect(variables).to.containSubset([{ name: "bool_var", value: "true" }]);
+    expect(variables).to.containSubset([{ name: "string_var", value: "Hello, Godot!" }]);
+    expect(variables).to.containSubset([{ name: "nil_var", value: "null" }]);
+    expect(variables).to.containSubset([{ name: "vector2", value: "Vector2(10, 20)" }]);
+    expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]);
+    expect(variables).to.containSubset([{ name: "rect2", value: "Rect2((0, 0) - (100, 50))" }]);
+    expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]);
+    // expect(variables).to.containSubset([{ name: "simple_array", value: "[1, 2, 3]" }]);
+    expect(variables).to.containSubset([{ name: "simple_array", value: "Array[3]" }]);
+    // expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]);
+    // expect(variables).to.containSubset([{ name: "nested_dict.sub_dict.sub_key", value: "99" }]);
+    expect(variables).to.containSubset([{ name: "nested_dict", value: "Dictionary[2]" }]);
+    // expect(variables).to.containSubset([{ name: "byte_array", value: "[0, 1, 2, 255]" }]);
+    expect(variables).to.containSubset([{ name: "byte_array", value: "Array[4]" }]);
+    // expect(variables).to.containSubset([{ name: "int32_array", value: "[100, 200, 300]" }]);
+    expect(variables).to.containSubset([{ name: "int32_array", value: "Array[3]" }]);
+    expect(variables).to.containSubset([{ name: "color_var", value: "Color(1, 0, 0, 1)" }]);
+    expect(variables).to.containSubset([{ name: "aabb_var", value: "AABB((0, 0, 0), (1, 1, 1))" }]);
+    expect(variables).to.containSubset([{ name: "plane_var", value: "Plane(0, 1, 0, -5)" }]);
+    expect(variables).to.containSubset([{ name: "callable_var", value: "Callable()" }]);
+    expect(variables).to.containSubset([{ name: "signal_var" }]);
+    const signal_var = variables.find(v => v.name === "signal_var");
+    expect(signal_var.value).to.match(/Signal\(member_signal\, <\d+>\)/, "Should be in format of 'Signal(member_signal, <28236055815>)'");
+  
+    await sleep(1000);
+    await vscode.debug.stopDebugging();
+  })?.timeout(5000);
+
+  test("should retrieve all complex variables correctly", async function() {
+    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
+    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]);
+    vscode.debug.addBreakpoints([breakpoint]);
+
+    await startDebugging("ExtensiveVars.tscn");
+    await waitForBreakpoint(breakpoint, 2000);
+
+    // TODO: current DAP needs a delay before it will return variables
+    console.log("Sleeping for 2 seconds");
+    await sleep(2000);
+
+    const memberVariables = await getVariablesForScope(VariableScope.Members);
+    
+    expect(memberVariables.length).to.equal(3);
+    expect(memberVariables).to.containSubset([{name: "self"}]);
+    expect(memberVariables).to.containSubset([{name: "self_var"}]);
+    const self = memberVariables.find(v => v.name === "self");
+    const self_var = memberVariables.find(v => v.name === "self_var");
+    expect(self.value).to.deep.equal(self_var.value);
+    
+    const localVariables = await getVariablesForScope(VariableScope.Locals);
+    expect(localVariables.length).to.equal(4);
+    expect(localVariables).to.containSubset([
+      { name: "local_label", value: "Label" },
+      { name: "local_self_var_through_label", value: "Node2D" },
+      { name: "local_classA", value: "RefCounted" },
+      { name: "local_classB", value: "RefCounted" }
+    ]);
+    
+    await sleep(1000);
+    await vscode.debug.stopDebugging();
+  })?.timeout(15000);
+});

+ 34 - 28
src/debugger/godot4/helpers.ts

@@ -36,38 +36,40 @@ export function is_variable_built_in_type(va: GodotVariable) {
 	return ["number", "bigint", "boolean", "string"].some(x => x == type);
 	return ["number", "bigint", "boolean", "string"].some(x => x == type);
 }
 }
 
 
-export function build_sub_values(va: GodotVariable) {
-	const value = va.value;
-
+export function get_sub_values(value: any) {
 	let subValues: GodotVariable[] = undefined;
 	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;
-		});
+	if (value) {
+		if (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 (typeof value["sub_values"] === "function") {
+			subValues = value.sub_values()?.map((sva) => {
+				return { name: sva.name, value: sva.value } as GodotVariable;
+			});
+		}
 	}
 	}
 
 
-	va.sub_values = subValues;
+	for (let i = 0; i < subValues?.length; i++) {
+		subValues[i].sub_values = get_sub_values(subValues[i].value);
+	}
 
 
-	subValues?.forEach(build_sub_values);
+	return subValues;
 }
 }
 
 
 export function parse_variable(va: GodotVariable, i?: number) {
 export function parse_variable(va: GodotVariable, i?: number) {
@@ -103,7 +105,11 @@ export function parse_variable(va: GodotVariable, i?: number) {
 			array_type = "named";
 			array_type = "named";
 			reference = i ? i : 0;
 			reference = i ? i : 0;
 		} else {
 		} else {
-			rendered_value = `${value.type_name()}${value.stringify_value()}`;
+			try {
+				rendered_value = `${value.type_name()}${value.stringify_value()}`;
+			} catch (e) {
+				rendered_value = `${value}`;
+			}
 			reference = i ? i : 0;
 			reference = i ? i : 0;
 		}
 		}
 	}
 	}

+ 10 - 8
src/debugger/godot4/server_controller.ts

@@ -19,7 +19,7 @@ import { killSubProcesses, subProcess } from "../../utils/subspawn";
 import { GodotStackFrame, GodotStackVars, GodotVariable } from "../debug_runtime";
 import { GodotStackFrame, GodotStackVars, GodotVariable } from "../debug_runtime";
 import { AttachRequestArguments, LaunchRequestArguments, pinnedScene } from "../debugger";
 import { AttachRequestArguments, LaunchRequestArguments, pinnedScene } from "../debugger";
 import { GodotDebugSession } from "./debug_session";
 import { GodotDebugSession } from "./debug_session";
-import { build_sub_values, parse_next_scene_node, split_buffers } from "./helpers";
+import { get_sub_values, parse_next_scene_node, split_buffers } from "./helpers";
 import { VariantDecoder } from "./variables/variant_decoder";
 import { VariantDecoder } from "./variables/variant_decoder";
 import { VariantEncoder } from "./variables/variant_encoder";
 import { VariantEncoder } from "./variables/variant_encoder";
 import { RawObject } from "./variables/variants";
 import { RawObject } from "./variables/variants";
@@ -395,13 +395,15 @@ export class ServerController {
 				for (const prop of properties) {
 				for (const prop of properties) {
 					rawObject.set(prop[0], prop[5]);
 					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);
+				const sub_values = get_sub_values(rawObject);
+				
+				const inspect_callback = this.session.inspect_callbacks.get(BigInt(id));
+				if (inspect_callback !== undefined) {
+					const inspectedVariable = { name: "", value: rawObject, sub_values: sub_values } as GodotVariable;
+					inspect_callback(inspectedVariable.name, inspectedVariable);
 					this.session.inspect_callbacks.delete(BigInt(id));
 					this.session.inspect_callbacks.delete(BigInt(id));
 				}
 				}
-				this.session.set_inspection(id, inspectedVariable);
+				this.session.set_inspection(id, rawObject, sub_values);
 				break;
 				break;
 			}
 			}
 			case "stack_dump": {
 			case "stack_dump": {
@@ -641,8 +643,8 @@ export class ServerController {
 			throw new Error("More stack frame variables were sent than expected.");
 			throw new Error("More stack frame variables were sent than expected.");
 		}
 		}
 
 
-		const variable: GodotVariable = { name, value, type };
-		build_sub_values(variable);
+		const sub_values = get_sub_values(value);
+		const variable = { name, value, type, sub_values } as GodotVariable;
 
 
 		const scopeName = ["locals", "members", "globals"][scope];
 		const scopeName = ["locals", "members", "globals"][scope];
 		this.partialStackVars[scopeName].push(variable);
 		this.partialStackVars[scopeName].push(variable);

+ 1 - 1
src/debugger/godot4/variables/variants.ts

@@ -471,7 +471,7 @@ export class Signal implements GDObject {
 	constructor(public name: string, public oid: ObjectId) {}
 	constructor(public name: string, public oid: ObjectId) {}
 
 
 	public stringify_value(): string {
 	public stringify_value(): string {
-		return `${this.name}() ${this.oid.stringify_value()}`;
+		return `(${this.name}, ${this.oid.stringify_value()})`;
 	}
 	}
 
 
 	public sub_values(): GodotVariable[] {
 	public sub_values(): GodotVariable[] {

+ 3 - 2
src/debugger/scene_tree_provider.ts

@@ -5,6 +5,7 @@ import {
 	ProviderResult,
 	ProviderResult,
 	TreeItem,
 	TreeItem,
 	TreeItemCollapsibleState,
 	TreeItemCollapsibleState,
+	Uri
 } from "vscode";
 } from "vscode";
 import path = require("path");
 import path = require("path");
 import { get_extension_uri } from "../utils";
 import { get_extension_uri } from "../utils";
@@ -81,8 +82,8 @@ export class SceneNode extends TreeItem {
 		const iconName = class_name + ".svg";
 		const iconName = class_name + ".svg";
 
 
 		this.iconPath = {
 		this.iconPath = {
-			light: path.join(iconDir, "light", iconName),
-			dark: path.join(iconDir, "dark", iconName),
+			light: Uri.file(path.join(iconDir, "light", iconName)),
+			dark: Uri.file(path.join(iconDir, "dark", iconName)),
 		};
 		};
 	}
 	}
 }
 }

+ 4 - 0
src/formatter/formatter.test.ts

@@ -138,6 +138,10 @@ function parse_test_file(content: string): Test[] {
 suite("GDScript Formatter Tests", () => {
 suite("GDScript Formatter Tests", () => {
 	const testFiles = fs.readdirSync(snapshotsFolderPath, { withFileTypes: true, recursive: true });
 	const testFiles = fs.readdirSync(snapshotsFolderPath, { withFileTypes: true, recursive: true });
 
 
+	teardown(async () => {
+		await vscode.commands.executeCommand("workbench.action.closeAllEditors");
+	});
+
 	for (const file of testFiles.filter((f) => f.isFile())) {
 	for (const file of testFiles.filter((f) => f.isFile())) {
 		if (["in.gd", "out.gd"].includes(file.name) || !file.name.endsWith(".gd")) {
 		if (["in.gd", "out.gd"].includes(file.name) || !file.name.endsWith(".gd")) {
 			continue;
 			continue;

+ 3 - 2
src/scene_tools/types.ts

@@ -2,6 +2,7 @@ import {
 	TreeItem,
 	TreeItem,
 	TreeItemCollapsibleState,
 	TreeItemCollapsibleState,
 	MarkdownString,
 	MarkdownString,
+	Uri
 } from "vscode";
 } from "vscode";
 import * as path from "path";
 import * as path from "path";
 import { get_extension_uri } from "../utils";
 import { get_extension_uri } from "../utils";
@@ -31,8 +32,8 @@ export class SceneNode extends TreeItem {
 		const iconName = className + ".svg";
 		const iconName = className + ".svg";
 
 
 		this.iconPath = {
 		this.iconPath = {
-			light: path.join(iconDir, "light", iconName),
-			dark: path.join(iconDir, "dark", iconName),
+			light: Uri.file(path.join(iconDir, "light", iconName)),
+			dark: Uri.file(path.join(iconDir, "dark", iconName)),
 		};
 		};
 	}
 	}
 
 

+ 40 - 0
test_projects/test-dap-project-godot4/.vscode/launch.json

@@ -0,0 +1,40 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+    {
+        "name": "GDScript: Launch ScopeVars.tscn",
+        "type": "godot",
+        "request": "launch",
+        "project": "${workspaceFolder}",
+        "scene": "ScopeVars.tscn"
+        // "debug_collisions": false,
+        // "debug_paths": false,
+        // "debug_navigation": false,
+        // "additional_options": ""
+    },
+    {
+        "name": "GDScript: Launch ExtensiveVars.tscn",
+        "type": "godot",
+        "request": "launch",
+        "project": "${workspaceFolder}",
+        "scene": "ExtensiveVars.tscn"
+    },
+    {
+        "name": "GDScript: Launch BuiltInTypes.tscn",
+        "type": "godot",
+        "request": "launch",
+        "project": "${workspaceFolder}",
+        "scene": "BuiltInTypes.tscn"
+    },
+    {
+        "name": "GDScript: Launch NodeVars.tscn",
+        "type": "godot",
+        "request": "launch",
+        "project": "${workspaceFolder}",
+        "scene": "NodeVars.tscn"
+    }
+    ]
+}

+ 39 - 0
test_projects/test-dap-project-godot4/BuiltInTypes.gd

@@ -0,0 +1,39 @@
+extends Node
+
+signal member_signal
+signal member_signal_with_parameters(my_param1: String)
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+  var int_var = 42
+  var float_var = 3.14
+  var bool_var = true
+  var string_var = "Hello, Godot!"
+  var nil_var = null
+  var vector2 = Vector2(10, 20)
+  var vector3 = Vector3(1, 2, 3)
+  var rect2 = Rect2(0, 0, 100, 50)
+  var quaternion = Quaternion(0, 0, 0, 1)
+  var simple_array = [1, 2, 3]
+  var nested_dict = {
+      "nested_key": "Nested Value",
+      "sub_dict": {"sub_key": 99}
+  }  
+  var byte_array = PackedByteArray([0, 1, 2, 255])
+  var int32_array = PackedInt32Array([100, 200, 300])
+  var color_var = Color(1, 0, 0, 1) # Red color
+  var aabb_var = AABB(Vector3(0, 0, 0), Vector3(1, 1, 1))
+  var plane_var = Plane(Vector3(0, 1, 0), -5)
+
+  var callable_var = self.my_callable_func
+
+  var signal_var = member_signal
+  member_signal.connect(singal_connected_func)
+
+  print("breakpoint::BuiltInTypes::_ready")
+  
+func my_callable_func():
+  pass
+
+func singal_connected_func():
+  pass

+ 11 - 0
test_projects/test-dap-project-godot4/BuiltInTypes.tscn

@@ -0,0 +1,11 @@
+[gd_scene load_steps=2 format=3 uid="uid://d0ovhv6f38jj4"]
+
+[ext_resource type="Script" path="res://BuiltInTypes.gd" id="1_2dpge"]
+
+[node name="BuiltInTypes" type="Node"]
+script = ExtResource("1_2dpge")
+
+[node name="Label" type="Label" parent="."]
+offset_right = 40.0
+offset_bottom = 23.0
+text = "Built-in types"

+ 38 - 0
test_projects/test-dap-project-godot4/ExtensiveVars.gd

@@ -0,0 +1,38 @@
+extends Node2D
+
+var self_var := self
+@onready var label: ExtensiveVars_Label = $Label
+
+class ClassA:
+  var member_classB
+  var member_self := self
+  
+class ClassB:
+  var member_classA
+
+func _ready() -> void:
+  var local_label := label
+  var local_self_var_through_label := label.parent_var
+  
+  var local_classA = ClassA.new()
+  var local_classB = ClassB.new()
+  local_classA.member_classB = local_classB
+  local_classB.member_classA = local_classA
+
+  # Circular reference.
+  # Note: that causes the godot engine to omit this variable, since stack_frame_var cannot be completed and sent
+  # https://github.com/godotengine/godot/issues/76019
+  # var dict = {}
+  # dict["self_ref"] = dict
+  
+  print("breakpoint::ExtensiveVars::_ready")
+
+func _process(delta: float) -> void:
+  var local_label := label
+  var local_self_var_through_label := label.parent_var
+  
+  var local_classA = ClassA.new()
+  var local_classB = ClassB.new()
+  local_classA.member_classB = local_classB
+  local_classB.member_classA = local_classA
+  pass

+ 12 - 0
test_projects/test-dap-project-godot4/ExtensiveVars.tscn

@@ -0,0 +1,12 @@
+[gd_scene load_steps=3 format=3 uid="uid://bsonfthpqa3dx"]
+
+[ext_resource type="Script" path="res://ExtensiveVars.gd" id="1_fnilr"]
+[ext_resource type="Script" path="res://ExtensiveVars_Label.gd" id="2_jijf2"]
+
+[node name="ExtensiveVars" type="Node2D"]
+script = ExtResource("1_fnilr")
+
+[node name="Label" type="Label" parent="."]
+text = "Extensive Vars scene"
+script = ExtResource("2_jijf2")
+metadata/_edit_use_anchors_ = true

+ 14 - 0
test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd

@@ -0,0 +1,14 @@
+extends Label
+
+class_name ExtensiveVars_Label
+
+@onready var parent_var: Node2D = $".."
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+  pass # Replace with function body.
+
+
+# Called every frame. 'delta' is the elapsed time since the previous frame.
+func _process(delta: float) -> void:
+  pass

+ 3 - 0
test_projects/test-dap-project-godot4/GlobalScript.gd

@@ -0,0 +1,3 @@
+extends Node
+
+var globalMember := "global member"

+ 8 - 0
test_projects/test-dap-project-godot4/Node1.gd

@@ -0,0 +1,8 @@
+extends Node
+
+@onready var parent_node: Node2D = $".."
+@onready var sibling_node2: Node = $"../node2"
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+  pass # Replace with function body.

+ 9 - 0
test_projects/test-dap-project-godot4/NodeVars.gd

@@ -0,0 +1,9 @@
+extends Node2D
+
+@onready var node_1: Node = $node1
+@onready var node_2: Node = $node2
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+  print("breakpoint::NodeVars::_ready")
+  pass

+ 17 - 0
test_projects/test-dap-project-godot4/NodeVars.tscn

@@ -0,0 +1,17 @@
+[gd_scene load_steps=3 format=3 uid="uid://xrjtth0d2nc5"]
+
+[ext_resource type="Script" path="res://NodeVars.gd" id="1_6eeca"]
+[ext_resource type="Script" path="res://Node1.gd" id="2_bl41t"]
+
+[node name="NodeVars" type="Node2D"]
+script = ExtResource("1_6eeca")
+
+[node name="node1" type="Node" parent="."]
+script = ExtResource("2_bl41t")
+
+[node name="node2" type="Node" parent="."]
+
+[node name="Label" type="Label" parent="."]
+offset_right = 40.0
+offset_bottom = 23.0
+text = "NodeVars"

+ 8 - 0
test_projects/test-dap-project-godot4/ScopeVars.gd

@@ -0,0 +1,8 @@
+extends Node
+
+var member1 := TestClassA.new()
+
+func _ready() -> void:
+  var local1 := TestClassA.new()
+  var local2 = GlobalScript.globalMember
+  print("breakpoint::ScopeVars::_ready")

+ 11 - 0
test_projects/test-dap-project-godot4/ScopeVars.tscn

@@ -0,0 +1,11 @@
+[gd_scene load_steps=2 format=3 uid="uid://g5gqewj2i2xs"]
+
+[ext_resource type="Script" path="res://ScopeVars.gd" id="1_wtcpp"]
+
+[node name="RootNode" type="Node"]
+script = ExtResource("1_wtcpp")
+
+[node name="Label" type="Label" parent="."]
+offset_right = 40.0
+offset_bottom = 23.0
+text = "Godot test project"

+ 5 - 0
test_projects/test-dap-project-godot4/TestClassA.gd

@@ -0,0 +1,5 @@
+class_name TestClassA
+
+var testclassa_member1 := "member1"
+
+var testclassa_member2: Node

+ 19 - 0
test_projects/test-dap-project-godot4/project.godot

@@ -0,0 +1,19 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Test DAP project godot4"
+run/main_scene="res://ScopeVars.tscn"
+config/features=PackedStringArray("4.3", "Forward Plus")
+
+[autoload]
+
+GlobalScript="*res://GlobalScript.gd"

+ 2 - 1
tsconfig.json

@@ -14,7 +14,8 @@
 		"rootDir": "src",
 		"rootDir": "src",
 		"strict": false,
 		"strict": false,
 		"skipLibCheck": true,
 		"skipLibCheck": true,
-		"allowJs": true
+		"allowJs": true,
+		"strictBindCallApply": true
 	},
 	},
 	"exclude": [
 	"exclude": [
 		"node_modules",
 		"node_modules",