Jelajahi Sumber

DebugAdapter variables overhaul (#793)

- Redesigned the representation of godot objects to match internal structure of godot server
- Lazy evaluation for the godot objects
- Stack frames now can be switched with variables updated
MichaelXt 5 bulan lalu
induk
melakukan
53f48ede63
31 mengubah file dengan 1480 tambahan dan 662 penghapusan
  1. 1 1
      .github/workflows/ci.yml
  2. 1 1
      .vscode-test.js
  3. 22 1
      .vscode/launch.json
  4. 478 11
      package-lock.json
  5. 7 4
      package.json
  6. 1 1
      src/debugger/debug_runtime.ts
  7. 94 94
      src/debugger/debugger.ts
  8. 71 346
      src/debugger/godot4/debug_session.ts
  9. 9 71
      src/debugger/godot4/helpers.ts
  10. 103 46
      src/debugger/godot4/server_controller.ts
  11. 127 69
      src/debugger/godot4/variables/debugger_variables.test.ts
  12. 58 0
      src/debugger/godot4/variables/godot_id_to_vscode_id_mapper.test.ts
  13. 67 0
      src/debugger/godot4/variables/godot_id_to_vscode_id_mapper.ts
  14. 79 0
      src/debugger/godot4/variables/godot_object_promise.test.ts
  15. 52 0
      src/debugger/godot4/variables/godot_object_promise.ts
  16. 240 0
      src/debugger/godot4/variables/variables_manager.ts
  17. 1 1
      src/debugger/godot4/variables/variants.ts
  18. 4 4
      src/debugger/inspector_provider.ts
  19. 4 4
      src/debugger/scene_tree_provider.ts
  20. 5 2
      test_projects/test-dap-project-godot4/BuiltInTypes.gd
  21. 1 0
      test_projects/test-dap-project-godot4/BuiltInTypes.gd.uid
  22. 27 4
      test_projects/test-dap-project-godot4/ExtensiveVars.gd
  23. 1 0
      test_projects/test-dap-project-godot4/ExtensiveVars.gd.uid
  24. 1 0
      test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd.uid
  25. 1 0
      test_projects/test-dap-project-godot4/GlobalScript.gd.uid
  26. 1 0
      test_projects/test-dap-project-godot4/Node1.gd.uid
  27. 1 0
      test_projects/test-dap-project-godot4/NodeVars.gd
  28. 1 0
      test_projects/test-dap-project-godot4/NodeVars.gd.uid
  29. 20 2
      test_projects/test-dap-project-godot4/ScopeVars.gd
  30. 1 0
      test_projects/test-dap-project-godot4/ScopeVars.gd.uid
  31. 1 0
      test_projects/test-dap-project-godot4/TestClassA.gd.uid

+ 1 - 1
.github/workflows/ci.yml

@@ -16,7 +16,7 @@ jobs:
       - name: Install Node.js
       - name: Install Node.js
         uses: actions/[email protected]
         uses: actions/[email protected]
         with:
         with:
-          node-version: 16.x
+          node-version: 22.x
 
 
       - name: Install Godot (Ubuntu)
       - name: Install Godot (Ubuntu)
         if: matrix.os == 'ubuntu-latest'
         if: matrix.os == 'ubuntu-latest'

+ 1 - 1
.vscode-test.js

@@ -2,7 +2,7 @@ const { defineConfig } = require('@vscode/test-cli');
 
 
 module.exports = defineConfig(
 module.exports = defineConfig(
 	{
 	{
-		// version: '1.84.0',
+		// version: '1.96.4',
 		label: 'unitTests',
 		label: 'unitTests',
 		files: 'out/**/*.test.js',
 		files: 'out/**/*.test.js',
 		launchArgs: ['--disable-extensions'],
 		launchArgs: ['--disable-extensions'],

+ 22 - 1
.vscode/launch.json

@@ -5,7 +5,6 @@
 {
 {
 	"version": "0.2.0",
 	"version": "0.2.0",
 	"configurations": [
 	"configurations": [
-		
 		{
 		{
 			"name": "Run Extension",
 			"name": "Run Extension",
 			"type": "extensionHost",
 			"type": "extensionHost",
@@ -48,5 +47,27 @@
 				"VSCODE_DEBUG_MODE": "true"
 				"VSCODE_DEBUG_MODE": "true"
 			}
 			}
 		},
 		},
+		{
+			"name": "Run Extension with local workspace file",
+			"type": "extensionHost",
+			"request": "launch",
+			"runtimeExecutable": "${execPath}",
+			"args": [
+				"--profile=temp",
+				"--extensionDevelopmentPath=${workspaceFolder}",
+				"${workspaceFolder}/workspace.code-workspace"
+			],
+			"outFiles": [
+				"${workspaceFolder}/out/**/*.js"
+			],
+			"skipFiles": [
+				"**/extensionHostProcess.js",
+				"<node_internals>/**/*.js"
+			],
+			"preLaunchTask": "npm: watch",
+			"env": {
+				"VSCODE_DEBUG_MODE": "true"
+			}
+		},
 	]
 	]
 }
 }

+ 478 - 11
package-lock.json

@@ -25,24 +25,27 @@
 			},
 			},
 			"devDependencies": {
 			"devDependencies": {
 				"@types/chai": "^4.3.11",
 				"@types/chai": "^4.3.11",
+				"@types/chai-as-promised": "^8.0.1",
 				"@types/chai-subset": "^1.3.5",
 				"@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.19.75",
 				"@types/prismjs": "^1.16.8",
 				"@types/prismjs": "^1.16.8",
 				"@types/vscode": "^1.96.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",
 				"@typescript-eslint/parser": "^5.57.1",
 				"@typescript-eslint/parser": "^5.57.1",
-				"@vscode/test-cli": "^0.0.4",
+				"@vscode/test-cli": "^0.0.10",
 				"@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.5.0",
+				"chai-as-promised": "^8.0.1",
 				"chai-subset": "^1.6.0",
 				"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.8.2",
+				"sinon": "^19.0.2",
 				"ts-node": "^10.9.1",
 				"ts-node": "^10.9.1",
 				"tsconfig-paths": "^4.2.0",
 				"tsconfig-paths": "^4.2.0",
 				"tslint": "^5.20.1",
 				"tslint": "^5.20.1",
@@ -381,6 +384,12 @@
 				"js-tokens": "^4.0.0"
 				"js-tokens": "^4.0.0"
 			}
 			}
 		},
 		},
+		"node_modules/@bcoe/v8-coverage": {
+			"version": "0.2.3",
+			"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+			"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+			"dev": true
+		},
 		"node_modules/@cspotcode/source-map-support": {
 		"node_modules/@cspotcode/source-map-support": {
 			"version": "0.8.1",
 			"version": "0.8.1",
 			"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
 			"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -960,6 +969,15 @@
 				"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
 				"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
 			}
 			}
 		},
 		},
+		"node_modules/@istanbuljs/schema": {
+			"version": "0.1.3",
+			"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+			"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
 		"node_modules/@jridgewell/resolve-uri": {
 		"node_modules/@jridgewell/resolve-uri": {
 			"version": "3.1.0",
 			"version": "3.1.0",
 			"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
 			"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
@@ -1030,6 +1048,50 @@
 				"node": ">=14"
 				"node": ">=14"
 			}
 			}
 		},
 		},
+		"node_modules/@sinonjs/commons": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+			"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+			"dev": true,
+			"dependencies": {
+				"type-detect": "4.0.8"
+			}
+		},
+		"node_modules/@sinonjs/commons/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==",
+			"dev": true,
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/@sinonjs/fake-timers": {
+			"version": "13.0.5",
+			"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+			"integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+			"dev": true,
+			"dependencies": {
+				"@sinonjs/commons": "^3.0.1"
+			}
+		},
+		"node_modules/@sinonjs/samsam": {
+			"version": "8.0.2",
+			"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz",
+			"integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==",
+			"dev": true,
+			"dependencies": {
+				"@sinonjs/commons": "^3.0.1",
+				"lodash.get": "^4.4.2",
+				"type-detect": "^4.1.0"
+			}
+		},
+		"node_modules/@sinonjs/text-encoding": {
+			"version": "0.7.3",
+			"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz",
+			"integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==",
+			"dev": true
+		},
 		"node_modules/@tootallnate/once": {
 		"node_modules/@tootallnate/once": {
 			"version": "1.1.2",
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
 			"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -1069,6 +1131,15 @@
 			"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
 			"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/@types/chai-as-promised": {
+			"version": "8.0.1",
+			"resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-8.0.1.tgz",
+			"integrity": "sha512-dAlDhLjJlABwAVYObo9TPWYTRg9NaQM5CXeaeJYcYAkvzUf0JRLIiog88ao2Wqy/20WUnhbbUZcgvngEbJ3YXQ==",
+			"dev": true,
+			"dependencies": {
+				"@types/chai": "*"
+			}
+		},
 		"node_modules/@types/chai-subset": {
 		"node_modules/@types/chai-subset": {
 			"version": "1.3.5",
 			"version": "1.3.5",
 			"resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz",
 			"resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz",
@@ -1078,6 +1149,12 @@
 				"@types/chai": "*"
 				"@types/chai": "*"
 			}
 			}
 		},
 		},
+		"node_modules/@types/istanbul-lib-coverage": {
+			"version": "2.0.6",
+			"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+			"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+			"dev": true
+		},
 		"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",
@@ -1097,10 +1174,13 @@
 			"dev": true
 			"dev": true
 		},
 		},
 		"node_modules/@types/node": {
 		"node_modules/@types/node": {
-			"version": "18.18.3",
-			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz",
-			"integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==",
-			"dev": true
+			"version": "18.19.75",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz",
+			"integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==",
+			"dev": true,
+			"dependencies": {
+				"undici-types": "~5.26.4"
+			}
 		},
 		},
 		"node_modules/@types/prismjs": {
 		"node_modules/@types/prismjs": {
 			"version": "1.16.8",
 			"version": "1.16.8",
@@ -1441,13 +1521,15 @@
 			"integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg=="
 			"integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg=="
 		},
 		},
 		"node_modules/@vscode/test-cli": {
 		"node_modules/@vscode/test-cli": {
-			"version": "0.0.4",
-			"resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.4.tgz",
-			"integrity": "sha512-Tx0tfbxeSb2Xlo+jpd+GJrNLgKQHobhRHrYvOipZRZQYWZ82sKiK02VY09UjU1Czc/YnZnqyAnjUfaVGl3h09w==",
+			"version": "0.0.10",
+			"resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz",
+			"integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==",
 			"dev": true,
 			"dev": true,
 			"dependencies": {
 			"dependencies": {
 				"@types/mocha": "^10.0.2",
 				"@types/mocha": "^10.0.2",
+				"c8": "^9.1.0",
 				"chokidar": "^3.5.3",
 				"chokidar": "^3.5.3",
+				"enhanced-resolve": "^5.15.0",
 				"glob": "^10.3.10",
 				"glob": "^10.3.10",
 				"minimatch": "^9.0.3",
 				"minimatch": "^9.0.3",
 				"mocha": "^10.2.0",
 				"mocha": "^10.2.0",
@@ -1456,6 +1538,9 @@
 			},
 			},
 			"bin": {
 			"bin": {
 				"vscode-test": "out/bin.mjs"
 				"vscode-test": "out/bin.mjs"
+			},
+			"engines": {
+				"node": ">=18"
 			}
 			}
 		},
 		},
 		"node_modules/@vscode/test-cli/node_modules/ansi-regex": {
 		"node_modules/@vscode/test-cli/node_modules/ansi-regex": {
@@ -2162,6 +2247,116 @@
 				"node": ">=0.10.0"
 				"node": ">=0.10.0"
 			}
 			}
 		},
 		},
+		"node_modules/c8": {
+			"version": "9.1.0",
+			"resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz",
+			"integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==",
+			"dev": true,
+			"dependencies": {
+				"@bcoe/v8-coverage": "^0.2.3",
+				"@istanbuljs/schema": "^0.1.3",
+				"find-up": "^5.0.0",
+				"foreground-child": "^3.1.1",
+				"istanbul-lib-coverage": "^3.2.0",
+				"istanbul-lib-report": "^3.0.1",
+				"istanbul-reports": "^3.1.6",
+				"test-exclude": "^6.0.0",
+				"v8-to-istanbul": "^9.0.0",
+				"yargs": "^17.7.2",
+				"yargs-parser": "^21.1.1"
+			},
+			"bin": {
+				"c8": "bin/c8.js"
+			},
+			"engines": {
+				"node": ">=14.14.0"
+			}
+		},
+		"node_modules/c8/node_modules/ansi-regex": {
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+			"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/c8/node_modules/cliui": {
+			"version": "8.0.1",
+			"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+			"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+			"dev": true,
+			"dependencies": {
+				"string-width": "^4.2.0",
+				"strip-ansi": "^6.0.1",
+				"wrap-ansi": "^7.0.0"
+			},
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/c8/node_modules/is-fullwidth-code-point": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+			"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/c8/node_modules/string-width": {
+			"version": "4.2.3",
+			"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+			"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+			"dev": true,
+			"dependencies": {
+				"emoji-regex": "^8.0.0",
+				"is-fullwidth-code-point": "^3.0.0",
+				"strip-ansi": "^6.0.1"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/c8/node_modules/strip-ansi": {
+			"version": "6.0.1",
+			"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+			"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+			"dev": true,
+			"dependencies": {
+				"ansi-regex": "^5.0.1"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/c8/node_modules/yargs": {
+			"version": "17.7.2",
+			"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+			"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+			"dev": true,
+			"dependencies": {
+				"cliui": "^8.0.1",
+				"escalade": "^3.1.1",
+				"get-caller-file": "^2.0.5",
+				"require-directory": "^2.1.1",
+				"string-width": "^4.2.3",
+				"y18n": "^5.0.5",
+				"yargs-parser": "^21.1.1"
+			},
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/c8/node_modules/yargs-parser": {
+			"version": "21.1.1",
+			"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+			"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+			"dev": true,
+			"engines": {
+				"node": ">=12"
+			}
+		},
 		"node_modules/call-bind": {
 		"node_modules/call-bind": {
 			"version": "1.0.7",
 			"version": "1.0.7",
 			"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
 			"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@@ -2220,6 +2415,27 @@
 				"node": ">=4"
 				"node": ">=4"
 			}
 			}
 		},
 		},
+		"node_modules/chai-as-promised": {
+			"version": "8.0.1",
+			"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.1.tgz",
+			"integrity": "sha512-OIEJtOL8xxJSH8JJWbIoRjybbzR52iFuDHuF8eb+nTPD6tgXLjRqsgnUGqQfFODxYvq5QdirT0pN9dZ0+Gz6rA==",
+			"dev": true,
+			"dependencies": {
+				"check-error": "^2.0.0"
+			},
+			"peerDependencies": {
+				"chai": ">= 2.1.2 < 6"
+			}
+		},
+		"node_modules/chai-as-promised/node_modules/check-error": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+			"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+			"dev": true,
+			"engines": {
+				"node": ">= 16"
+			}
+		},
 		"node_modules/chai-subset": {
 		"node_modules/chai-subset": {
 			"version": "1.6.0",
 			"version": "1.6.0",
 			"resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz",
 			"resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz",
@@ -2463,6 +2679,12 @@
 			"dev": true,
 			"dev": true,
 			"optional": true
 			"optional": true
 		},
 		},
+		"node_modules/convert-source-map": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+			"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+			"dev": true
+		},
 		"node_modules/core-util-is": {
 		"node_modules/core-util-is": {
 			"version": "1.0.3",
 			"version": "1.0.3",
 			"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
 			"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -2771,6 +2993,19 @@
 				"once": "^1.4.0"
 				"once": "^1.4.0"
 			}
 			}
 		},
 		},
+		"node_modules/enhanced-resolve": {
+			"version": "5.18.1",
+			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
+			"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+			"dev": true,
+			"dependencies": {
+				"graceful-fs": "^4.2.4",
+				"tapable": "^2.2.0"
+			},
+			"engines": {
+				"node": ">=10.13.0"
+			}
+		},
 		"node_modules/entities": {
 		"node_modules/entities": {
 			"version": "2.2.0",
 			"version": "2.2.0",
 			"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
 			"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
@@ -3569,6 +3804,12 @@
 				"url": "https://github.com/sponsors/ljharb"
 				"url": "https://github.com/sponsors/ljharb"
 			}
 			}
 		},
 		},
+		"node_modules/graceful-fs": {
+			"version": "4.2.11",
+			"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+			"dev": true
+		},
 		"node_modules/graphemer": {
 		"node_modules/graphemer": {
 			"version": "1.4.0",
 			"version": "1.4.0",
 			"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
 			"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -3660,6 +3901,12 @@
 				"node": ">=10"
 				"node": ">=10"
 			}
 			}
 		},
 		},
+		"node_modules/html-escaper": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+			"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+			"dev": true
+		},
 		"node_modules/htmlparser2": {
 		"node_modules/htmlparser2": {
 			"version": "6.1.0",
 			"version": "6.1.0",
 			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
 			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@@ -3914,6 +4161,63 @@
 			"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
 			"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/istanbul-lib-coverage": {
+			"version": "3.2.2",
+			"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+			"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/istanbul-lib-report": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+			"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+			"dev": true,
+			"dependencies": {
+				"istanbul-lib-coverage": "^3.0.0",
+				"make-dir": "^4.0.0",
+				"supports-color": "^7.1.0"
+			},
+			"engines": {
+				"node": ">=10"
+			}
+		},
+		"node_modules/istanbul-lib-report/node_modules/has-flag": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+			"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/istanbul-lib-report/node_modules/supports-color": {
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+			"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+			"dev": true,
+			"dependencies": {
+				"has-flag": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/istanbul-reports": {
+			"version": "3.1.7",
+			"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+			"integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+			"dev": true,
+			"dependencies": {
+				"html-escaper": "^2.0.0",
+				"istanbul-lib-report": "^3.0.0"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
 		"node_modules/jackspeak": {
 		"node_modules/jackspeak": {
 			"version": "2.3.6",
 			"version": "2.3.6",
 			"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
 			"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@@ -4054,6 +4358,12 @@
 				"setimmediate": "^1.0.5"
 				"setimmediate": "^1.0.5"
 			}
 			}
 		},
 		},
+		"node_modules/just-extend": {
+			"version": "6.2.0",
+			"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
+			"integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==",
+			"dev": true
+		},
 		"node_modules/jwa": {
 		"node_modules/jwa": {
 			"version": "2.0.0",
 			"version": "2.0.0",
 			"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
@@ -4151,6 +4461,13 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 			}
 		},
 		},
+		"node_modules/lodash.get": {
+			"version": "4.4.2",
+			"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+			"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+			"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
+			"dev": true
+		},
 		"node_modules/lodash.includes": {
 		"node_modules/lodash.includes": {
 			"version": "4.3.0",
 			"version": "4.3.0",
 			"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
 			"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -4305,6 +4622,33 @@
 				"node": ">=10"
 				"node": ">=10"
 			}
 			}
 		},
 		},
+		"node_modules/make-dir": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+			"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+			"dev": true,
+			"dependencies": {
+				"semver": "^7.5.3"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/make-dir/node_modules/semver": {
+			"version": "7.7.1",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+			"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+			"dev": true,
+			"bin": {
+				"semver": "bin/semver.js"
+			},
+			"engines": {
+				"node": ">=10"
+			}
+		},
 		"node_modules/make-error": {
 		"node_modules/make-error": {
 			"version": "1.3.6",
 			"version": "1.3.6",
 			"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
 			"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -4672,6 +5016,19 @@
 			"resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz",
 			"resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz",
 			"integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g="
 			"integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g="
 		},
 		},
+		"node_modules/nise": {
+			"version": "6.1.1",
+			"resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz",
+			"integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==",
+			"dev": true,
+			"dependencies": {
+				"@sinonjs/commons": "^3.0.1",
+				"@sinonjs/fake-timers": "^13.0.1",
+				"@sinonjs/text-encoding": "^0.7.3",
+				"just-extend": "^6.2.0",
+				"path-to-regexp": "^8.1.0"
+			}
+		},
 		"node_modules/node-abi": {
 		"node_modules/node-abi": {
 			"version": "2.30.1",
 			"version": "2.30.1",
 			"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz",
 			"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz",
@@ -4928,6 +5285,15 @@
 				"node": "14 || >=16.14"
 				"node": "14 || >=16.14"
 			}
 			}
 		},
 		},
+		"node_modules/path-to-regexp": {
+			"version": "8.2.0",
+			"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+			"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+			"dev": true,
+			"engines": {
+				"node": ">=16"
+			}
+		},
 		"node_modules/path-type": {
 		"node_modules/path-type": {
 			"version": "4.0.0",
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -5381,6 +5747,54 @@
 				"simple-concat": "^1.0.0"
 				"simple-concat": "^1.0.0"
 			}
 			}
 		},
 		},
+		"node_modules/sinon": {
+			"version": "19.0.2",
+			"resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz",
+			"integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==",
+			"dev": true,
+			"dependencies": {
+				"@sinonjs/commons": "^3.0.1",
+				"@sinonjs/fake-timers": "^13.0.2",
+				"@sinonjs/samsam": "^8.0.1",
+				"diff": "^7.0.0",
+				"nise": "^6.1.1",
+				"supports-color": "^7.2.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/sinon"
+			}
+		},
+		"node_modules/sinon/node_modules/diff": {
+			"version": "7.0.0",
+			"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+			"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+			"dev": true,
+			"engines": {
+				"node": ">=0.3.1"
+			}
+		},
+		"node_modules/sinon/node_modules/has-flag": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+			"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/sinon/node_modules/supports-color": {
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+			"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+			"dev": true,
+			"dependencies": {
+				"has-flag": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
 		"node_modules/slash": {
 		"node_modules/slash": {
 			"version": "3.0.0",
 			"version": "3.0.0",
 			"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -5560,6 +5974,15 @@
 				"node": ">=4"
 				"node": ">=4"
 			}
 			}
 		},
 		},
+		"node_modules/tapable": {
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+			"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+			"dev": true,
+			"engines": {
+				"node": ">=6"
+			}
+		},
 		"node_modules/tar-fs": {
 		"node_modules/tar-fs": {
 			"version": "2.1.1",
 			"version": "2.1.1",
 			"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
 			"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
@@ -5616,6 +6039,20 @@
 				"node": ">=0.10"
 				"node": ">=0.10"
 			}
 			}
 		},
 		},
+		"node_modules/test-exclude": {
+			"version": "6.0.0",
+			"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+			"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+			"dev": true,
+			"dependencies": {
+				"@istanbuljs/schema": "^0.1.2",
+				"glob": "^7.1.4",
+				"minimatch": "^3.0.4"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
 		"node_modules/text-table": {
 		"node_modules/text-table": {
 			"version": "0.2.0",
 			"version": "0.2.0",
 			"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
 			"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -5847,6 +6284,12 @@
 			"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
 			"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/undici-types": {
+			"version": "5.26.5",
+			"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+			"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+			"dev": true
+		},
 		"node_modules/uri-js": {
 		"node_modules/uri-js": {
 			"version": "4.4.1",
 			"version": "4.4.1",
 			"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
 			"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -5883,6 +6326,30 @@
 			"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
 			"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/v8-to-istanbul": {
+			"version": "9.3.0",
+			"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+			"integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+			"dev": true,
+			"dependencies": {
+				"@jridgewell/trace-mapping": "^0.3.12",
+				"@types/istanbul-lib-coverage": "^2.0.1",
+				"convert-source-map": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=10.12.0"
+			}
+		},
+		"node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": {
+			"version": "0.3.25",
+			"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+			"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+			"dev": true,
+			"dependencies": {
+				"@jridgewell/resolve-uri": "^3.1.0",
+				"@jridgewell/sourcemap-codec": "^1.4.14"
+			}
+		},
 		"node_modules/vscode-jsonrpc": {
 		"node_modules/vscode-jsonrpc": {
 			"version": "6.0.0",
 			"version": "6.0.0",
 			"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz",

+ 7 - 4
package.json

@@ -871,24 +871,27 @@
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
 		"@types/chai": "^4.3.11",
 		"@types/chai": "^4.3.11",
+		"@types/chai-as-promised": "^8.0.1",
 		"@types/chai-subset": "^1.3.5",
 		"@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.19.75",
 		"@types/prismjs": "^1.16.8",
 		"@types/prismjs": "^1.16.8",
 		"@types/vscode": "^1.96.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",
 		"@typescript-eslint/parser": "^5.57.1",
 		"@typescript-eslint/parser": "^5.57.1",
-		"@vscode/test-cli": "^0.0.4",
+		"@vscode/test-cli": "^0.0.10",
 		"@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.5.0",
+		"chai-as-promised": "^8.0.1",
 		"chai-subset": "^1.6.0",
 		"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.8.2",
+		"sinon": "^19.0.2",
 		"ts-node": "^10.9.1",
 		"ts-node": "^10.9.1",
 		"tsconfig-paths": "^4.2.0",
 		"tsconfig-paths": "^4.2.0",
 		"tslint": "^5.20.1",
 		"tslint": "^5.20.1",

+ 1 - 1
src/debugger/debug_runtime.ts

@@ -46,7 +46,7 @@ export interface GodotVariable {
 	scope_path?: string;
 	scope_path?: string;
 	sub_values?: GodotVariable[];
 	sub_values?: GodotVariable[];
 	value: any;
 	value: any;
-	type?: bigint;
+	type?: number;
 	id?: bigint;
 	id?: bigint;
 }
 }
 
 

+ 94 - 94
src/debugger/debugger.ts

@@ -25,6 +25,9 @@ import { GodotDebugSession as Godot4DebugSession } from "./godot4/debug_session"
 import { register_command, set_context, createLogger, get_project_version } from "../utils";
 import { register_command, set_context, createLogger, get_project_version } from "../utils";
 import { SceneTreeProvider, SceneNode } from "./scene_tree_provider";
 import { SceneTreeProvider, SceneNode } from "./scene_tree_provider";
 import { InspectorProvider, RemoteProperty } from "./inspector_provider";
 import { InspectorProvider, RemoteProperty } from "./inspector_provider";
+import { GodotVariable, RawObject } from "./debug_runtime";
+import { GodotObject, GodotObjectPromise } from "./godot4/variables/godot_object_promise";
+import { InvalidatedEvent } from "@vscode/debugadapter";
 
 
 const log = createLogger("debugger", { output: "Godot Debugger" });
 const log = createLogger("debugger", { output: "Godot Debugger" });
 
 
@@ -256,38 +259,34 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
 		}
 		}
 	}
 	}
 
 
-	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 async inspect_node(element: SceneNode | RemoteProperty) {
+		await this.fill_provider_tree(element.label, BigInt(element.object_id));
 	}
 	}
 
 
-	public refresh_scene_tree() {
-		this.session?.controller.request_scene_tree();
+	private create_godot_variable(godot_object: GodotObject): GodotVariable {
+		return {
+			value: {
+				type_name: function() { return godot_object.type; },
+				stringify_value: function() { return `<${godot_object.godot_id}>`; },
+				sub_values: function() {return godot_object.sub_values; },
+			},
+		} as GodotVariable;
 	}
 	}
 
 
-	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));
+	private async fill_provider_tree(label: string, godot_id: bigint, force_refresh = false) {
+		if (this.session instanceof Godot4DebugSession) {
+			const godot_object = await this.session.variables_manager.get_godot_object(BigInt(godot_id), force_refresh);
+			const va = this.create_godot_variable(godot_object);
+			this.inspectorProvider.fill_tree(label, godot_object.type, Number(godot_object.godot_id), va);
+		} else {
+			this.session?.controller.request_inspect_object(BigInt(godot_id));
 			this.session?.inspect_callbacks.set(
 			this.session?.inspect_callbacks.set(
-				BigInt(id),
+				BigInt(godot_id),
 				(class_name, variable) => {
 				(class_name, variable) => {
 					this.inspectorProvider.fill_tree(
 					this.inspectorProvider.fill_tree(
-						name,
+						label,
 						class_name,
 						class_name,
-						id,
+						Number(godot_id),
 						variable
 						variable
 					);
 					);
 				},
 				},
@@ -295,82 +294,83 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
 		}
 		}
 	}
 	}
 
 
-	public edit_value(property: RemoteProperty) {
+	public refresh_scene_tree() {
+		this.session?.controller.request_scene_tree();
+	}
+
+	public async refresh_inspector() {
+		if (this.inspectorProvider.has_tree()) {
+			const label = this.inspectorProvider.get_top_name();
+			const id = this.inspectorProvider.get_top_id();
+			await this.fill_provider_tree(label, BigInt(id), /*force_refresh*/ true);
+		}
+	}
+
+	public async edit_value(property: RemoteProperty) {
 		const previous_value = property.value;
 		const previous_value = property.value;
 		const type = typeof previous_value;
 		const type = typeof previous_value;
 		const is_float = type === "number" && !Number.isInteger(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 = Number.parseFloat(value);
-							if (Number.isNaN(new_parsed_value)) {
-								return;
-							}
-						} else {
-							new_parsed_value = Number.parseInt(value);
-							if (Number.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 value = await window.showInputBox({ value: `${property.description}` });
+		let new_parsed_value: any;
+		switch (type) {
+			case "string":
+				new_parsed_value = value;
+				break;
+			case "number":
+				if (is_float) {
+					new_parsed_value = Number.parseFloat(value);
+					if (Number.isNaN(new_parsed_value)) {
+						return;
 					}
 					}
-					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 {
 				} else {
-					this.session?.controller.set_object_property(
-						BigInt(property.object_id),
-						property.label,
-						new_parsed_value,
-					);
+					new_parsed_value = Number.parseInt(value);
+					if (Number.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 label = this.inspectorProvider.get_top_name();
+		const godot_id = BigInt(this.inspectorProvider.get_top_id());
 
 
-				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
-						);
-					},
-				);
-			});
+		await this.fill_provider_tree(label, godot_id, /*force_refresh*/ true);
+		// const res = await debug.activeDebugSession?.customRequest("refreshVariables"); // refresh vscode.debug variables
+		this.session.sendEvent(new InvalidatedEvent(["variables"]));
+		console.log("foo");
 	}
 	}
 }
 }

+ 71 - 346
src/debugger/godot4/debug_session.ts

@@ -9,36 +9,25 @@ import {
 import { DebugProtocol } from "@vscode/debugprotocol";
 import { DebugProtocol } from "@vscode/debugprotocol";
 import { Subject } from "await-notify";
 import { Subject } from "await-notify";
 import * as fs from "node:fs";
 import * as fs from "node:fs";
-import { debug } from "vscode";
 
 
 import { createLogger } from "../../utils";
 import { createLogger } from "../../utils";
-import { GodotDebugData, GodotStackVars, GodotVariable } from "../debug_runtime";
+import { GodotDebugData } from "../debug_runtime";
 import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
 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 { ServerController } from "./server_controller";
 import { ServerController } from "./server_controller";
-import { ObjectId, RawObject } from "./variables/variants";
+import { VariablesManager } from "./variables/variables_manager";
 
 
 const log = createLogger("debugger.session", { output: "Godot Debugger" });
 const log = createLogger("debugger.session", { output: "Godot Debugger" });
 
 
-interface Variable {
-	variable: GodotVariable;
-	index: number;
-	object_id: number;
-}
-
 export class GodotDebugSession extends LoggingDebugSession {
 export class GodotDebugSession extends LoggingDebugSession {
-	private all_scopes: GodotVariable[];
 	public controller = new ServerController(this);
 	public controller = new ServerController(this);
 	public debug_data = new GodotDebugData(this);
 	public debug_data = new GodotDebugData(this);
 	public sceneTree: SceneTreeProvider;
 	public sceneTree: SceneTreeProvider;
 	private exception = false;
 	private exception = false;
-	private got_scope: Subject = new Subject();
-	private ongoing_inspections: bigint[] = [];
-	private previous_inspections: bigint[] = [];
 	private configuration_done: Subject = new Subject();
 	private configuration_done: Subject = new Subject();
 	private mode: "launch" | "attach" | "" = "";
 	private mode: "launch" | "attach" | "" = "";
-	public inspect_callbacks: Map<bigint, (class_name: string, variable: GodotVariable) => void> = new Map();
+
+	public variables_manager: VariablesManager;
 
 
 	public constructor() {
 	public constructor() {
 		super();
 		super();
@@ -126,34 +115,6 @@ export class GodotDebugSession extends LoggingDebugSession {
 		}
 		}
 	}
 	}
 
 
-	protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
-		log.info("evaluateRequest", args);
-		await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
-
-		if (this.all_scopes) {
-			try {
-				const variable = this.get_variable(args.expression, null, null, null);
-				const parsed_variable = parse_variable(variable.variable);
-				response.body = {
-					result: parsed_variable.value,
-					variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0,
-				};
-			} catch (error) {
-				response.success = false;
-				response.message = error.toString();
-			}
-		}
-
-		if (!response.body) {
-			response.body = {
-				result: "null",
-				variablesReference: 0,
-			};
-		}
-
-		this.sendResponse(response);
-	}
-
 	protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
 	protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
 		log.info("nextRequest", args);
 		log.info("nextRequest", args);
 		if (!this.exception) {
 		if (!this.exception) {
@@ -170,21 +131,6 @@ export class GodotDebugSession extends LoggingDebugSession {
 		}
 		}
 	}
 	}
 
 
-	protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
-		log.info("scopesRequest", args);
-		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(
 	protected setBreakPointsRequest(
 		response: DebugProtocol.SetBreakpointsResponse,
 		response: DebugProtocol.SetBreakpointsResponse,
 		args: DebugProtocol.SetBreakpointsArguments,
 		args: DebugProtocol.SetBreakpointsArguments,
@@ -225,25 +171,6 @@ export class GodotDebugSession extends LoggingDebugSession {
 		}
 		}
 	}
 	}
 
 
-	protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
-		log.info("stackTraceRequest", args);
-		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) {
 	protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
 		log.info("stepInRequest", args);
 		log.info("stepInRequest", args);
 		if (!this.exception) {
 		if (!this.exception) {
@@ -272,299 +199,97 @@ export class GodotDebugSession extends LoggingDebugSession {
 	protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
 	protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
 		log.info("threadsRequest");
 		log.info("threadsRequest");
 		response.body = { threads: [new Thread(0, "thread_1")] };
 		response.body = { threads: [new Thread(0, "thread_1")] };
+		log.info("threadsRequest response", response);
 		this.sendResponse(response);
 		this.sendResponse(response);
 	}
 	}
 
 
-	protected async variablesRequest(
-		response: DebugProtocol.VariablesResponse,
-		args: DebugProtocol.VariablesArguments,
-	) {
-		log.info("variablesRequest", args);
-		if (!this.all_scopes) {
+	protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
+		log.info("stackTraceRequest", args);
+		if (this.debug_data.last_frame) {
 			response.body = {
 			response.body = {
-				variables: [],
+				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);
-			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,
-		};
-
+		log.info("stackTraceRequest response", response);
 		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: "@",
-			},
-		];
-
-		for (const va of stackVars.locals) {
-			va.scope_path = "@.local";
-			this.append_variable(va);
-		}
+	protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
+		log.info("scopesRequest", args);
+		// this.variables_manager.variablesFrameId = args.frameId;
 
 
-		for (const va of stackVars.members) {
-			va.scope_path = "@.member";
-			this.append_variable(va);
-		}
+		// TODO: create scopes dynamically for a given frame
+		const vscode_scope_ids = this.variables_manager.get_or_create_frame_scopes(args.frameId);
+		const scopes_with_references =  [
+      {name: "Locals", variablesReference: vscode_scope_ids.Locals, expensive: false},
+			{name: "Members", variablesReference: vscode_scope_ids.Members, expensive: false},
+			{name: "Globals", variablesReference: vscode_scope_ids.Globals, expensive: false},
+    ];
 
 
-		for (const va of stackVars.globals) {
-			va.scope_path = "@.global";
-			this.append_variable(va);
-		}
+		response.body = {
+			scopes: scopes_with_references
+			// scopes: [
+			// 	{ name: "Locals", variablesReference: 1, expensive: false },
+			// 	{ name: "Members", variablesReference: 2, expensive: false },
+			// 	{ name: "Globals", variablesReference: 3, expensive: false },
+			// ],
+		};
 
 
-		this.add_to_inspections();
-
-		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.got_scope.notify();
-		}
+		log.info("scopesRequest response", response);
+		this.sendResponse(response);
 	}
 	}
 
 
-	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);
-
-		for (const va of variables) {
-			const index = this.all_scopes.findIndex((va_id) => va_id === va);
-			if (index < 0) {
-				continue;
-			}
-			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.scope_path = old[0].scope_path;
-			this.append_variable(replacement, index);
-		}
-
-		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.add_to_inspections();
+	protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments) {
+		log.info("variablesRequest", args);
+		try {
+			const variables = await this.variables_manager.get_vscode_object(args.variablesReference);
 
 
-		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.got_scope.notify();
+			response.body = {
+				variables: variables,
+			};
+		} catch (error) {
+			log.error("variablesRequest", error);
+			response.success = false;
+			response.message = error.toString();
 		}
 		}
-	}
 
 
-	private add_to_inspections() {
-		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);
-			}
-		}
+		log.info("variablesRequest response", response);
+		this.sendResponse(response);
 	}
 	}
 
 
-	protected get_variable(
-		expression: string,
-		root: GodotVariable = null,
-		index = 0,
-		object_id: number = null,
-	): Variable {
-		let result: Variable = {
-			variable: null,
-			index: null,
-			object_id: 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;
-		}
-
-		const items = expression.split(".");
-		let propertyName = items[index + 1];
-		let 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
-		let 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("");
-		}
-
-		const sanitized_all_scopes = this.all_scopes
-			.filter((x) => x)
-			.map((x) => ({
-				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) {
-			throw new Error(`Could not find: ${propertyName}`);
-		}
-
-		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) {
-				const collection = path.split(".")[path.split(".").length - 1];
-				const 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 {
-				const item = Array.from(root.value.entries()).find(
-					(x) => x && x[0].split("Members/").join("").split("Locals/").join("") === propertyName,
-				);
-				result.object_id = item?.[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,
-		);
+	protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
+		log.info("evaluateRequest", args);
 
 
-		if (items.length > 2 && index < items.length - 2) {
-			result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id);
+		try {
+			const parsed_variable = await this.variables_manager.get_vscode_variable_by_name(args.expression, args.frameId);
+			response.body = {
+				result: parsed_variable.value,
+				variablesReference: parsed_variable.variablesReference
+			};
+		} catch (error) {
+			response.success = false;
+			response.message = error.toString();
+			response.body = {
+				result: "null",
+				variablesReference: 0,
+			};
 		}
 		}
 
 
-		return result;
+		log.info("evaluateRequest response", response);
+		this.sendResponse(response);
 	}
 	}
 
 
-	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);
-			});
-		}
+	public set_exception(exception: boolean) {
+		this.exception = true;
 	}
 	}
 }
 }

+ 9 - 71
src/debugger/godot4/helpers.ts

@@ -1,5 +1,6 @@
 import { GodotVariable, } from "../debug_runtime";
 import { GodotVariable, } from "../debug_runtime";
 import { SceneNode } from "../scene_tree_provider";
 import { SceneNode } from "../scene_tree_provider";
+import { ObjectId } from "./variables/variants";
 
 
 export function parse_next_scene_node(params: any[], ofs: { offset: number } = { offset: 0 }): SceneNode {
 export function parse_next_scene_node(params: any[], ofs: { offset: number } = { offset: 0 }): SceneNode {
 	const childCount: number = params[ofs.offset++];
 	const childCount: number = params[ofs.offset++];
@@ -31,12 +32,7 @@ export function split_buffers(buffer: Buffer) {
 	return buffers;
 	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 get_sub_values(value: any) {
+export function get_sub_values(value: any): GodotVariable[] {
 	let subValues: GodotVariable[] = undefined;
 	let subValues: GodotVariable[] = undefined;
 
 
 	if (value) {
 	if (value) {
@@ -45,19 +41,12 @@ export function get_sub_values(value: any) {
 				return { name: `${i}`, value: va } as GodotVariable;
 				return { name: `${i}`, value: va } as GodotVariable;
 			});
 			});
 		} else if (value instanceof Map) {
 		} 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;
-				}
-			});
+			subValues = [];
+			for (const [key, val] of value.entries()) {
+				const name = typeof key["stringify_value"] === "function" ? `${key.type_name()}${key.stringify_value()}` : `${key}`;
+				const godot_id = val instanceof ObjectId ? val.id : undefined;
+				subValues.push({id: godot_id, name, value: val } as GodotVariable);
+			}
 		} else if (typeof value["sub_values"] === "function") {
 		} else if (typeof value["sub_values"] === "function") {
 			subValues = value.sub_values()?.map((sva) => {
 			subValues = value.sub_values()?.map((sva) => {
 				return { name: sva.name, value: sva.value } as GodotVariable;
 				return { name: sva.name, value: sva.value } as GodotVariable;
@@ -70,55 +59,4 @@ export function get_sub_values(value: any) {
 	}
 	}
 
 
 	return subValues;
 	return subValues;
-}
-
-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 {
-			try {
-				rendered_value = `${value.type_name()}${value.stringify_value()}`;
-			} catch (e) {
-				rendered_value = `${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,
-	};
-}
+}

+ 103 - 46
src/debugger/godot4/server_controller.ts

@@ -16,13 +16,14 @@ import {
 } from "../../utils";
 } from "../../utils";
 import { prompt_for_godot_executable } from "../../utils/prompts";
 import { prompt_for_godot_executable } from "../../utils/prompts";
 import { killSubProcesses, subProcess } from "../../utils/subspawn";
 import { killSubProcesses, subProcess } from "../../utils/subspawn";
-import { GodotStackFrame, GodotStackVars, GodotVariable } from "../debug_runtime";
+import { GodotStackFrame, 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 { get_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";
+import { VariablesManager } from "./variables/variables_manager";
 
 
 const log = createLogger("debugger.controller", { output: "Godot Debugger" });
 const log = createLogger("debugger.controller", { output: "Godot Debugger" });
 const socketLog = createLogger("debugger.socket");
 const socketLog = createLogger("debugger.socket");
@@ -35,6 +36,33 @@ class Command {
 	public threadId: number = 0;
 	public threadId: number = 0;
 }
 }
 
 
+class GodotPartialStackVars {
+	Locals: GodotVariable[] = [];
+	Members: GodotVariable[] = [];
+	Globals: GodotVariable [] = [];
+	public remaining: number;
+	public stack_frame_id: number;
+	constructor(stack_frame_id: number) {
+		this.stack_frame_id = stack_frame_id;
+	}
+
+	public reset(remaining: number) {
+		this.remaining = remaining;
+		this.Locals = [];
+		this.Members = [];
+		this.Globals = [];
+	}
+
+	public append(name: string, godotScopeIndex: 0|1|2, type: number, value: any, sub_values?: GodotVariable[]) {
+		const scopeName = ["Locals", "Members", "Globals"][godotScopeIndex];
+		const scope = this[scopeName];
+		// const objectId = value instanceof ObjectId ? value : undefined; // won't work, unless the value is re-created through new ObjectId(godot_id)
+		const godot_id = type === 24 ? value.id : undefined;
+		scope.push({ id: godot_id, name, value, type, sub_values } as GodotVariable);
+		this.remaining--;
+	}
+}
+
 export class ServerController {
 export class ServerController {
 	private commandBuffer: Buffer[] = [];
 	private commandBuffer: Buffer[] = [];
 	private encoder = new VariantEncoder();
 	private encoder = new VariantEncoder();
@@ -46,7 +74,7 @@ export class ServerController {
 	private socket?: net.Socket;
 	private socket?: net.Socket;
 	private steppingOut = false;
 	private steppingOut = false;
 	private didFirstOutput = false;
 	private didFirstOutput = false;
-	private partialStackVars = new GodotStackVars();
+	private partialStackVars: GodotPartialStackVars;
 	private connectedVersion = "";
 	private connectedVersion = "";
 
 
 	public constructor(public session: GodotDebugSession) {}
 	public constructor(public session: GodotDebugSession) {}
@@ -93,8 +121,16 @@ export class ServerController {
 		this.send_command("get_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 request_stack_frame_vars(stack_frame_id: number) {
+		if (this.partialStackVars !== undefined) {
+			log.warn("Partial stack frames have been requested, while existing request hasn't been completed yet." +
+							`Remaining stack_frames: ${this.partialStackVars.remaining}` +
+							`Current stack_frame_id: ${this.partialStackVars.stack_frame_id}` + 
+							`Requested stack_frame_id: ${stack_frame_id}`
+						);
+		}
+		this.partialStackVars = new GodotPartialStackVars(stack_frame_id);
+		this.send_command("get_stack_frame_vars", [stack_frame_id]);
 	}
 	}
 
 
 	public set_object_property(objectId: bigint, label: string, newParsedValue) {
 	public set_object_property(objectId: bigint, label: string, newParsedValue) {
@@ -259,7 +295,7 @@ export class ServerController {
 				return;
 				return;
 			}
 			}
 
 
-			socketLog.debug("rx:", data[0]);
+			socketLog.debug("rx:", data[0], data[0][2]);
 			const command = this.parse_message(data[0]);
 			const command = this.parse_message(data[0]);
 			this.handle_command(command);
 			this.handle_command(command);
 		}
 		}
@@ -362,9 +398,11 @@ export class ServerController {
 					this.set_exception("");
 					this.set_exception("");
 				}
 				}
 				this.request_stack_dump();
 				this.request_stack_dump();
+				this.session.variables_manager = new VariablesManager(this);
 				break;
 				break;
 			}
 			}
 			case "debug_exit":
 			case "debug_exit":
+				this.session.variables_manager = undefined;
 				break;
 				break;
 			case "message:click_ctrl":
 			case "message:click_ctrl":
 				// TODO: what is this?
 				// TODO: what is this?
@@ -381,14 +419,14 @@ export class ServerController {
 				break;
 				break;
 			}
 			}
 			case "scene:inspect_object": {
 			case "scene:inspect_object": {
-				let id = BigInt(command.parameters[0]);
+				let godot_id = BigInt(command.parameters[0]);
 				const className: string = command.parameters[1];
 				const className: string = command.parameters[1];
 				const properties: string[] = command.parameters[2];
 				const properties: string[] = command.parameters[2];
 
 
 				// message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer,
 				// message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer,
 				// thus we need to convert it to its equivalent unsigned value here.
 				// thus we need to convert it to its equivalent unsigned value here.
-				if (id < 0) {
-					id = id + BigInt(2) ** BigInt(64);
+				if (godot_id < 0) {
+					godot_id = godot_id + BigInt(2) ** BigInt(64);
 				}
 				}
 
 
 				const rawObject = new RawObject(className);
 				const rawObject = new RawObject(className);
@@ -396,14 +434,19 @@ export class ServerController {
 					rawObject.set(prop[0], prop[5]);
 					rawObject.set(prop[0], prop[5]);
 				}
 				}
 				const sub_values = get_sub_values(rawObject);
 				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));
+
+				// race condition here:
+				// 0. DebuggerStop1 happens
+				// 1. the DA may have sent the "inspect_object" message
+				// 2. the vscode hit "continue"
+				// 3. new breakpoint hit, DebuggerStop2 happens
+				// 4. the godot server will return response for `1.` with "scene:inspect_object"
+				// at this moment there is no way to tell if "scene:inspect_object" is for DebuggerStop1 or DebuggerStop2
+				try {
+					this.session.variables_manager?.resolve_variable(godot_id, className, sub_values);
+				} catch (error) {
+					log.error("Race condition error error in scene:inspect_object", error);
 				}
 				}
-				this.session.set_inspection(id, rawObject, sub_values);
 				break;
 				break;
 			}
 			}
 			case "stack_dump": {
 			case "stack_dump": {
@@ -423,17 +466,53 @@ export class ServerController {
 				break;
 				break;
 			}
 			}
 			case "stack_frame_vars": {
 			case "stack_frame_vars": {
-				this.partialStackVars.reset(command.parameters[0]);
-				this.session.set_scopes(this.partialStackVars);
+				/** first response to {@link request_stack_frame_vars} */
+				if (this.partialStackVars !== undefined) {
+					log.warn("'stack_frame_vars' received again from godot engine before all partial 'stack_frame_var' are received");
+				}
+				const remaining = command.parameters[0];
+				// init this.partialStackVars, which will be filled with "stack_frame_var" responses data
+				this.partialStackVars.reset(remaining);
 				break;
 				break;
 			}
 			}
 			case "stack_frame_var": {
 			case "stack_frame_var": {
-				this.do_stack_frame_var(
-					command.parameters[0],
-					command.parameters[1],
-					command.parameters[2],
-					command.parameters[3],
-				);
+				if (this.partialStackVars === undefined) {
+					log.error("Unexpected 'stack_frame_var' received. Should have received 'stack_frame_vars' first.");
+					return;
+				}
+				if (typeof command.parameters[0] !== "string") {
+					log.error("Unexpected parameter type for 'stack_frame_var'. Expected string for name, got " + typeof command.parameters[0]);
+					return;
+				}
+				if (typeof command.parameters[1] !== "number" || command.parameters[1] !== 0 && command.parameters[1] !== 1 && command.parameters[1] !== 2) {
+					log.error("Unexpected parameter type for 'stack_frame_var'. Expected number for scope, got " + typeof command.parameters[1]);
+					return;
+				}
+				if (typeof command.parameters[2] !== "number") {
+					log.error("Unexpected parameter type for 'stack_frame_var'. Expected number for type, got " + typeof command.parameters[2]);
+					return;
+				}
+				var name: string = command.parameters[0];
+				var scope: 0 | 1 | 2 = command.parameters[1]; // 0 = locals, 1 = members, 2 = globals
+				var type: number = command.parameters[2];
+				var value: any = command.parameters[3];
+				var subValues: GodotVariable[] = get_sub_values(value);
+				this.partialStackVars.append(name, scope, type, value, subValues);
+
+				if (this.partialStackVars.remaining === 0) {
+					const stackVars = this.partialStackVars;
+					this.partialStackVars = undefined;
+					log.info("All partial 'stack_frame_var' are received.");
+					// godot server doesn't send the frame_id for the stack_vars, assume the remembered stack_frame_id:
+					const frame_id = BigInt(stackVars.stack_frame_id);
+					const local_scopes_godot_id = -frame_id*3n-1n;
+					const member_scopes_godot_id = -frame_id*3n-2n;
+					const global_scopes_godot_id = -frame_id*3n-3n;
+	
+					this.session.variables_manager.resolve_variable(local_scopes_godot_id, "Locals", stackVars.Locals);
+					this.session.variables_manager.resolve_variable(member_scopes_godot_id, "Members", stackVars.Members);
+					this.session.variables_manager.resolve_variable(global_scopes_godot_id, "Globals", stackVars.Globals);
+				}
 				break;
 				break;
 			}
 			}
 			case "output": {
 			case "output": {
@@ -616,7 +695,7 @@ export class ServerController {
 			commandArray.push(this.threadId);
 			commandArray.push(this.threadId);
 		}
 		}
 		commandArray.push(parameters ?? []);
 		commandArray.push(parameters ?? []);
-		socketLog.debug("tx:", commandArray);
+		socketLog.debug("tx:", commandArray, commandArray[2]);
 		const buffer = this.encoder.encode_variant(commandArray);
 		const buffer = this.encoder.encode_variant(commandArray);
 		this.commandBuffer.push(buffer);
 		this.commandBuffer.push(buffer);
 		this.send_buffer();
 		this.send_buffer();
@@ -632,26 +711,4 @@ export class ServerController {
 			this.draining = !this.socket.write(command);
 			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 sub_values = get_sub_values(value);
-		const variable = { name, value, type, sub_values } as GodotVariable;
-
-		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);
-		}
-	}
 }
 }

+ 127 - 69
src/debugger/godot4/debugger_variables.test.ts → src/debugger/godot4/variables/debugger_variables.test.ts

@@ -4,6 +4,12 @@ import * as vscode from "vscode";
 import { DebugProtocol } from "@vscode/debugprotocol";
 import { DebugProtocol } from "@vscode/debugprotocol";
 import chai from "chai";
 import chai from "chai";
 import chaiSubset from "chai-subset";
 import chaiSubset from "chai-subset";
+var chaiAsPromised = import("chai-as-promised");
+// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
+
+chaiAsPromised.then((module) => {
+  chai.use(module.default);
+});
 
 
 import { promisify } from "util";
 import { promisify } from "util";
 import { execFile } from "child_process";
 import { execFile } from "child_process";
@@ -83,19 +89,31 @@ async function waitForBreakpoint(breakpoint: vscode.SourceBreakpoint, timeoutMs:
 }
 }
 
 
 enum VariableScope {
 enum VariableScope {
-  Locals  = 1,
-  Members = 2,
-  Globals = 3
+  Locals,
+  Members,
+  Globals
 }
 }
 
 
-async function getVariablesForScope(scope: VariableScope): Promise<DebugProtocol.Variable[]> {
+async function getVariablesForVSCodeID(vscode_id: number): Promise<DebugProtocol.Variable[]> {
   // corresponds to file://./debug_session.ts protected async variablesRequest
   // corresponds to file://./debug_session.ts protected async variablesRequest
   const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
   const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
-    variablesReference: scope
+    variablesReference: vscode_id
   });
   });
   return variablesResponse?.variables || [];
   return variablesResponse?.variables || [];
 }
 }
 
 
+async function getVariablesForScope(scope: VariableScope, stack_frame_id: number = 0): Promise<DebugProtocol.Variable[]> {
+  const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: stack_frame_id});
+  const scope_name = VariableScope[scope];
+  const scope_res = res_scopes.scopes.find(s => s.name == scope_name);
+  if (scope_res === undefined) {
+    throw new Error(`No ${scope_name} scope found in responce from "scopes" request`);
+  }
+  const vscode_id = scope_res.variablesReference;
+  const variables  = await getVariablesForVSCodeID(vscode_id);
+  return variables;
+}
+
 async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise<any> {
 async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise<any> {
   // corresponds to file://./debug_session.ts protected async evaluateRequest
   // corresponds to file://./debug_session.ts protected async evaluateRequest
   const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest("evaluate", {
   const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest("evaluate", {
@@ -118,6 +136,31 @@ function formatMessage(this: Mocha.Context, msg: string): string {
 
 
 var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
 var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
 
 
+
+declare global {
+  // eslint-disable-next-line @typescript-eslint/no-namespace
+  namespace Chai {
+    interface Assertion {
+      unique: Assertion;
+    }
+  }
+}
+
+chai.Assertion.addProperty("unique", function() {
+  const actual = this._obj; // The object being tested
+  if (!Array.isArray(actual)) {
+    throw new chai.AssertionError("Expected value to be an array");
+  }
+  const uniqueArray = [...new Set(actual)];
+  this.assert(
+    actual.length === uniqueArray.length,
+    "expected #{this} to contain only unique elements",
+    "expected #{this} to not contain only unique elements",
+    uniqueArray,
+    actual
+  );
+});
+
 async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn"): Promise<void> {
 async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn"): Promise<void> {
   const t0 = performance.now();
   const t0 = performance.now();
   const debugConfig: vscode.DebugConfiguration = {
   const debugConfig: vscode.DebugConfiguration = {
@@ -138,7 +181,10 @@ async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "
 
 
 suite("DAP Integration Tests - Variable Scopes", () => {
 suite("DAP Integration Tests - Variable Scopes", () => {
   // workspaceFolder should match `.vscode-test.js`::workspaceFolder
   // workspaceFolder should match `.vscode-test.js`::workspaceFolder
-  const workspaceFolder = path.resolve(__dirname, "../../../test_projects/test-dap-project-godot4");
+  const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
+  if (!workspaceFolder || !workspaceFolder.endsWith("test-dap-project-godot4")) {
+    throw new Error(`workspaceFolder should contain 'test-dap-project-godot4' project, got: ${workspaceFolder}`);
+  }
 
 
   suiteSetup(async function() {
   suiteSetup(async function() {
     this.timeout(20000); // enough time to do `godot --import`
     this.timeout(20000); // enough time to do `godot --import`
@@ -149,11 +195,11 @@ suite("DAP Integration Tests - Variable Scopes", () => {
 
 
     // init the godot project by importing it in godot engine:
     // init the godot project by importing it in godot engine:
     const config = vscode.workspace.getConfiguration("godotTools");
     const config = vscode.workspace.getConfiguration("godotTools");
+    // config.update("editorPath.godot4", "godot4", vscode.ConfigurationTarget.Workspace);
     var godot4_path = config.get<string>("editorPath.godot4");
     var godot4_path = config.get<string>("editorPath.godot4");
     // get the path for currently opened project in vscode test instance:
     // 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});
+    console.log("Executing", [godot4_path, "--headless", "--import", workspaceFolder]);
+    const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", workspaceFolder], {shell: true, cwd: workspaceFolder});
     if (exec_res.stderr !== "") {
     if (exec_res.stderr !== "") {
       throw new Error(exec_res.stderr);
       throw new Error(exec_res.stderr);
     }
     }
@@ -172,24 +218,25 @@ suite("DAP Integration Tests - Variable Scopes", () => {
 
 
 
 
   teardown(async function() {
   teardown(async function() {
+    this.timeout(3000);
     await sleep(1000);
     await sleep(1000);
     if (vscode.debug.activeDebugSession !== undefined) {
     if (vscode.debug.activeDebugSession !== undefined) {
       console.log("Closing debug session");
       console.log("Closing debug session");
       await vscode.debug.stopDebugging();
       await vscode.debug.stopDebugging();
+      await sleep(1000);
     }
     }
     console.log(`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`);
     console.log(`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`);
   });
   });
 
 
+  // test("sample test", async function() {
+  //   expect(true).to.equal(true);
+  //   expect([1,2,3]).to.be.unique;
+  //   expect([1,1]).not.to.be.unique;
+  // });
 
 
-  test("sample test", async function() {
-    // await sleep(1000);
-    await startDebugging("ScopeVars.tscn");
-  });
-
-  
   test("should return correct scopes", async function() {
   test("should return correct scopes", async function() {
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
-    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::ClassFoo::test_function"]);
     vscode.debug.addBreakpoints([breakpoint]);
     vscode.debug.addBreakpoints([breakpoint]);
 
 
     await startDebugging("ScopeVars.tscn");
     await startDebugging("ScopeVars.tscn");
@@ -200,23 +247,40 @@ suite("DAP Integration Tests - Variable Scopes", () => {
     await sleep(2000);
     await sleep(2000);
 
 
     // corresponds to file://./debug_session.ts async scopesRequest
     // corresponds to file://./debug_session.ts async scopesRequest
-    const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: 1});
+    const stack_scopes_map: Map<number, {
+      "Locals": number;
+      "Members": number; 
+      "Globals": number;
+    }> = new Map();
+    for (var stack_frame_id = 0; stack_frame_id < 3; stack_frame_id++) {
+      const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: stack_frame_id});
+      expect(res_scopes).to.exist;
+      expect(res_scopes.scopes).to.exist;
+      expect(res_scopes.scopes.length).to.equal(3, "Expected 3 scopes");
+      expect(res_scopes.scopes[0].name).to.equal(VariableScope[VariableScope.Locals], "Expected Locals scope");
+      expect(res_scopes.scopes[1].name).to.equal(VariableScope[VariableScope.Members], "Expected Members scope");
+      expect(res_scopes.scopes[2].name).to.equal(VariableScope[VariableScope.Globals], "Expected Globals scope");
+      const vscode_ids = res_scopes.scopes.map(s => s.variablesReference);
+      expect(vscode_ids, "VSCode IDs should be unique for each scope").to.be.unique;
+      stack_scopes_map[stack_frame_id] = {
+        "Locals": vscode_ids[0],
+        "Members": vscode_ids[1], 
+        "Globals": vscode_ids[2]
+      };
+    }
 
 
-    expect(res_scopes).to.exist;
-    expect(res_scopes.scopes).to.exist;
+    const all_scopes_vscode_ids = Array.from(stack_scopes_map.values()).flatMap(s => Object.values(s));
+    expect(all_scopes_vscode_ids, "All scopes should be unique").to.be.unique;
 
 
-    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");
+    const vars_frame0_locals = await getVariablesForVSCodeID(stack_scopes_map[0].Locals);
+    expect(vars_frame0_locals).to.containSubset([{name: "str_var", value: "ScopeVars::ClassFoo::test_function::local::str_var"}]);
 
 
-    await sleep(1000);
-    await vscode.debug.stopDebugging();
-  })?.timeout(5000);
+    const vars_frame1_locals = await getVariablesForVSCodeID(stack_scopes_map[1].Locals);
+    expect(vars_frame1_locals).to.containSubset([{name: "str_var", value: "ScopeVars::test::local::str_var"}]);
+
+    const vars_frame2_locals = await getVariablesForVSCodeID(stack_scopes_map[2].Locals);
+    expect(vars_frame2_locals).to.containSubset([{name: "str_var", value: "ScopeVars::_ready::local::str_var"}]);
+  })?.timeout(10000);
 
 
   test("should return global variables", async function() {
   test("should return global variables", async function() {
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
@@ -232,12 +296,10 @@ suite("DAP Integration Tests - Variable Scopes", () => {
 
 
     const variables = await getVariablesForScope(VariableScope.Globals);
     const variables = await getVariablesForScope(VariableScope.Globals);
     expect(variables).to.containSubset([{name: "GlobalScript"}]);
     expect(variables).to.containSubset([{name: "GlobalScript"}]);
-    
-    await sleep(1000);
-    await vscode.debug.stopDebugging();
-  })?.timeout(7000);
+  })?.timeout(10000);
 
 
-  test("should return local variables", async function() {
+  test("should return all local variables", async function() {
+    /** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
     const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
     const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
     vscode.debug.addBreakpoints([breakpoint]);
     vscode.debug.addBreakpoints([breakpoint]);
@@ -251,14 +313,12 @@ suite("DAP Integration Tests - Variable Scopes", () => {
 
 
     const variables = await getVariablesForScope(VariableScope.Locals);
     const variables = await getVariablesForScope(VariableScope.Locals);
     expect(variables.length).to.equal(2);
     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);
+    expect(variables).to.containSubset([{name: "str_var"}]);
+    expect(variables).to.containSubset([{name: "self_var"}]);
+  })?.timeout(10000);
 
 
-  test("should return member variables", async function() {
+  test("should return all member variables", async function() {
+    /** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
     const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
     const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
     vscode.debug.addBreakpoints([breakpoint]);
     vscode.debug.addBreakpoints([breakpoint]);
@@ -271,13 +331,12 @@ suite("DAP Integration Tests - Variable Scopes", () => {
     await sleep(2000);
     await sleep(2000);
 
 
     const variables = await getVariablesForScope(VariableScope.Members);
     const variables = await getVariablesForScope(VariableScope.Members);
-    expect(variables.length).to.equal(2);
+    expect(variables.length).to.equal(4);
     expect(variables).to.containSubset([{name: "self"}]);
     expect(variables).to.containSubset([{name: "self"}]);
     expect(variables).to.containSubset([{name: "member1"}]);
     expect(variables).to.containSubset([{name: "member1"}]);
-
-    await sleep(1000);
-    await vscode.debug.stopDebugging();
-  })?.timeout(5000);
+    expect(variables).to.containSubset([{name: "str_var", value: "ScopeVars::member::str_var"}]);
+    expect(variables).to.containSubset([{name: "str_var_member_only", value: "ScopeVars::member::str_var_member_only"}]);
+  })?.timeout(10000);
 
 
   test("should retrieve all built-in types correctly", async function() {
   test("should retrieve all built-in types correctly", async function() {
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
@@ -302,15 +361,12 @@ suite("DAP Integration Tests - Variable Scopes", () => {
     expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]);
     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: "rect2", value: "Rect2((0, 0) - (100, 50))" }]);
     expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]);
     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: "simple_array", value: "(3) [1, 2, 3]" }]);
     // expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]);
     // 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.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: "nested_dict", value: "Dictionary(2)" }]);
+    expect(variables).to.containSubset([{ name: "byte_array", value: "(4) [0, 1, 2, 255]" }]);
+    expect(variables).to.containSubset([{ name: "int32_array", value: "(3) [100, 200, 300]" }]);
     expect(variables).to.containSubset([{ name: "color_var", value: "Color(1, 0, 0, 1)" }]);
     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: "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: "plane_var", value: "Plane(0, 1, 0, -5)" }]);
@@ -318,10 +374,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
     expect(variables).to.containSubset([{ name: "signal_var" }]);
     expect(variables).to.containSubset([{ name: "signal_var" }]);
     const signal_var = variables.find(v => v.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>)'");
     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);
+  })?.timeout(10000);
 
 
   test("should retrieve all complex variables correctly", async function() {
   test("should retrieve all complex variables correctly", async function() {
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
     const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
@@ -337,23 +390,28 @@ suite("DAP Integration Tests - Variable Scopes", () => {
 
 
     const memberVariables = await getVariablesForScope(VariableScope.Members);
     const memberVariables = await getVariablesForScope(VariableScope.Members);
     
     
-    expect(memberVariables.length).to.equal(3);
+    expect(memberVariables.length).to.equal(3, "Incorrect member variables count");
     expect(memberVariables).to.containSubset([{name: "self"}]);
     expect(memberVariables).to.containSubset([{name: "self"}]);
     expect(memberVariables).to.containSubset([{name: "self_var"}]);
     expect(memberVariables).to.containSubset([{name: "self_var"}]);
+    expect(memberVariables).to.containSubset([{name: "label"}]);
     const self = memberVariables.find(v => v.name === "self");
     const self = memberVariables.find(v => v.name === "self");
     const self_var = memberVariables.find(v => v.name === "self_var");
     const self_var = memberVariables.find(v => v.name === "self_var");
     expect(self.value).to.deep.equal(self_var.value);
     expect(self.value).to.deep.equal(self_var.value);
     
     
     const localVariables = await getVariablesForScope(VariableScope.Locals);
     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();
+    const expectedLocalVariables = [
+      { name: "local_label", value: /Label<\d+>/ },
+      { name: "local_self_var_through_label", value: /Node2D<\d+>/ },
+      { name: "local_classA", value: /RefCounted<\d+>/ },
+      { name: "local_classB", value: /RefCounted<\d+>/ },
+      { name: "str_var", value: /^ExtensiveVars::_ready::local::str_var$/ },
+    ];
+    expect(localVariables.length).to.equal(expectedLocalVariables.length, "Incorrect local variables count");
+    expect(localVariables).to.containSubset(expectedLocalVariables.map(v => ({ name: v.name })));
+    for (const expectedLocalVariable of expectedLocalVariables) {
+      const localVariable = localVariables.find(v => v.name === expectedLocalVariable.name);
+      expect(localVariable).to.exist;
+      expect(localVariable.value).to.match(expectedLocalVariable.value, `Variable '${expectedLocalVariable.name}' has incorrect value'`);
+    }
   })?.timeout(15000);
   })?.timeout(15000);
 });
 });

+ 58 - 0
src/debugger/godot4/variables/godot_id_to_vscode_id_mapper.test.ts

@@ -0,0 +1,58 @@
+import { expect } from "chai";
+import { GodotIdWithPath, GodotIdToVscodeIdMapper } from "./godot_id_to_vscode_id_mapper";
+
+suite("GodotIdToVscodeIdMapper", () => {
+  test("create_vscode_id assigns unique ID", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    const godotId = new GodotIdWithPath(BigInt(1), ["path1"]);
+    const vscodeId = mapper.create_vscode_id(godotId);
+    expect(vscodeId).to.equal(1);
+  });
+
+  test("create_vscode_id throws error on duplicate", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    const godotId = new GodotIdWithPath(BigInt(1), ["path1"]);
+    mapper.create_vscode_id(godotId);
+    expect(() => mapper.create_vscode_id(godotId)).to.throw("Duplicate godot_id: 1:path1");
+  });
+
+  test("get_godot_id_with_path returns correct object", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    const godotId = new GodotIdWithPath(BigInt(2), ["path2"]);
+    const vscodeId = mapper.create_vscode_id(godotId);
+    expect(mapper.get_godot_id_with_path(vscodeId)).to.deep.equal(godotId);
+  });
+
+  test("get_godot_id_with_path throws error if not found", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    expect(() => mapper.get_godot_id_with_path(999)).to.throw("Unknown vscode_id: 999");
+  });
+
+  test("get_vscode_id retrieves correct ID", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    const godotId = new GodotIdWithPath(BigInt(3), ["path3"]);
+    const vscodeId = mapper.create_vscode_id(godotId);
+    expect(mapper.get_vscode_id(godotId)).to.equal(vscodeId);
+  });
+
+  test("get_vscode_id throws error if not found", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    const godotId = new GodotIdWithPath(BigInt(4), ["path4"]);
+    expect(() => mapper.get_vscode_id(godotId)).to.throw("Unknown godot_id_with_path: 4:path4");
+  });
+
+  test("get_or_create_vscode_id creates new ID if not found", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    const godotId = new GodotIdWithPath(BigInt(5), ["path5"]);
+    const vscodeId = mapper.get_or_create_vscode_id(godotId);
+    expect(vscodeId).to.equal(1);
+  });
+
+  test("get_or_create_vscode_id retrieves existing ID if already created", () => {
+    const mapper = new GodotIdToVscodeIdMapper();
+    const godotId = new GodotIdWithPath(BigInt(6), ["path6"]);
+    const vscodeId1 = mapper.get_or_create_vscode_id(godotId);
+    const vscodeId2 = mapper.get_or_create_vscode_id(godotId);
+    expect(vscodeId1).to.equal(vscodeId2);
+  });
+});

+ 67 - 0
src/debugger/godot4/variables/godot_id_to_vscode_id_mapper.ts

@@ -0,0 +1,67 @@
+export class GodotIdWithPath {
+  constructor(public godot_id: bigint, public path: string[] = []) {
+  }
+
+  toString(): string {
+    return `${this.godot_id.toString()}:${this.path.join("/")}`;
+  }
+}
+
+type GodotIdWithPathString = string;
+
+export class GodotIdToVscodeIdMapper {
+  // Maps `godot_id` to `vscode_id` and back.
+  // Each `vscode_id` corresponds to expandable variable in vscode UI.
+  // Each `godot_id` corresponds to object in godot server.
+  // `vscode_id` maps 1:1 with [`godot_id`, path_to_variable_inside_godot_object].
+  // For example, if godot_object with id 12345 looks like: { SomeDict: { SomeField: [1,2,3] } },
+  //   then `vscode_id` for the 'SomeField' will map to [12345, ["SomeDict", "SomeField"]] in order to allow expansion of SomeField in the vscode UI.
+  // Note: `vscode_id` is a number and `godot_id` is a bigint.
+  
+  private godot_to_vscode: Map<GodotIdWithPathString, number>; // use GodotIdWithPathString, since JS Map treats GodotIdWithPath only by reference
+  private vscode_to_godot: Map<number, GodotIdWithPath>;
+  private next_vscode_id: number;
+
+  constructor() {
+    this.godot_to_vscode = new Map<GodotIdWithPathString, number>();
+    this.vscode_to_godot = new Map<number, GodotIdWithPath>();
+    this.next_vscode_id = 1;
+  }
+
+  // Creates `vscode_id` for a given `godot_id` and path
+  create_vscode_id(godot_id_with_path: GodotIdWithPath): number {
+    const godot_id_with_path_str = godot_id_with_path.toString();
+    if (this.godot_to_vscode.has(godot_id_with_path_str)) {
+      throw new Error(`Duplicate godot_id: ${godot_id_with_path_str}`);
+    }
+
+    const vscode_id = this.next_vscode_id++;
+    this.godot_to_vscode.set(godot_id_with_path_str, vscode_id);
+    this.vscode_to_godot.set(vscode_id, godot_id_with_path);
+    return vscode_id;
+  }
+
+  get_godot_id_with_path(vscode_id: number): GodotIdWithPath {
+    const godot_id_with_path = this.vscode_to_godot.get(vscode_id);
+    if (godot_id_with_path === undefined) {
+      throw new Error(`Unknown vscode_id: ${vscode_id}`);
+    }
+    return godot_id_with_path;
+  }
+
+  get_vscode_id(godot_id_with_path: GodotIdWithPath, fail_if_not_found = true): number | undefined {
+    const vscode_id = this.godot_to_vscode.get(godot_id_with_path.toString());
+    if (fail_if_not_found && vscode_id === undefined) {
+      throw new Error(`Unknown godot_id_with_path: ${godot_id_with_path}`);
+    }
+    return vscode_id;
+  }
+
+  get_or_create_vscode_id(godot_id_with_path: GodotIdWithPath): number {
+    let vscode_id = this.get_vscode_id(godot_id_with_path, false);
+    if (vscode_id === undefined) {
+      vscode_id = this.create_vscode_id(godot_id_with_path);
+    }
+    return vscode_id;
+  }
+}

+ 79 - 0
src/debugger/godot4/variables/godot_object_promise.test.ts

@@ -0,0 +1,79 @@
+import sinon from "sinon";
+import chai from "chai";
+import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
+// import chaiAsPromised from "chai-as-promised";
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+var chaiAsPromised = import("chai-as-promised");
+// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
+
+chaiAsPromised.then((module) => {
+  chai.use(module.default);
+});
+const { expect } = chai;
+
+
+suite("GodotObjectPromise", () => {
+  let clock;
+
+  setup(() => {
+    clock = sinon.useFakeTimers(); // Use Sinon to control time
+  });
+
+  teardown(() => {
+    clock.restore(); // Restore the real timers after each test
+  });
+
+  test("resolves successfully with a valid GodotObject", async () => {
+    const godotObject: GodotObject = {
+      godot_id: BigInt(1),
+      type: "TestType",
+      sub_values: []
+    };
+
+    const promise = new GodotObjectPromise();
+    setTimeout(() => promise.resolve(godotObject), 10);
+    clock.tick(10); // Fast-forward time
+    await expect(promise.promise).to.eventually.equal(godotObject);
+  });
+
+  test("rejects with an error when explicitly called", async () => {
+    const promise = new GodotObjectPromise();
+    const error = new Error("Test rejection");
+    setTimeout(() => promise.reject(error), 10);
+    clock.tick(10); // Fast-forward time
+    await expect(promise.promise).to.be.rejectedWith("Test rejection");
+  });
+
+  test("rejects due to timeout", async () => {
+    const promise = new GodotObjectPromise(50);
+    clock.tick(50); // Fast-forward time
+    await expect(promise.promise).to.be.rejectedWith("GodotObjectPromise timed out");
+  });
+
+  test("does not reject if resolved before timeout", async () => {
+    const godotObject: GodotObject = {
+      godot_id: BigInt(2),
+      type: "AnotherTestType",
+      sub_values: []
+    };
+
+    const promise = new GodotObjectPromise(100);
+    setTimeout(() => promise.resolve(godotObject), 10);
+    clock.tick(10); // Fast-forward time
+    await expect(promise.promise).to.eventually.equal(godotObject);
+  });
+
+  test("clears timeout when resolved", async () => {
+    const promise = new GodotObjectPromise(1000);
+    promise.resolve({ godot_id: BigInt(3), type: "ResolvedType", sub_values: [] });
+    clock.tick(1000); // Fast-forward time
+    await expect(promise.promise).to.eventually.be.fulfilled;
+  });
+
+  test("clears timeout when rejected", async () => {
+    const promise = new GodotObjectPromise(1000);
+    promise.reject(new Error("Rejected"));
+    clock.tick(1000); // Fast-forward time
+    await expect(promise.promise).to.be.rejectedWith("Rejected");
+  });
+});

+ 52 - 0
src/debugger/godot4/variables/godot_object_promise.ts

@@ -0,0 +1,52 @@
+import { GodotVariable } from "../../debug_runtime";
+
+export interface GodotObject {
+  godot_id: bigint;
+  type: string;
+  sub_values: GodotVariable[];
+}
+
+/**
+ * A promise that resolves to a {@link GodotObject}.
+ *
+ * This promise is used to handle the asynchronous nature of requesting a Godot object.
+ * It is used as a placeholder until the actual object is received.
+ *
+ * When the object is received from the server, the promise is resolved with the object.
+ * If the object is not received within a certain time, the promise is rejected with an error.
+ */
+export class GodotObjectPromise {
+  private _resolve!: (value: GodotObject | PromiseLike<GodotObject>) => void;
+  private _reject!: (reason?: any) => void;
+  public promise: Promise<GodotObject>;
+  private timeoutId?: NodeJS.Timeout;
+
+  constructor(timeoutMs?: number) {
+    this.promise = new Promise<GodotObject>((resolve_arg, reject_arg) => {
+      this._resolve = resolve_arg;
+      this._reject = reject_arg;
+
+      if (timeoutMs !== undefined) {
+        this.timeoutId = setTimeout(() => {
+          reject_arg(new Error("GodotObjectPromise timed out"));
+        }, timeoutMs);
+      }
+    });
+  }
+
+  async resolve(value: GodotObject) {
+    if (this.timeoutId) {
+      clearTimeout(this.timeoutId);
+      this.timeoutId = undefined;
+    }
+    await this._resolve(value);
+  }
+
+  async reject(reason: Error) {
+    if (this.timeoutId) {
+      clearTimeout(this.timeoutId);
+      this.timeoutId = undefined;
+    }
+    await this._reject(reason);
+  }
+}

+ 240 - 0
src/debugger/godot4/variables/variables_manager.ts

@@ -0,0 +1,240 @@
+import { DebugProtocol } from "@vscode/debugprotocol";
+import { ServerController } from "../server_controller";
+import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
+import { GodotVariable } from "../../debug_runtime";
+import { ObjectId } from "./variants";
+import { GodotIdToVscodeIdMapper, GodotIdWithPath } from "./godot_id_to_vscode_id_mapper";
+
+export interface VsCodeScopeIDs {
+  Locals: number;
+  Members: number;
+  Globals: number;
+}
+
+export class VariablesManager {
+  constructor(public controller: ServerController) {
+  }
+  
+  public godot_object_promises: Map<bigint, GodotObjectPromise>= new Map();
+	public godot_id_to_vscode_id_mapper = new GodotIdToVscodeIdMapper();
+	
+	// variablesFrameId: number;
+
+  private frame_id_to_scopes_map: Map<number, VsCodeScopeIDs> = new Map();
+
+  /**
+   * Returns Locals, Members, and Globals vscode_ids
+   * @param stack_frame_id the id of the stack frame
+   * @returns an object with Locals, Members, and Globals vscode_ids
+   */
+  public get_or_create_frame_scopes(stack_frame_id: number): VsCodeScopeIDs {
+    var scopes = this.frame_id_to_scopes_map.get(stack_frame_id);
+    if (scopes === undefined) {
+      const frame_id = BigInt(stack_frame_id);
+      scopes = {} as VsCodeScopeIDs;
+      scopes.Locals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(-frame_id*3n-1n, []));
+      scopes.Members = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(-frame_id*3n-2n, []));
+      scopes.Globals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(-frame_id*3n-3n, []));
+      this.frame_id_to_scopes_map.set(stack_frame_id, scopes);
+    }
+
+    return scopes;
+	}
+
+  /**
+   * Retrieves a Godot object from the cache or godot debug server
+   * @param godot_id the id of the object
+   * @returns a promise that resolves to the requested object
+   */
+  public async get_godot_object(godot_id: bigint, force_refresh = false) {
+    if (force_refresh) {
+      // delete the object
+      this.godot_object_promises.delete(godot_id);
+
+      // check if member scopes also need to be refreshed:
+      for (const [stack_frame_id, scopes] of this.frame_id_to_scopes_map) {
+        const members_godot_id = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(scopes.Members);
+        const scopes_object = await this.get_godot_object(members_godot_id.godot_id);
+        const self = scopes_object.sub_values.find((sv) => sv.name === "self");
+        if (self !== undefined && self.value instanceof ObjectId) {
+          if (self.value.id === godot_id) {
+            this.godot_object_promises.delete(members_godot_id.godot_id); // force refresh the member scope
+          }
+        }
+      }
+    }
+    var variable_promise = this.godot_object_promises.get(godot_id);
+    if (variable_promise === undefined) {
+      // variable not found, request one
+      if (godot_id < 0) {
+        // special case for scopes, which have godot_id below 0. see @this.get_or_create_frame_scopes
+        // all 3 scopes for current stackFrameId are retrieved at the same time, aka [-1,-2-,3], [-4,-5,-6], etc..
+        // init corresponding promises
+        const requested_stack_frame_id = (-godot_id-1n)/3n;
+        // this.variablesFrameId will be undefined when the debugger just stopped at breakpoint:
+        // evaluateRequest is called before scopesRequest
+        const local_scopes_godot_id = -requested_stack_frame_id*3n-1n;
+        const member_scopes_godot_id = -requested_stack_frame_id*3n-2n;
+        const global_scopes_godot_id = -requested_stack_frame_id*3n-3n;
+        this.godot_object_promises.set(local_scopes_godot_id, new GodotObjectPromise());
+        this.godot_object_promises.set(member_scopes_godot_id, new GodotObjectPromise());
+        this.godot_object_promises.set(global_scopes_godot_id, new GodotObjectPromise());
+        variable_promise = this.godot_object_promises.get(godot_id);
+        // request stack vars from godot server, which will resolve variable promises 1,2 & 3
+        // see file://../server_controller.ts 'case "stack_frame_vars":'
+        this.controller.request_stack_frame_vars(Number(requested_stack_frame_id));
+      } else {
+        this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(godot_id, []));
+        variable_promise = new GodotObjectPromise();
+        this.godot_object_promises.set(godot_id, variable_promise);
+        // request the object from godot server. Once godot server responds, the controller will resolve the variable_promise
+        this.controller.request_inspect_object(godot_id);
+      }
+    }
+    const godot_object = await variable_promise.promise;
+
+    return godot_object;
+  }
+
+  public async get_vscode_object(vscode_id: number): Promise<DebugProtocol.Variable[]> {
+    const godot_id_with_path = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id);
+    if (godot_id_with_path === undefined) {
+      throw new Error(`Unknown variablesReference ${vscode_id}`);
+    }
+    const godot_object = await this.get_godot_object(godot_id_with_path.godot_id);
+    if (godot_object === undefined) {
+      throw new Error(`Cannot retrieve path '${godot_id_with_path.toString()}'. Godot object with id ${godot_id_with_path.godot_id} not found.`);
+    }
+
+    let sub_values: GodotVariable[] = godot_object.sub_values;
+
+    // if the path is specified, walk the godot_object using it to access the requested variable:
+    for (const [idx, path] of godot_id_with_path.path.entries()) {
+      const sub_val = sub_values.find((sv) => sv.name === path);
+      if (sub_val === undefined) {
+        throw new Error(`Cannot retrieve path '${godot_id_with_path.toString()}'. Following subpath not found: '${godot_id_with_path.path.slice(0, idx+1).join("/")}'.`);
+      }
+      sub_values = sub_val.sub_values;				
+    }
+
+    const variables: DebugProtocol.Variable[] = [];
+    for (const va of sub_values) {
+      const godot_id_with_path_sub = va.id !== undefined ? new GodotIdWithPath(va.id, []) : undefined;
+      const vscode_id = godot_id_with_path_sub !== undefined ? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(godot_id_with_path_sub) : 0;
+      const variable: DebugProtocol.Variable = await this.parse_variable(va, vscode_id, godot_id_with_path.godot_id, godot_id_with_path.path, this.godot_id_to_vscode_id_mapper);
+      variables.push(variable);
+    }
+
+    return variables;
+  }
+
+  public async get_vscode_variable_by_name(variable_name: string, stack_frame_id: number): Promise<DebugProtocol.Variable> {
+    let variable: GodotVariable;
+
+    const variable_names = variable_name.split(".");
+
+    for (var i = 0; i < variable_names.length; i++) {
+      if (i === 0) {
+        // find the first part of variable_name in scopes. Locals first, then Members, then Globals
+        const vscode_scope_ids = this.get_or_create_frame_scopes(stack_frame_id);
+        const vscode_ids = [vscode_scope_ids.Locals, vscode_scope_ids.Members, vscode_scope_ids.Globals];
+        const godot_ids = vscode_ids.map(vscode_id => this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id))
+                                    .map(godot_id_with_path => godot_id_with_path.godot_id);
+        for (var godot_id of godot_ids) {
+          // check each scope for requested variable
+          const scope = await this.get_godot_object(godot_id);
+          variable = scope.sub_values.find((sv) => sv.name === variable_names[0]);
+          if (variable !== undefined) {
+            break;
+          }
+        }
+      } else {
+        // just look up the subpath using the current variable
+        if (variable.value instanceof ObjectId) {
+          const godot_object = await this.get_godot_object(variable.value.id);
+          variable = godot_object.sub_values.find((sv) => sv.name === variable_names[i]);
+        } else {
+          variable = variable.sub_values.find((sv) => sv.name === variable_names[i]);
+        }
+      }
+      if (variable === undefined) {
+        throw new Error(`Cannot retrieve path '${variable_name}'. Following subpath not found: '${variable_names.slice(0, i+1).join(".")}'`);
+      }
+    }
+
+    const parsed_variable = await this.parse_variable(variable, undefined, godot_id, [], this.godot_id_to_vscode_id_mapper);
+    if (parsed_variable.variablesReference === undefined) {
+      const objectId = variable.value instanceof ObjectId ? variable.value : undefined;
+      const vscode_id = objectId !== undefined ? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(objectId.id, [])) : 0;
+      parsed_variable.variablesReference = vscode_id;
+    }
+
+    return parsed_variable;
+  }
+
+  private async parse_variable(va: GodotVariable, vscode_id?: number, parent_godot_id?: bigint, relative_path?: string[], mapper?: GodotIdToVscodeIdMapper): Promise<DebugProtocol.Variable> {
+    const value = va.value;
+    let rendered_value = "";
+    let reference = 0;
+  
+    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 = `(${value.length}) [${value.slice(0, 10).join(", ")}]`;
+        reference = mapper.get_or_create_vscode_id(new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]));
+      } else if (value instanceof Map) {
+        rendered_value = value["class_name"] ?? `Dictionary(${value.size})`;
+        reference = mapper.get_or_create_vscode_id(new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]));
+      } else if (value instanceof ObjectId) {
+        if (value.id === undefined) {
+          throw new Error("Invalid godot object: instanceof ObjectId but id is undefined");
+        }
+        // Godot returns only ID for the object.
+        // In order to retrieve the class name, we need to request the object
+        const godot_object = await this.get_godot_object(value.id);
+        rendered_value = `${godot_object.type}${value.stringify_value()}`;
+        // rendered_value = `${value.type_name()}${value.stringify_value()}`;
+        reference = vscode_id;
+      }
+      else {
+        try {
+          rendered_value = `${value.type_name()}${value.stringify_value()}`;
+        } catch (e) {
+          rendered_value = `${value}`;
+        }
+        reference = mapper.get_or_create_vscode_id(new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]));
+        // reference = vsode_id ? vsode_id : 0;
+      }
+    }
+  
+    const variable: DebugProtocol.Variable = {
+      name: va.name,
+      value: rendered_value,
+      variablesReference: reference
+    };
+    
+    return variable;
+  }  
+
+  public resolve_variable(godot_id: bigint, className: string, sub_values: GodotVariable[]) {
+    const variable_promise = this.godot_object_promises.get(godot_id);
+    if (variable_promise === undefined) {
+      throw new Error(`Received 'inspect_object' for godot_id ${godot_id} but no variable promise to resolve found`);
+    }
+
+    variable_promise.resolve({godot_id: godot_id, type: className, sub_values: sub_values} as GodotObject);
+  }
+}

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

@@ -283,7 +283,7 @@ export class ObjectId implements GDObject {
 	}
 	}
 
 
 	public type_name(): string {
 	public type_name(): string {
-		return "Object";
+		return "ObjectId";
 	}
 	}
 }
 }
 
 

+ 4 - 4
src/debugger/inspector_provider.ts

@@ -24,15 +24,15 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
 		this._on_did_change_tree_data.fire(undefined);
 		this._on_did_change_tree_data.fire(undefined);
 	}
 	}
 
 
-	public getChildren(element?: RemoteProperty): ProviderResult<RemoteProperty[]> {
+	public getChildren(element?: RemoteProperty): RemoteProperty[] {
 		if (!this.tree) {
 		if (!this.tree) {
-			return Promise.resolve([]);
+			return [];
 		}
 		}
 
 
 		if (!element) {
 		if (!element) {
-			return Promise.resolve([this.tree]);
+			return [this.tree];
 		} else {
 		} else {
-			return Promise.resolve(element.properties);
+			return element.properties;
 		}
 		}
 	}
 	}
 
 

+ 4 - 4
src/debugger/scene_tree_provider.ts

@@ -28,15 +28,15 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
 		this._on_did_change_tree_data.fire(undefined);
 		this._on_did_change_tree_data.fire(undefined);
 	}
 	}
 
 
-	public getChildren(element?: SceneNode): ProviderResult<SceneNode[]> {
+	public getChildren(element?: SceneNode): SceneNode[] {
 		if (!this.tree) {
 		if (!this.tree) {
-			return Promise.resolve([]);
+			return [];
 		}
 		}
 
 
 		if (!element) {
 		if (!element) {
-			return Promise.resolve([this.tree]);
+			return [this.tree];
 		} else {
 		} else {
-			return Promise.resolve(element.children);
+			return element.children;
 		}
 		}
 	}
 	}
 
 

+ 5 - 2
test_projects/test-dap-project-godot4/BuiltInTypes.gd

@@ -17,8 +17,11 @@ func _ready() -> void:
   var simple_array = [1, 2, 3]
   var simple_array = [1, 2, 3]
   var nested_dict = {
   var nested_dict = {
       "nested_key": "Nested Value",
       "nested_key": "Nested Value",
-      "sub_dict": {"sub_key": 99}
-  }  
+      "sub_dict": {"sub_key": 99},
+  }
+  var mixed_dict = {
+    "nested_array": [1,2, {"nested_dict": [3,4,5]}]
+  }
   var byte_array = PackedByteArray([0, 1, 2, 255])
   var byte_array = PackedByteArray([0, 1, 2, 255])
   var int32_array = PackedInt32Array([100, 200, 300])
   var int32_array = PackedInt32Array([100, 200, 300])
   var color_var = Color(1, 0, 0, 1) # Red color
   var color_var = Color(1, 0, 0, 1) # Red color

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

@@ -0,0 +1 @@
+uid://bl7k8rh4vgbma

+ 27 - 4
test_projects/test-dap-project-godot4/ExtensiveVars.gd

@@ -1,11 +1,22 @@
 extends Node2D
 extends Node2D
 
 
+class_name ExtensiveVars
+
 var self_var := self
 var self_var := self
 @onready var label: ExtensiveVars_Label = $Label
 @onready var label: ExtensiveVars_Label = $Label
 
 
+# var editor_description := "ExtensiveVars::member::text overrides"
+# var rotation = 2
+
 class ClassA:
 class ClassA:
   var member_classB
   var member_classB
   var member_self := self
   var member_self := self
+  var str_var := "ExtensiveVars::ClassA::member::str_var"
+  func test_function(delta: float) -> void:
+    var str_var := "ExtensiveVars::ClassA::test_function::local::str_var"
+    var local_self := self.member_self;
+    print("breakpoint::ExtensiveVars::ClassA::test_function")
+
   
   
 class ClassB:
 class ClassB:
   var member_classA
   var member_classA
@@ -19,6 +30,8 @@ func _ready() -> void:
   local_classA.member_classB = local_classB
   local_classA.member_classB = local_classB
   local_classB.member_classA = local_classA
   local_classB.member_classA = local_classA
 
 
+  var str_var := "ExtensiveVars::_ready::local::str_var"
+
   # Circular reference.
   # Circular reference.
   # Note: that causes the godot engine to omit this variable, since stack_frame_var cannot be completed and sent
   # 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
   # https://github.com/godotengine/godot/issues/76019
@@ -28,11 +41,21 @@ func _ready() -> void:
   print("breakpoint::ExtensiveVars::_ready")
   print("breakpoint::ExtensiveVars::_ready")
 
 
 func _process(delta: float) -> void:
 func _process(delta: float) -> void:
+  var str_var := "ExtensiveVars::_process::local::str_var"
+  test(delta)
+
+func test(delta: float):
+  var str_var := "ExtensiveVars::test::local::str_var"
   var local_label := label
   var local_label := label
   var local_self_var_through_label := label.parent_var
   var local_self_var_through_label := label.parent_var
+
+  var large_dict = {}
+  for i in range(1000):
+    large_dict["variable" + str(i)] = "Some very long value, which will be in the dictionary"
   
   
-  var local_classA = ClassA.new()
-  var local_classB = ClassB.new()
-  local_classA.member_classB = local_classB
-  local_classB.member_classA = local_classA
+  var local_classA2 = ClassA.new()
+  var local_classB2 = ClassB.new()
+  local_classA2.member_classB = local_classB2
+  local_classB2.member_classA = local_classA2
+  local_classA2.test_function(delta);
   pass
   pass

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

@@ -0,0 +1 @@
+uid://jj6y8lb0lkij

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

@@ -0,0 +1 @@
+uid://ca1f5tmqgm6hu

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

@@ -0,0 +1 @@
+uid://c4ypojhmiyhhf

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

@@ -0,0 +1 @@
+uid://bxlldk7s267hd

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

@@ -5,5 +5,6 @@ extends Node2D
 
 
 # Called when the node enters the scene tree for the first time.
 # Called when the node enters the scene tree for the first time.
 func _ready() -> void:
 func _ready() -> void:
+  var local_node_1 = node_1;
   print("breakpoint::NodeVars::_ready")
   print("breakpoint::NodeVars::_ready")
   pass
   pass

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

@@ -0,0 +1 @@
+uid://ciokiqoyaox13

+ 20 - 2
test_projects/test-dap-project-godot4/ScopeVars.gd

@@ -2,7 +2,25 @@ extends Node
 
 
 var member1 := TestClassA.new()
 var member1 := TestClassA.new()
 
 
+var str_var := "ScopeVars::member::str_var"
+var str_var_member_only := "ScopeVars::member::str_var_member_only"
+
+class ClassFoo:
+  var member_ClassFoo
+  var str_var := "ScopeVars::ClassFoo::member::str_var"
+  var str_var_member_only := "ScopeVars::ClassFoo::member::str_var_member_only"
+  func test_function(delta: float) -> void:
+    var str_var := "ScopeVars::ClassFoo::test_function::local::str_var"
+    print("breakpoint::ScopeVars::ClassFoo::test_function")
+
+
 func _ready() -> void:
 func _ready() -> void:
-  var local1 := TestClassA.new()
-  var local2 = GlobalScript.globalMember
+  var str_var := "ScopeVars::_ready::local::str_var"
+  var self_var := self
   print("breakpoint::ScopeVars::_ready")
   print("breakpoint::ScopeVars::_ready")
+  test(0.123);
+
+func test(val: float):
+  var str_var := "ScopeVars::test::local::str_var"
+  var foo := ClassFoo.new()
+  foo.test_function(val)

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

@@ -0,0 +1 @@
+uid://cbgugy44s0uia

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

@@ -0,0 +1 @@
+uid://ct5jeingo4ge