mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2026-01-04 10:09:58 +03:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0da21f23a3 | ||
|
|
a04f58c82d | ||
|
|
03606fdb3a | ||
|
|
f4ae73c9a0 | ||
|
|
1dcbd651df | ||
|
|
54f68f15ea | ||
|
|
0203cec293 | ||
|
|
34de1b64f0 | ||
|
|
e528384ea5 | ||
|
|
c5c7aa2ced | ||
|
|
0a3c319879 | ||
|
|
3f7a944e96 | ||
|
|
51ef0ef0c0 | ||
|
|
b29fbb75a0 | ||
|
|
b986ce0e51 | ||
|
|
035211276d | ||
|
|
53f48ede63 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -29,7 +29,7 @@ body:
|
|||||||
Use the **Help > About** menu to see your current version.
|
Use the **Help > About** menu to see your current version.
|
||||||
Specify the Git commit hash if using a development or non-official build.
|
Specify the Git commit hash if using a development or non-official build.
|
||||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||||
placeholder: "1.97.2"
|
placeholder: "1.98.1"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ body:
|
|||||||
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
|
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
|
||||||
Specify the Git commit hash if using a development or non-official build.
|
Specify the Git commit hash if using a development or non-official build.
|
||||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||||
placeholder: "2.4.0"
|
placeholder: "2.5.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -29,7 +29,7 @@ body:
|
|||||||
Use the **Help > About** menu to see your current version.
|
Use the **Help > About** menu to see your current version.
|
||||||
Specify the Git commit hash if using a development or non-official build.
|
Specify the Git commit hash if using a development or non-official build.
|
||||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||||
placeholder: "1.93.1"
|
placeholder: "1.98.1"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ body:
|
|||||||
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
|
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
|
||||||
Specify the Git commit hash if using a development or non-official build.
|
Specify the Git commit hash if using a development or non-official build.
|
||||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||||
placeholder: "2.3.0"
|
placeholder: "2.5.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4.2.0
|
uses: actions/setup-node@v4.2.0
|
||||||
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'
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
ls -l godot-tools.vsix
|
ls -l godot-tools.vsix
|
||||||
|
|
||||||
- name: Upload extension VSIX
|
- name: Upload extension VSIX
|
||||||
uses: actions/upload-artifact@v4.6.0
|
uses: actions/upload-artifact@v4.6.1
|
||||||
with:
|
with:
|
||||||
name: godot-tools
|
name: godot-tools
|
||||||
path: godot-tools.vsix
|
path: godot-tools.vsix
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
23
.vscode/launch.json
vendored
23
.vscode/launch.json
vendored
@@ -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"
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### 2.5.0
|
||||||
|
|
||||||
|
- [**Add `print_rich()` support to debug console**](https://github.com/godotengine/godot-vscode-plugin/pull/792)
|
||||||
|
- [Improve Scene Preview drag-and-drop behavior](https://github.com/godotengine/godot-vscode-plugin/pull/815)
|
||||||
|
- [Add snippet/placeholder behavior to Scene Preview file drops](https://github.com/godotengine/godot-vscode-plugin/pull/813)
|
||||||
|
- [Overhaul the DebugAdapter variables in DAP](https://github.com/godotengine/godot-vscode-plugin/pull/793)
|
||||||
|
- [Fix opening a Godot project in Visual Studio Code before the editor resulting in bad file requests](https://github.com/godotengine/godot-vscode-plugin/pull/816)
|
||||||
|
- [Fix some GDScript syntax highlighting and formatting issues](https://github.com/godotengine/godot-vscode-plugin/pull/783)
|
||||||
|
- [Fix attached debugging](https://github.com/godotengine/godot-vscode-plugin/pull/784)
|
||||||
|
- [Fix multi-packet reponses breaking things when starting or ending in a multi-byte UTF-8 sequence](https://github.com/godotengine/godot-vscode-plugin/pull/797)
|
||||||
|
|
||||||
### 2.4.0
|
### 2.4.0
|
||||||
|
|
||||||
- [**Implement warnings and errors in debug console**](https://github.com/godotengine/godot-vscode-plugin/pull/749)
|
- [**Implement warnings and errors in debug console**](https://github.com/godotengine/godot-vscode-plugin/pull/749)
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -97,6 +97,12 @@ You can set VS Code as your default script editor for Godot by following these s
|
|||||||
* On macOS, this executable is typically located at: `/Applications/Visual Studio Code.app/Contents/MacOS/Electron`
|
* On macOS, this executable is typically located at: `/Applications/Visual Studio Code.app/Contents/MacOS/Electron`
|
||||||
5. Fill **Exec Flags** with `{project} --goto {file}:{line}:{col}`
|
5. Fill **Exec Flags** with `{project} --goto {file}:{line}:{col}`
|
||||||
|
|
||||||
|
You can make Godot seamlessly reload VSCode-edited scripts by changing some additional settings. More details about each are available when hovering over the description in the Settings window:
|
||||||
|
|
||||||
|
- **Editor Settings > Text Editor > Behavior > Files > Auto Reload Scripts on External Change**
|
||||||
|
- **Editor Settings > Interface > Editor > Save on Focus Loss**
|
||||||
|
- **Editor Settings > Interface > Editor > Import Resources When Unfocused**
|
||||||
|
|
||||||
### VS Code
|
### VS Code
|
||||||
|
|
||||||
You can use the following settings to configure Godot Tools:
|
You can use the following settings to configure Godot Tools:
|
||||||
@@ -198,6 +204,8 @@ see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|||||||
- Make sure to open the project in the Godot editor first. If you opened
|
- Make sure to open the project in the Godot editor first. If you opened
|
||||||
the editor after opening VS Code, you can click the **Retry** button
|
the editor after opening VS Code, you can click the **Retry** button
|
||||||
in the bottom-right corner in VS Code.
|
in the bottom-right corner in VS Code.
|
||||||
|
- Reset the LSP Server port to the default values in both Godot's Editor Settings and
|
||||||
|
in VSCode.
|
||||||
|
|
||||||
### Why isn't IntelliSense displaying script members?
|
### Why isn't IntelliSense displaying script members?
|
||||||
|
|
||||||
@@ -205,3 +213,11 @@ see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|||||||
infer all variable types.
|
infer all variable types.
|
||||||
- To increase the number of results displayed, open the **Editor Settings**,
|
- To increase the number of results displayed, open the **Editor Settings**,
|
||||||
go to the **Language Server** section then check **Enable Smart Resolve**.
|
go to the **Language Server** section then check **Enable Smart Resolve**.
|
||||||
|
|
||||||
|
### Can Godot/VSCode load in my script changes automatically instead of showing a confirmation window?
|
||||||
|
|
||||||
|
Godot has some Editor Settings that can help you if your workflow involves changing files in both editors:
|
||||||
|
|
||||||
|
- **Editor Settings > Text Editor > Behavior > Files > Auto Reload Scripts on External Change**
|
||||||
|
- **Editor Settings > Interface > Editor > Save on Focus Loss**
|
||||||
|
- **Editor Settings > Interface > Editor > Import Resources When Unfocused**
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"vcs": {
|
||||||
|
"defaultBranch": "master"
|
||||||
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"formatWithErrors": false,
|
"formatWithErrors": false,
|
||||||
@@ -16,7 +19,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"style": {
|
"style": {
|
||||||
"noUselessElse": "off",
|
"noUselessElse": "off",
|
||||||
"useImportType": "off"
|
"useImportType": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1326
package-lock.json
generated
1326
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "godot-tools",
|
"name": "godot-tools",
|
||||||
"displayName": "godot-tools",
|
"displayName": "godot-tools",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"version": "2.4.0",
|
"version": "2.5.0",
|
||||||
"description": "Tools for game development with Godot Engine and GDScript",
|
"description": "Tools for game development with Godot Engine and GDScript",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
],
|
],
|
||||||
"main": "./out/extension.js",
|
"main": "./out/extension.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"format": "biome format --write --changed src",
|
||||||
"compile": "tsc -p ./",
|
"compile": "tsc -p ./",
|
||||||
"lint": "eslint ./src --quiet",
|
"lint": "eslint ./src --quiet",
|
||||||
"watch": "tsc -watch -p ./",
|
"watch": "tsc -watch -p ./",
|
||||||
@@ -60,6 +61,11 @@
|
|||||||
"command": "godotTools.openEditor",
|
"command": "godotTools.openEditor",
|
||||||
"title": "Open workspace with Godot editor"
|
"title": "Open workspace with Godot editor"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"category": "Godot Tools",
|
||||||
|
"command": "godotTools.openEditorSettings",
|
||||||
|
"title": "Open EditorSettings File"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"category": "Godot Tools",
|
"category": "Godot Tools",
|
||||||
"command": "godotTools.startLanguageServer",
|
"command": "godotTools.startLanguageServer",
|
||||||
@@ -258,7 +264,7 @@
|
|||||||
"maximum": 200,
|
"maximum": 200,
|
||||||
"description": "Scale factor (%) to apply to the Godot documentation viewer."
|
"description": "Scale factor (%) to apply to the Godot documentation viewer."
|
||||||
},
|
},
|
||||||
"godotTools.documentation.displayMinimap":{
|
"godotTools.documentation.displayMinimap": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
"description": "Whether to display the minimap for the Godot documentation viewer."
|
"description": "Whether to display the minimap for the Godot documentation viewer."
|
||||||
@@ -870,25 +876,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@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.25.0",
|
||||||
"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",
|
||||||
@@ -898,12 +908,13 @@
|
|||||||
"@vscode/debugadapter": "^1.68.0",
|
"@vscode/debugadapter": "^1.68.0",
|
||||||
"@vscode/debugprotocol": "^1.68.0",
|
"@vscode/debugprotocol": "^1.68.0",
|
||||||
"await-notify": "^1.0.1",
|
"await-notify": "^1.0.1",
|
||||||
|
"bbcode-to-ansi": "^1.0.0",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"marked": "^4.0.11",
|
"marked": "^4.0.11",
|
||||||
"net": "^1.0.2",
|
"net": "^1.0.2",
|
||||||
"prismjs": "^1.17.1",
|
"prismjs": "^1.17.1",
|
||||||
"terminate": "^2.5.0",
|
"terminate": "^2.5.0",
|
||||||
"vscode-languageclient": "^7.0.0",
|
"vscode-languageclient": "^9.0.1",
|
||||||
"vscode-oniguruma": "^2.0.1",
|
"vscode-oniguruma": "^2.0.1",
|
||||||
"vscode-textmate": "^9.0.0",
|
"vscode-textmate": "^9.0.0",
|
||||||
"ws": "^8.17.1",
|
"ws": "^8.17.1",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
|
||||||
@@ -106,7 +109,7 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
|
|||||||
log.info(`Project version identified as ${projectVersion}`);
|
log.info(`Project version identified as ${projectVersion}`);
|
||||||
|
|
||||||
if (projectVersion.startsWith("4")) {
|
if (projectVersion.startsWith("4")) {
|
||||||
this.session = new Godot4DebugSession();
|
this.session = new Godot4DebugSession(projectVersion);
|
||||||
} else {
|
} else {
|
||||||
this.session = new Godot3DebugSession();
|
this.session = new Godot3DebugSession();
|
||||||
}
|
}
|
||||||
@@ -256,38 +259,34 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public inspect_node(element: SceneNode | RemoteProperty) {
|
public async inspect_node(element: SceneNode | RemoteProperty) {
|
||||||
this.session?.controller.request_inspect_object(BigInt(element.object_id));
|
await this.fill_provider_tree(element.label, BigInt(element.object_id));
|
||||||
this.session?.inspect_callbacks.set(
|
}
|
||||||
BigInt(element.object_id),
|
|
||||||
(class_name, variable) => {
|
private create_godot_variable(godot_object: GodotObject): GodotVariable {
|
||||||
this.inspectorProvider.fill_tree(
|
return {
|
||||||
element.label,
|
value: {
|
||||||
class_name,
|
type_name: function() { return godot_object.type; },
|
||||||
element.object_id,
|
stringify_value: function() { return `<${godot_object.godot_id}>`; },
|
||||||
variable
|
sub_values: function() {return godot_object.sub_values; },
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
} as GodotVariable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public refresh_scene_tree() {
|
private async fill_provider_tree(label: string, godot_id: bigint, force_refresh = false) {
|
||||||
this.session?.controller.request_scene_tree();
|
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);
|
||||||
public refresh_inspector() {
|
this.inspectorProvider.fill_tree(label, godot_object.type, Number(godot_object.godot_id), va);
|
||||||
if (this.inspectorProvider.has_tree()) {
|
} else {
|
||||||
const name = this.inspectorProvider.get_top_name();
|
this.session?.controller.request_inspect_object(BigInt(godot_id));
|
||||||
const id = this.inspectorProvider.get_top_id();
|
|
||||||
|
|
||||||
this.session?.controller.request_inspect_object(BigInt(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
|
const value = await window.showInputBox({ value: `${property.description}` });
|
||||||
.showInputBox({ value: `${property.description}` })
|
let new_parsed_value: any;
|
||||||
.then((value) => {
|
switch (type) {
|
||||||
let new_parsed_value: any;
|
case "string":
|
||||||
switch (type) {
|
new_parsed_value = value;
|
||||||
case "string":
|
break;
|
||||||
new_parsed_value = value;
|
case "number":
|
||||||
break;
|
if (is_float) {
|
||||||
case "number":
|
new_parsed_value = Number.parseFloat(value);
|
||||||
if (is_float) {
|
if (Number.isNaN(new_parsed_value)) {
|
||||||
new_parsed_value = Number.parseFloat(value);
|
return;
|
||||||
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 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(
|
new_parsed_value = Number.parseInt(value);
|
||||||
BigInt(property.object_id),
|
if (Number.isNaN(new_parsed_value)) {
|
||||||
property.label,
|
return;
|
||||||
new_parsed_value,
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case "boolean":
|
||||||
|
if (
|
||||||
|
value.toLowerCase() === "true" ||
|
||||||
|
value.toLowerCase() === "false"
|
||||||
|
) {
|
||||||
|
new_parsed_value = value.toLowerCase() === "true";
|
||||||
|
} else if (value === "0" || value === "1") {
|
||||||
|
new_parsed_value = value === "1";
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (property.changes_parent) {
|
||||||
|
const parents = [property.parent];
|
||||||
|
let idx = 0;
|
||||||
|
while (parents[idx].changes_parent) {
|
||||||
|
parents.push(parents[idx++].parent);
|
||||||
|
}
|
||||||
|
const changed_value = this.inspectorProvider.get_changed_value(
|
||||||
|
parents,
|
||||||
|
property,
|
||||||
|
new_parsed_value
|
||||||
|
);
|
||||||
|
this.session?.controller.set_object_property(
|
||||||
|
BigInt(property.object_id),
|
||||||
|
parents[idx].label,
|
||||||
|
changed_value,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.session?.controller.set_object_property(
|
||||||
|
BigInt(property.object_id),
|
||||||
|
property.label,
|
||||||
|
new_parsed_value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const name = this.inspectorProvider.get_top_name();
|
const label = this.inspectorProvider.get_top_name();
|
||||||
const id = this.inspectorProvider.get_top_id();
|
const godot_id = BigInt(this.inspectorProvider.get_top_id());
|
||||||
|
|
||||||
this.session?.controller.request_inspect_object(BigInt(id));
|
await this.fill_provider_tree(label, godot_id, /*force_refresh*/ true);
|
||||||
this.session?.inspect_callbacks.set(
|
// const res = await debug.activeDebugSession?.customRequest("refreshVariables"); // refresh vscode.debug variables
|
||||||
BigInt(id),
|
this.session.sendEvent(new InvalidatedEvent(["variables"]));
|
||||||
(class_name, variable) => {
|
console.log("foo");
|
||||||
this.inspectorProvider.fill_tree(
|
|
||||||
name,
|
|
||||||
class_name,
|
|
||||||
id,
|
|
||||||
variable
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,12 @@ import { build_sub_values, parse_next_scene_node, split_buffers } from "./helper
|
|||||||
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 BBCodeToAnsi from 'bbcode-to-ansi';
|
||||||
|
|
||||||
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");
|
||||||
|
//initialize bbcodeParser and set default output color to grey
|
||||||
|
const bbcodeParser = new BBCodeToAnsi("\u001b[38;2;211;211;211m");
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
public command: string = "";
|
public command: string = "";
|
||||||
@@ -427,9 +430,8 @@ export class ServerController {
|
|||||||
this.didFirstOutput = true;
|
this.didFirstOutput = true;
|
||||||
// this.request_scene_tree();
|
// this.request_scene_tree();
|
||||||
}
|
}
|
||||||
const lines = command.parameters;
|
for (const output of command.parameters){
|
||||||
for (const line of lines) {
|
output[0].split("\n").forEach(line => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
|
||||||
debug.activeDebugConsole.appendLine(ansi.bright.blue + line[0]);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,42 +9,33 @@ 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 constructor() {
|
public variables_manager: VariablesManager;
|
||||||
|
|
||||||
|
public constructor(projectVersion: string) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.setDebuggerLinesStartAt1(false);
|
this.setDebuggerLinesStartAt1(false);
|
||||||
this.setDebuggerColumnsStartAt1(false);
|
this.setDebuggerColumnsStartAt1(false);
|
||||||
|
|
||||||
|
this.controller.setProjectVersion(projectVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
@@ -102,6 +93,7 @@ export class GodotDebugSession extends LoggingDebugSession {
|
|||||||
|
|
||||||
this.mode = "attach";
|
this.mode = "attach";
|
||||||
|
|
||||||
|
this.debug_data.projectPath = args.project;
|
||||||
this.exception = false;
|
this.exception = false;
|
||||||
await this.controller.attach(args);
|
await this.controller.attach(args);
|
||||||
|
|
||||||
@@ -126,34 +118,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 +134,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 +174,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,6 +202,53 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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://", "")}`),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("stackTraceRequest response", response);
|
||||||
|
this.sendResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
|
||||||
|
log.info("scopesRequest", args);
|
||||||
|
// this.variables_manager.variablesFrameId = args.frameId;
|
||||||
|
|
||||||
|
// 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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
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 },
|
||||||
|
// ],
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info("scopesRequest response", response);
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,291 +257,48 @@ export class GodotDebugSession extends LoggingDebugSession {
|
|||||||
args: DebugProtocol.VariablesArguments,
|
args: DebugProtocol.VariablesArguments,
|
||||||
) {
|
) {
|
||||||
log.info("variablesRequest", args);
|
log.info("variablesRequest", args);
|
||||||
if (!this.all_scopes) {
|
try {
|
||||||
|
const variables = await this.variables_manager.get_vscode_object(args.variablesReference);
|
||||||
|
|
||||||
response.body = {
|
response.body = {
|
||||||
variables: [],
|
variables: variables,
|
||||||
};
|
};
|
||||||
this.sendResponse(response);
|
} catch (error) {
|
||||||
return;
|
log.error("variablesRequest", error);
|
||||||
|
response.success = false;
|
||||||
|
response.message = error.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const reference = this.all_scopes[args.variablesReference];
|
log.info("variablesRequest response", response);
|
||||||
let variables: DebugProtocol.Variable[];
|
this.sendResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
if (!reference.sub_values) {
|
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
|
||||||
variables = [];
|
log.info("evaluateRequest", args);
|
||||||
} else {
|
|
||||||
variables = reference.sub_values.map((va) => {
|
try {
|
||||||
const sva = this.all_scopes.find(
|
const parsed_variable = await this.variables_manager.get_vscode_variable_by_name(
|
||||||
(sva) => sva && sva.scope_path === va.scope_path && sva.name === va.name,
|
args.expression,
|
||||||
);
|
args.frameId,
|
||||||
if (sva) {
|
);
|
||||||
return parse_variable(
|
response.body = {
|
||||||
sva,
|
result: parsed_variable.value,
|
||||||
this.all_scopes.findIndex(
|
variablesReference: parsed_variable.variablesReference,
|
||||||
(va_idx) =>
|
};
|
||||||
va_idx &&
|
} catch (error) {
|
||||||
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
|
response.success = false;
|
||||||
va_idx.name === va.name,
|
response.message = error.toString();
|
||||||
),
|
response.body = {
|
||||||
);
|
result: "null",
|
||||||
}
|
variablesReference: 0,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
response.body = {
|
log.info("evaluateRequest response", response);
|
||||||
variables: variables,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public set_exception(exception: boolean) {
|
public set_exception(exception: boolean) {
|
||||||
this.exception = true;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const va of stackVars.members) {
|
|
||||||
va.scope_path = "@.member";
|
|
||||||
this.append_variable(va);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const va of stackVars.globals) {
|
|
||||||
va.scope_path = "@.global";
|
|
||||||
this.append_variable(va);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (items.length > 2 && index < items.length - 2) {
|
|
||||||
result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private append_variable(variable: GodotVariable, index?: number) {
|
|
||||||
if (index) {
|
|
||||||
this.all_scopes.splice(index, 0, variable);
|
|
||||||
} else {
|
|
||||||
this.all_scopes.push(variable);
|
|
||||||
}
|
|
||||||
const base_path = `${variable.scope_path}.${variable.name}`;
|
|
||||||
if (variable.sub_values) {
|
|
||||||
variable.sub_values.forEach((va, i) => {
|
|
||||||
va.scope_path = base_path;
|
|
||||||
this.append_variable(va, index ? index + i + 1 : undefined);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,359 +0,0 @@
|
|||||||
import { promises as fs } from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import * as vscode from "vscode";
|
|
||||||
import { DebugProtocol } from "@vscode/debugprotocol";
|
|
||||||
import chai from "chai";
|
|
||||||
import chaiSubset from "chai-subset";
|
|
||||||
|
|
||||||
import { promisify } from "util";
|
|
||||||
import { execFile } from "child_process";
|
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
chai.use(chaiSubset);
|
|
||||||
const { expect } = chai;
|
|
||||||
|
|
||||||
async function sleep(ms) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a path to a script, returns an object where each key is the name of a
|
|
||||||
* breakpoint (delimited by `breakpoint::`) and each value is the line number
|
|
||||||
* where the breakpoint appears in the script.
|
|
||||||
*
|
|
||||||
* @param scriptPath The path to the script to scan.
|
|
||||||
* @returns An object of breakpoint names to line numbers.
|
|
||||||
*/
|
|
||||||
async function getBreakpointLocations(scriptPath: string): Promise<{ [key: string]: vscode.Location }> {
|
|
||||||
const script_content = await fs.readFile(scriptPath, "utf-8");
|
|
||||||
const breakpoints: { [key: string]: vscode.Location } = {};
|
|
||||||
const breakpointRegex = /\b(breakpoint::.*)\b/g;
|
|
||||||
let match: RegExpExecArray | null;
|
|
||||||
while ((match = breakpointRegex.exec(script_content)) !== null) {
|
|
||||||
const breakpointName = match[1];
|
|
||||||
const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1;
|
|
||||||
breakpoints[breakpointName] = new vscode.Location(vscode.Uri.file(scriptPath), new vscode.Position(line - 1, 0));
|
|
||||||
}
|
|
||||||
return breakpoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForActiveStackItemChange(ms: number = 10000): Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
|
|
||||||
const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((resolve, reject) => {
|
|
||||||
const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => {
|
|
||||||
debugListener.dispose();
|
|
||||||
resolve(vscode.debug.activeStackItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Timeout fallback in case stack item never changes
|
|
||||||
setTimeout(() => {
|
|
||||||
debugListener.dispose();
|
|
||||||
console.warn();
|
|
||||||
reject(new Error(`The ActiveStackItem eventwas not changed within the timeout period of '${ms}'`));
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStackFrames(threadId: number = 1): Promise<DebugProtocol.StackFrame[]> {
|
|
||||||
// Ensure there is an active debug session
|
|
||||||
if (!vscode.debug.activeDebugSession) {
|
|
||||||
throw new Error("No active debug session found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// corresponds to file://./debug_session.ts stackTraceRequest(...)
|
|
||||||
const stackTraceResponse = await vscode.debug.activeDebugSession.customRequest("stackTrace", {
|
|
||||||
threadId: threadId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract and return the stack frames
|
|
||||||
return stackTraceResponse.stackFrames || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForBreakpoint(breakpoint: vscode.SourceBreakpoint, timeoutMs: number, ctx?: Mocha.Context): Promise<void> {
|
|
||||||
const t0 = performance.now();
|
|
||||||
console.log(fmt(`Waiting for breakpoint ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}`));
|
|
||||||
const res = await waitForActiveStackItemChange(timeoutMs);
|
|
||||||
const t1 = performance.now();
|
|
||||||
console.log(fmt(`Waiting for breakpoint completed ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}, took ${t1 - t0}ms`));
|
|
||||||
const stackFrames = await getStackFrames();
|
|
||||||
if (stackFrames[0].source.path !== breakpoint.location.uri.fsPath || stackFrames[0].line != breakpoint.location.range.start.line+1) {
|
|
||||||
throw new Error(`Wrong breakpoint was hit. Expected: ${breakpoint.location.uri.fsPath}:${breakpoint.location.range.start.line+1}, Got: ${stackFrames[0].source.path}:${stackFrames[0].line}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum VariableScope {
|
|
||||||
Locals = 1,
|
|
||||||
Members = 2,
|
|
||||||
Globals = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getVariablesForScope(scope: VariableScope): Promise<DebugProtocol.Variable[]> {
|
|
||||||
// corresponds to file://./debug_session.ts protected async variablesRequest
|
|
||||||
const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
|
|
||||||
variablesReference: scope
|
|
||||||
});
|
|
||||||
return variablesResponse?.variables || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise<any> {
|
|
||||||
// corresponds to file://./debug_session.ts protected async evaluateRequest
|
|
||||||
const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest("evaluate", {
|
|
||||||
context,
|
|
||||||
expression,
|
|
||||||
frameId
|
|
||||||
});
|
|
||||||
return evaluateResponse.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMs(ms: number): string {
|
|
||||||
const seconds = Math.floor((ms / 1000) % 60);
|
|
||||||
const minutes = Math.floor((ms / (1000 * 60)) % 60);
|
|
||||||
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${(Math.round(ms) % 1000).toString().padStart(3, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMessage(this: Mocha.Context, msg: string): string {
|
|
||||||
return `[${formatMs(performance.now()-this.testStart)}] ${msg}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
|
|
||||||
|
|
||||||
async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn"): Promise<void> {
|
|
||||||
const t0 = performance.now();
|
|
||||||
const debugConfig: vscode.DebugConfiguration = {
|
|
||||||
type: "godot",
|
|
||||||
request: "launch",
|
|
||||||
name: "Godot Debug",
|
|
||||||
scene: scene,
|
|
||||||
additional_options: "--headless"
|
|
||||||
};
|
|
||||||
console.log(fmt(`Starting debugger for scene ${scene}`));
|
|
||||||
const res = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfig);
|
|
||||||
const t1 = performance.now();
|
|
||||||
console.log(fmt(`Starting debugger for scene ${scene} completed, took ${t1 - t0}ms`));
|
|
||||||
if (!res) {
|
|
||||||
throw new Error(`Failed to start debugging for scene ${scene}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suite("DAP Integration Tests - Variable Scopes", () => {
|
|
||||||
// workspaceFolder should match `.vscode-test.js`::workspaceFolder
|
|
||||||
const workspaceFolder = path.resolve(__dirname, "../../../test_projects/test-dap-project-godot4");
|
|
||||||
|
|
||||||
suiteSetup(async function() {
|
|
||||||
this.timeout(20000); // enough time to do `godot --import`
|
|
||||||
console.log("Environment Variables:");
|
|
||||||
for (const [key, value] of Object.entries(process.env)) {
|
|
||||||
console.log(`${key}: ${value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// init the godot project by importing it in godot engine:
|
|
||||||
const config = vscode.workspace.getConfiguration("godotTools");
|
|
||||||
var godot4_path = config.get<string>("editorPath.godot4");
|
|
||||||
// get the path for currently opened project in vscode test instance:
|
|
||||||
var project_path = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
|
||||||
console.log("Executing", [godot4_path, "--headless", "--import", project_path]);
|
|
||||||
const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", project_path], {shell: true, cwd: project_path});
|
|
||||||
if (exec_res.stderr !== "") {
|
|
||||||
throw new Error(exec_res.stderr);
|
|
||||||
}
|
|
||||||
console.log(exec_res.stdout);
|
|
||||||
});
|
|
||||||
|
|
||||||
setup(async function() {
|
|
||||||
console.log(`➤ Test '${this?.currentTest.title}' starting`);
|
|
||||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors");
|
|
||||||
if (vscode.debug.breakpoints) {
|
|
||||||
await vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
|
|
||||||
}
|
|
||||||
this.testStart = performance.now();
|
|
||||||
fmt = formatMessage.bind(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
teardown(async function() {
|
|
||||||
await sleep(1000);
|
|
||||||
if (vscode.debug.activeDebugSession !== undefined) {
|
|
||||||
console.log("Closing debug session");
|
|
||||||
await vscode.debug.stopDebugging();
|
|
||||||
}
|
|
||||||
console.log(`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
test("sample test", async function() {
|
|
||||||
// await sleep(1000);
|
|
||||||
await startDebugging("ScopeVars.tscn");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
test("should return correct scopes", async function() {
|
|
||||||
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
|
|
||||||
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
|
|
||||||
vscode.debug.addBreakpoints([breakpoint]);
|
|
||||||
|
|
||||||
await startDebugging("ScopeVars.tscn");
|
|
||||||
await waitForBreakpoint(breakpoint, 2000);
|
|
||||||
|
|
||||||
// TODO: current DAP needs a delay before it will return variables
|
|
||||||
console.log("Sleeping for 2 seconds");
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
// corresponds to file://./debug_session.ts async scopesRequest
|
|
||||||
const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: 1});
|
|
||||||
|
|
||||||
expect(res_scopes).to.exist;
|
|
||||||
expect(res_scopes.scopes).to.exist;
|
|
||||||
|
|
||||||
const scopes = res_scopes.scopes;
|
|
||||||
expect(scopes.length).to.equal(3, "Expected 3 scopes");
|
|
||||||
expect(scopes[0].name).to.equal(VariableScope[VariableScope.Locals], "Expected Locals scope");
|
|
||||||
expect(scopes[0].variablesReference).to.equal(VariableScope.Locals, "Expected Locals variablesReference");
|
|
||||||
expect(scopes[1].name).to.equal(VariableScope[VariableScope.Members], "Expected Members scope");
|
|
||||||
expect(scopes[1].variablesReference).to.equal(VariableScope.Members, "Expected Members variablesReference");
|
|
||||||
expect(scopes[2].name).to.equal(VariableScope[VariableScope.Globals], "Expected Globals scope");
|
|
||||||
expect(scopes[2].variablesReference).to.equal(VariableScope.Globals, "Expected Globals variablesReference");
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
await vscode.debug.stopDebugging();
|
|
||||||
})?.timeout(5000);
|
|
||||||
|
|
||||||
test("should return global variables", async function() {
|
|
||||||
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
|
|
||||||
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
|
|
||||||
vscode.debug.addBreakpoints([breakpoint]);
|
|
||||||
|
|
||||||
await startDebugging("ScopeVars.tscn");
|
|
||||||
await waitForBreakpoint(breakpoint, 2000);
|
|
||||||
|
|
||||||
// TODO: current DAP needs a delay before it will return variables
|
|
||||||
console.log("Sleeping for 2 seconds");
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
const variables = await getVariablesForScope(VariableScope.Globals);
|
|
||||||
expect(variables).to.containSubset([{name: "GlobalScript"}]);
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
await vscode.debug.stopDebugging();
|
|
||||||
})?.timeout(7000);
|
|
||||||
|
|
||||||
test("should return local variables", async function() {
|
|
||||||
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
|
|
||||||
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
|
|
||||||
vscode.debug.addBreakpoints([breakpoint]);
|
|
||||||
|
|
||||||
await startDebugging("ScopeVars.tscn");
|
|
||||||
await waitForBreakpoint(breakpoint, 2000);
|
|
||||||
|
|
||||||
// TODO: current DAP needs a delay before it will return variables
|
|
||||||
console.log("Sleeping for 2 seconds");
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
const variables = await getVariablesForScope(VariableScope.Locals);
|
|
||||||
expect(variables.length).to.equal(2);
|
|
||||||
expect(variables).to.containSubset([{name: "local1"}]);
|
|
||||||
expect(variables).to.containSubset([{name: "local2"}]);
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
await vscode.debug.stopDebugging();
|
|
||||||
})?.timeout(5000);
|
|
||||||
|
|
||||||
test("should return member variables", async function() {
|
|
||||||
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
|
|
||||||
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
|
|
||||||
vscode.debug.addBreakpoints([breakpoint]);
|
|
||||||
|
|
||||||
await startDebugging("ScopeVars.tscn");
|
|
||||||
await waitForBreakpoint(breakpoint, 2000);
|
|
||||||
|
|
||||||
// TODO: current DAP needs a delay before it will return variables
|
|
||||||
console.log("Sleeping for 2 seconds");
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
const variables = await getVariablesForScope(VariableScope.Members);
|
|
||||||
expect(variables.length).to.equal(2);
|
|
||||||
expect(variables).to.containSubset([{name: "self"}]);
|
|
||||||
expect(variables).to.containSubset([{name: "member1"}]);
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
await vscode.debug.stopDebugging();
|
|
||||||
})?.timeout(5000);
|
|
||||||
|
|
||||||
test("should retrieve all built-in types correctly", async function() {
|
|
||||||
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
|
|
||||||
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]);
|
|
||||||
vscode.debug.addBreakpoints([breakpoint]);
|
|
||||||
|
|
||||||
await startDebugging("BuiltInTypes.tscn");
|
|
||||||
await waitForBreakpoint(breakpoint, 2000);
|
|
||||||
|
|
||||||
// TODO: current DAP needs a delay before it will return variables
|
|
||||||
console.log("Sleeping for 2 seconds");
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
const variables = await getVariablesForScope(VariableScope.Locals);
|
|
||||||
|
|
||||||
expect(variables).to.containSubset([{ name: "int_var", value: "42" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "float_var", value: "3.14" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "bool_var", value: "true" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "string_var", value: "Hello, Godot!" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "nil_var", value: "null" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "vector2", value: "Vector2(10, 20)" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "rect2", value: "Rect2((0, 0) - (100, 50))" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]);
|
|
||||||
// expect(variables).to.containSubset([{ name: "simple_array", value: "[1, 2, 3]" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "simple_array", value: "Array[3]" }]);
|
|
||||||
// expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]);
|
|
||||||
// expect(variables).to.containSubset([{ name: "nested_dict.sub_dict.sub_key", value: "99" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "nested_dict", value: "Dictionary[2]" }]);
|
|
||||||
// expect(variables).to.containSubset([{ name: "byte_array", value: "[0, 1, 2, 255]" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "byte_array", value: "Array[4]" }]);
|
|
||||||
// expect(variables).to.containSubset([{ name: "int32_array", value: "[100, 200, 300]" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "int32_array", value: "Array[3]" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "color_var", value: "Color(1, 0, 0, 1)" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "aabb_var", value: "AABB((0, 0, 0), (1, 1, 1))" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "plane_var", value: "Plane(0, 1, 0, -5)" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "callable_var", value: "Callable()" }]);
|
|
||||||
expect(variables).to.containSubset([{ name: "signal_var" }]);
|
|
||||||
const signal_var = variables.find(v => v.name === "signal_var");
|
|
||||||
expect(signal_var.value).to.match(/Signal\(member_signal\, <\d+>\)/, "Should be in format of 'Signal(member_signal, <28236055815>)'");
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
await vscode.debug.stopDebugging();
|
|
||||||
})?.timeout(5000);
|
|
||||||
|
|
||||||
test("should retrieve all complex variables correctly", async function() {
|
|
||||||
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
|
|
||||||
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]);
|
|
||||||
vscode.debug.addBreakpoints([breakpoint]);
|
|
||||||
|
|
||||||
await startDebugging("ExtensiveVars.tscn");
|
|
||||||
await waitForBreakpoint(breakpoint, 2000);
|
|
||||||
|
|
||||||
// TODO: current DAP needs a delay before it will return variables
|
|
||||||
console.log("Sleeping for 2 seconds");
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
const memberVariables = await getVariablesForScope(VariableScope.Members);
|
|
||||||
|
|
||||||
expect(memberVariables.length).to.equal(3);
|
|
||||||
expect(memberVariables).to.containSubset([{name: "self"}]);
|
|
||||||
expect(memberVariables).to.containSubset([{name: "self_var"}]);
|
|
||||||
const self = memberVariables.find(v => v.name === "self");
|
|
||||||
const self_var = memberVariables.find(v => v.name === "self_var");
|
|
||||||
expect(self.value).to.deep.equal(self_var.value);
|
|
||||||
|
|
||||||
const localVariables = await getVariablesForScope(VariableScope.Locals);
|
|
||||||
expect(localVariables.length).to.equal(4);
|
|
||||||
expect(localVariables).to.containSubset([
|
|
||||||
{ name: "local_label", value: "Label" },
|
|
||||||
{ name: "local_self_var_through_label", value: "Node2D" },
|
|
||||||
{ name: "local_classA", value: "RefCounted" },
|
|
||||||
{ name: "local_classB", value: "RefCounted" }
|
|
||||||
]);
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
await vscode.debug.stopDebugging();
|
|
||||||
})?.timeout(15000);
|
|
||||||
});
|
|
||||||
@@ -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) {
|
export function get_sub_values(value: any): GodotVariable[] {
|
||||||
var type = typeof va.value;
|
|
||||||
return ["number", "bigint", "boolean", "string"].some(x => x == type);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function get_sub_values(value: any) {
|
|
||||||
let subValues: GodotVariable[] = undefined;
|
let subValues: GodotVariable[] = undefined;
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -45,19 +41,15 @@ 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) => {
|
subValues = [];
|
||||||
if (typeof va["stringify_value"] === "function") {
|
for (const [key, val] of value.entries()) {
|
||||||
return {
|
const name =
|
||||||
name: `${va.type_name()}${va.stringify_value()}`,
|
typeof key["stringify_value"] === "function"
|
||||||
value: value.get(va),
|
? `${key.type_name()}${key.stringify_value()}`
|
||||||
} as GodotVariable;
|
: `${key}`;
|
||||||
} else {
|
const godot_id = val instanceof ObjectId ? val.id : undefined;
|
||||||
return {
|
subValues.push({ id: godot_id, name, value: val } as GodotVariable);
|
||||||
name: `${va}`,
|
}
|
||||||
value: value.get(va),
|
|
||||||
} 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;
|
||||||
@@ -71,54 +63,3 @@ 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,16 +16,20 @@ 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";
|
||||||
|
import BBCodeToAnsi from "bbcode-to-ansi";
|
||||||
|
|
||||||
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");
|
||||||
|
//initialize bbcodeParser and set default output color to grey
|
||||||
|
const bbcodeParser = new BBCodeToAnsi("\u001b[38;2;211;211;211m");
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
public command: string = "";
|
public command: string = "";
|
||||||
@@ -35,6 +39,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,11 +77,20 @@ 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 projectVersionMajor: number;
|
||||||
|
private projectVersionMinor: number;
|
||||||
|
private projectVersionPoint: number;
|
||||||
|
|
||||||
public constructor(public session: GodotDebugSession) {}
|
public constructor(public session: GodotDebugSession) {}
|
||||||
|
|
||||||
|
public setProjectVersion(projectVersion: string) {
|
||||||
|
const versionParts = projectVersion.split(".").map(Number);
|
||||||
|
this.projectVersionMajor = versionParts[0] || 0;
|
||||||
|
this.projectVersionMinor = versionParts[1] || 0;
|
||||||
|
this.projectVersionPoint = versionParts[2] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
public break() {
|
public break() {
|
||||||
this.send_command("break");
|
this.send_command("break");
|
||||||
}
|
}
|
||||||
@@ -93,8 +133,17 @@ export class ServerController {
|
|||||||
this.send_command("get_stack_dump");
|
this.send_command("get_stack_dump");
|
||||||
}
|
}
|
||||||
|
|
||||||
public request_stack_frame_vars(frame_id: number) {
|
public request_stack_frame_vars(stack_frame_id: number) {
|
||||||
this.send_command("get_stack_frame_vars", [frame_id]);
|
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) {
|
||||||
@@ -168,7 +217,7 @@ export class ServerController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectedVersion = result.version;
|
this.setProjectVersion(result.version);
|
||||||
|
|
||||||
let command = `"${godotPath}" --path "${args.project}"`;
|
let command = `"${godotPath}" --path "${args.project}"`;
|
||||||
const address = args.address.replace("tcp://", "");
|
const address = args.address.replace("tcp://", "");
|
||||||
@@ -259,7 +308,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);
|
||||||
}
|
}
|
||||||
@@ -345,7 +394,7 @@ export class ServerController {
|
|||||||
const command = new Command();
|
const command = new Command();
|
||||||
let i = 0;
|
let i = 0;
|
||||||
command.command = dataset[i++];
|
command.command = dataset[i++];
|
||||||
if (this.connectedVersion[2] >= "2") {
|
if (this.projectVersionMinor >= 2) {
|
||||||
command.threadId = dataset[i++];
|
command.threadId = dataset[i++];
|
||||||
}
|
}
|
||||||
command.parameters = dataset[i++];
|
command.parameters = dataset[i++];
|
||||||
@@ -362,9 +411,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 +432,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) {
|
if (godot_id < 0) {
|
||||||
id = id + BigInt(2) ** BigInt(64);
|
godot_id = godot_id + BigInt(2) ** BigInt(64);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawObject = new RawObject(className);
|
const rawObject = new RawObject(className);
|
||||||
@@ -396,14 +447,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));
|
// race condition here:
|
||||||
if (inspect_callback !== undefined) {
|
// 0. DebuggerStop1 happens
|
||||||
const inspectedVariable = { name: "", value: rawObject, sub_values: sub_values } as GodotVariable;
|
// 1. the DA may have sent the "inspect_object" message
|
||||||
inspect_callback(inspectedVariable.name, inspectedVariable);
|
// 2. the vscode hit "continue"
|
||||||
this.session.inspect_callbacks.delete(BigInt(id));
|
// 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 +479,75 @@ export class ServerController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "stack_frame_vars": {
|
case "stack_frame_vars": {
|
||||||
this.partialStackVars.reset(command.parameters[0]);
|
/** first response to {@link request_stack_frame_vars} */
|
||||||
this.session.set_scopes(this.partialStackVars);
|
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(
|
if (this.partialStackVars === undefined) {
|
||||||
command.parameters[0],
|
log.error("Unexpected 'stack_frame_var' received. Should have received 'stack_frame_vars' first.");
|
||||||
command.parameters[1],
|
return;
|
||||||
command.parameters[2],
|
}
|
||||||
command.parameters[3],
|
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": {
|
||||||
@@ -441,9 +555,8 @@ export class ServerController {
|
|||||||
this.didFirstOutput = true;
|
this.didFirstOutput = true;
|
||||||
// this.request_scene_tree();
|
// this.request_scene_tree();
|
||||||
}
|
}
|
||||||
const lines = command.parameters[0];
|
for (const output of command.parameters[0]) {
|
||||||
for (const line of lines) {
|
output.split("\n").forEach((line) => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
|
||||||
debug.activeDebugConsole.appendLine(ansi.bright.blue + line);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -612,11 +725,11 @@ export class ServerController {
|
|||||||
|
|
||||||
private send_command(command: string, parameters?: any[]) {
|
private send_command(command: string, parameters?: any[]) {
|
||||||
const commandArray: any[] = [command];
|
const commandArray: any[] = [command];
|
||||||
if (this.connectedVersion[2] >= "2") {
|
if (this.projectVersionMinor >= 2) {
|
||||||
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 +745,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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
467
src/debugger/godot4/variables/debugger_variables.test.ts
Normal file
467
src/debugger/godot4/variables/debugger_variables.test.ts
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import { DebugProtocol } from "@vscode/debugprotocol";
|
||||||
|
import chai from "chai";
|
||||||
|
import chaiSubset from "chai-subset";
|
||||||
|
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 { execFile } from "child_process";
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
chai.use(chaiSubset);
|
||||||
|
const { expect } = chai;
|
||||||
|
|
||||||
|
async function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a path to a script, returns an object where each key is the name of a
|
||||||
|
* breakpoint (delimited by `breakpoint::`) and each value is the line number
|
||||||
|
* where the breakpoint appears in the script.
|
||||||
|
*
|
||||||
|
* @param scriptPath The path to the script to scan.
|
||||||
|
* @returns An object of breakpoint names to line numbers.
|
||||||
|
*/
|
||||||
|
async function getBreakpointLocations(scriptPath: string): Promise<{ [key: string]: vscode.Location }> {
|
||||||
|
const script_content = await fs.readFile(scriptPath, "utf-8");
|
||||||
|
const breakpoints: { [key: string]: vscode.Location } = {};
|
||||||
|
const breakpointRegex = /\b(breakpoint::.*)\b/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = breakpointRegex.exec(script_content)) !== null) {
|
||||||
|
const breakpointName = match[1];
|
||||||
|
const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1;
|
||||||
|
breakpoints[breakpointName] = new vscode.Location(
|
||||||
|
vscode.Uri.file(scriptPath),
|
||||||
|
new vscode.Position(line - 1, 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return breakpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForActiveStackItemChange(
|
||||||
|
ms: number = 10000,
|
||||||
|
): Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
|
||||||
|
const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((resolve, reject) => {
|
||||||
|
const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => {
|
||||||
|
debugListener.dispose();
|
||||||
|
resolve(vscode.debug.activeStackItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout fallback in case stack item never changes
|
||||||
|
setTimeout(() => {
|
||||||
|
debugListener.dispose();
|
||||||
|
console.warn();
|
||||||
|
reject(new Error(`The ActiveStackItem eventwas not changed within the timeout period of '${ms}'`));
|
||||||
|
}, ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStackFrames(threadId: number = 1): Promise<DebugProtocol.StackFrame[]> {
|
||||||
|
// Ensure there is an active debug session
|
||||||
|
if (!vscode.debug.activeDebugSession) {
|
||||||
|
throw new Error("No active debug session found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// corresponds to file://./debug_session.ts stackTraceRequest(...)
|
||||||
|
const stackTraceResponse = await vscode.debug.activeDebugSession.customRequest("stackTrace", {
|
||||||
|
threadId: threadId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract and return the stack frames
|
||||||
|
return stackTraceResponse.stackFrames || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForBreakpoint(
|
||||||
|
breakpoint: vscode.SourceBreakpoint,
|
||||||
|
timeoutMs: number,
|
||||||
|
ctx?: Mocha.Context,
|
||||||
|
): Promise<void> {
|
||||||
|
const t0 = performance.now();
|
||||||
|
console.log(
|
||||||
|
fmt(
|
||||||
|
`Waiting for breakpoint ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const res = await waitForActiveStackItemChange(timeoutMs);
|
||||||
|
const t1 = performance.now();
|
||||||
|
console.log(
|
||||||
|
fmt(
|
||||||
|
`Waiting for breakpoint completed ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}, took ${t1 - t0}ms`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const stackFrames = await getStackFrames();
|
||||||
|
if (
|
||||||
|
stackFrames[0].source.path !== breakpoint.location.uri.fsPath ||
|
||||||
|
stackFrames[0].line != breakpoint.location.range.start.line + 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Wrong breakpoint was hit. Expected: ${breakpoint.location.uri.fsPath}:${breakpoint.location.range.start.line + 1}, Got: ${stackFrames[0].source.path}:${stackFrames[0].line}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VariableScope {
|
||||||
|
Locals,
|
||||||
|
Members,
|
||||||
|
Globals,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVariablesForVSCodeID(vscode_id: number): Promise<DebugProtocol.Variable[]> {
|
||||||
|
// corresponds to file://./debug_session.ts protected async variablesRequest
|
||||||
|
const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
|
||||||
|
variablesReference: vscode_id,
|
||||||
|
});
|
||||||
|
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> {
|
||||||
|
// corresponds to file://./debug_session.ts protected async evaluateRequest
|
||||||
|
const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest(
|
||||||
|
"evaluate",
|
||||||
|
{
|
||||||
|
context,
|
||||||
|
expression,
|
||||||
|
frameId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return evaluateResponse.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMs(ms: number): string {
|
||||||
|
const seconds = Math.floor((ms / 1000) % 60);
|
||||||
|
const minutes = Math.floor((ms / (1000 * 60)) % 60);
|
||||||
|
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${(Math.round(ms) % 1000).toString().padStart(3, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessage(this: Mocha.Context, msg: string): string {
|
||||||
|
return `[${formatMs(performance.now() - this.testStart)}] ${msg}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
|
||||||
|
|
||||||
|
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> {
|
||||||
|
const t0 = performance.now();
|
||||||
|
const debugConfig: vscode.DebugConfiguration = {
|
||||||
|
type: "godot",
|
||||||
|
request: "launch",
|
||||||
|
name: "Godot Debug",
|
||||||
|
scene: scene,
|
||||||
|
additional_options: "--headless",
|
||||||
|
};
|
||||||
|
console.log(fmt(`Starting debugger for scene ${scene}`));
|
||||||
|
const res = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfig);
|
||||||
|
const t1 = performance.now();
|
||||||
|
console.log(fmt(`Starting debugger for scene ${scene} completed, took ${t1 - t0}ms`));
|
||||||
|
if (!res) {
|
||||||
|
throw new Error(`Failed to start debugging for scene ${scene}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suite("DAP Integration Tests - Variable Scopes", () => {
|
||||||
|
// workspaceFolder should match `.vscode-test.js`::workspaceFolder
|
||||||
|
const workspaceFolder = 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 () {
|
||||||
|
this.timeout(20000); // enough time to do `godot --import`
|
||||||
|
console.log("Environment Variables:");
|
||||||
|
for (const [key, value] of Object.entries(process.env)) {
|
||||||
|
console.log(`${key}: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// init the godot project by importing it in godot engine:
|
||||||
|
const config = vscode.workspace.getConfiguration("godotTools");
|
||||||
|
// config.update("editorPath.godot4", "godot4", vscode.ConfigurationTarget.Workspace);
|
||||||
|
var godot4_path = config.get<string>("editorPath.godot4");
|
||||||
|
// get the path for currently opened project in vscode test instance:
|
||||||
|
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 !== "") {
|
||||||
|
throw new Error(exec_res.stderr);
|
||||||
|
}
|
||||||
|
console.log(exec_res.stdout);
|
||||||
|
});
|
||||||
|
|
||||||
|
setup(async function () {
|
||||||
|
console.log(`➤ Test '${this?.currentTest.title}' starting`);
|
||||||
|
await vscode.commands.executeCommand("workbench.action.closeAllEditors");
|
||||||
|
if (vscode.debug.breakpoints) {
|
||||||
|
await vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
|
||||||
|
}
|
||||||
|
this.testStart = performance.now();
|
||||||
|
fmt = formatMessage.bind(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(async function () {
|
||||||
|
this.timeout(3000);
|
||||||
|
await sleep(1000);
|
||||||
|
if (vscode.debug.activeDebugSession !== undefined) {
|
||||||
|
console.log("Closing debug session");
|
||||||
|
await vscode.debug.stopDebugging();
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
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("should return correct scopes", async function () {
|
||||||
|
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
|
||||||
|
const breakpoint = new vscode.SourceBreakpoint(
|
||||||
|
breakpointLocations["breakpoint::ScopeVars::ClassFoo::test_function"],
|
||||||
|
);
|
||||||
|
vscode.debug.addBreakpoints([breakpoint]);
|
||||||
|
|
||||||
|
await startDebugging("ScopeVars.tscn");
|
||||||
|
await waitForBreakpoint(breakpoint, 2000);
|
||||||
|
|
||||||
|
// TODO: current DAP needs a delay before it will return variables
|
||||||
|
console.log("Sleeping for 2 seconds");
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
// corresponds to file://./debug_session.ts async scopesRequest
|
||||||
|
const 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],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 () {
|
||||||
|
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
|
||||||
|
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
|
||||||
|
vscode.debug.addBreakpoints([breakpoint]);
|
||||||
|
|
||||||
|
await startDebugging("ScopeVars.tscn");
|
||||||
|
await waitForBreakpoint(breakpoint, 2000);
|
||||||
|
|
||||||
|
// TODO: current DAP needs a delay before it will return variables
|
||||||
|
console.log("Sleeping for 2 seconds");
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
const variables = await getVariablesForScope(VariableScope.Globals);
|
||||||
|
expect(variables).to.containSubset([{ name: "GlobalScript" }]);
|
||||||
|
})?.timeout(10000);
|
||||||
|
|
||||||
|
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 breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
|
||||||
|
vscode.debug.addBreakpoints([breakpoint]);
|
||||||
|
|
||||||
|
await startDebugging("ScopeVars.tscn");
|
||||||
|
await waitForBreakpoint(breakpoint, 2000);
|
||||||
|
|
||||||
|
// TODO: current DAP needs a delay before it will return variables
|
||||||
|
console.log("Sleeping for 2 seconds");
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
const variables = await getVariablesForScope(VariableScope.Locals);
|
||||||
|
expect(variables.length).to.equal(2);
|
||||||
|
expect(variables).to.containSubset([{ name: "str_var" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "self_var" }]);
|
||||||
|
})?.timeout(10000);
|
||||||
|
|
||||||
|
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 breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
|
||||||
|
vscode.debug.addBreakpoints([breakpoint]);
|
||||||
|
|
||||||
|
await startDebugging("ScopeVars.tscn");
|
||||||
|
await waitForBreakpoint(breakpoint, 2000);
|
||||||
|
|
||||||
|
// TODO: current DAP needs a delay before it will return variables
|
||||||
|
console.log("Sleeping for 2 seconds");
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
const variables = await getVariablesForScope(VariableScope.Members);
|
||||||
|
expect(variables.length).to.equal(4);
|
||||||
|
expect(variables).to.containSubset([{ name: "self" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "member1" }]);
|
||||||
|
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 () {
|
||||||
|
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
|
||||||
|
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]);
|
||||||
|
vscode.debug.addBreakpoints([breakpoint]);
|
||||||
|
|
||||||
|
await startDebugging("BuiltInTypes.tscn");
|
||||||
|
await waitForBreakpoint(breakpoint, 2000);
|
||||||
|
|
||||||
|
// TODO: current DAP needs a delay before it will return variables
|
||||||
|
console.log("Sleeping for 2 seconds");
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
const variables = await getVariablesForScope(VariableScope.Locals);
|
||||||
|
|
||||||
|
expect(variables).to.containSubset([{ name: "int_var", value: "42" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "float_var", value: "3.14" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "bool_var", value: "true" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "string_var", value: "Hello, Godot!" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "nil_var", value: "null" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "vector2", value: "Vector2(10, 20)" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "rect2", value: "Rect2((0, 0) - (100, 50))" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "simple_array", value: "(3) [1, 2, 3]" }]);
|
||||||
|
// expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]);
|
||||||
|
// expect(variables).to.containSubset([{ name: "nested_dict.sub_dict.sub_key", value: "99" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "nested_dict", value: "Dictionary(2)" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "byte_array", value: "(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: "aabb_var", value: "AABB((0, 0, 0), (1, 1, 1))" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "plane_var", value: "Plane(0, 1, 0, -5)" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "callable_var", value: "Callable()" }]);
|
||||||
|
expect(variables).to.containSubset([{ name: "signal_var" }]);
|
||||||
|
const signal_var = variables.find((v) => v.name === "signal_var");
|
||||||
|
expect(signal_var.value).to.match(
|
||||||
|
/Signal\(member_signal\, <\d+>\)/,
|
||||||
|
"Should be in format of 'Signal(member_signal, <28236055815>)'",
|
||||||
|
);
|
||||||
|
})?.timeout(10000);
|
||||||
|
|
||||||
|
test("should retrieve all complex variables correctly", async function () {
|
||||||
|
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
|
||||||
|
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]);
|
||||||
|
vscode.debug.addBreakpoints([breakpoint]);
|
||||||
|
|
||||||
|
await startDebugging("ExtensiveVars.tscn");
|
||||||
|
await waitForBreakpoint(breakpoint, 2000);
|
||||||
|
|
||||||
|
// TODO: current DAP needs a delay before it will return variables
|
||||||
|
console.log("Sleeping for 2 seconds");
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
const memberVariables = await getVariablesForScope(VariableScope.Members);
|
||||||
|
|
||||||
|
expect(memberVariables.length).to.equal(3, "Incorrect member variables count");
|
||||||
|
expect(memberVariables).to.containSubset([{ name: "self" }]);
|
||||||
|
expect(memberVariables).to.containSubset([{ name: "self_var" }]);
|
||||||
|
expect(memberVariables).to.containSubset([{ name: "label" }]);
|
||||||
|
const self = memberVariables.find((v) => v.name === "self");
|
||||||
|
const self_var = memberVariables.find((v) => v.name === "self_var");
|
||||||
|
expect(self.value).to.deep.equal(self_var.value);
|
||||||
|
|
||||||
|
const localVariables = await getVariablesForScope(VariableScope.Locals);
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/debugger/godot4/variables/godot_object_promise.test.ts
Normal file
78
src/debugger/godot4/variables/godot_object_promise.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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
src/debugger/godot4/variables/godot_object_promise.ts
Normal file
52
src/debugger/godot4/variables/godot_object_promise.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
282
src/debugger/godot4/variables/variables_manager.ts
Normal file
282
src/debugger/godot4/variables/variables_manager.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -283,7 +283,7 @@ export class ObjectId implements GDObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public type_name(): string {
|
public type_name(): string {
|
||||||
return "Object";
|
return "ObjectId";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { attemptSettingsUpdate, get_extension_uri, clean_godot_path } from "./utils";
|
import { attemptSettingsUpdate, get_extension_uri, clean_godot_path } from "./utils";
|
||||||
import {
|
import {
|
||||||
GDInlayHintsProvider,
|
GDInlayHintsProvider,
|
||||||
GDHoverProvider,
|
GDHoverProvider,
|
||||||
|
GDDocumentDropEditProvider,
|
||||||
GDDocumentLinkProvider,
|
GDDocumentLinkProvider,
|
||||||
GDSemanticTokensProvider,
|
GDSemanticTokensProvider,
|
||||||
GDCompletionItemProvider,
|
GDCompletionItemProvider,
|
||||||
@@ -21,9 +23,11 @@ import {
|
|||||||
find_project_file,
|
find_project_file,
|
||||||
register_command,
|
register_command,
|
||||||
set_context,
|
set_context,
|
||||||
|
get_editor_data_dir,
|
||||||
get_project_dir,
|
get_project_dir,
|
||||||
get_project_version,
|
get_project_version,
|
||||||
verify_godot_version,
|
verify_godot_version,
|
||||||
|
convert_uri_to_resource_path,
|
||||||
} 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";
|
||||||
@@ -34,6 +38,7 @@ interface Extension {
|
|||||||
debug?: GodotDebugger;
|
debug?: GodotDebugger;
|
||||||
scenePreviewProvider?: ScenePreviewProvider;
|
scenePreviewProvider?: ScenePreviewProvider;
|
||||||
linkProvider?: GDDocumentLinkProvider;
|
linkProvider?: GDDocumentLinkProvider;
|
||||||
|
dropsProvider?: GDDocumentDropEditProvider;
|
||||||
hoverProvider?: GDHoverProvider;
|
hoverProvider?: GDHoverProvider;
|
||||||
inlayProvider?: GDInlayHintsProvider;
|
inlayProvider?: GDInlayHintsProvider;
|
||||||
formattingProvider?: FormattingProvider;
|
formattingProvider?: FormattingProvider;
|
||||||
@@ -54,6 +59,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
globals.debug = new GodotDebugger(context);
|
globals.debug = new GodotDebugger(context);
|
||||||
globals.scenePreviewProvider = new ScenePreviewProvider(context);
|
globals.scenePreviewProvider = new ScenePreviewProvider(context);
|
||||||
globals.linkProvider = new GDDocumentLinkProvider(context);
|
globals.linkProvider = new GDDocumentLinkProvider(context);
|
||||||
|
globals.dropsProvider = new GDDocumentDropEditProvider(context);
|
||||||
globals.hoverProvider = new GDHoverProvider(context);
|
globals.hoverProvider = new GDHoverProvider(context);
|
||||||
globals.inlayProvider = new GDInlayHintsProvider(context);
|
globals.inlayProvider = new GDInlayHintsProvider(context);
|
||||||
globals.formattingProvider = new FormattingProvider(context);
|
globals.formattingProvider = new FormattingProvider(context);
|
||||||
@@ -65,13 +71,14 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
register_command("openEditor", open_workspace_with_editor),
|
register_command("openEditor", open_workspace_with_editor),
|
||||||
|
register_command("openEditorSettings", open_godot_editor_settings),
|
||||||
register_command("copyResourcePath", copy_resource_path),
|
register_command("copyResourcePath", copy_resource_path),
|
||||||
register_command("listGodotClasses", list_classes),
|
register_command("listGodotClasses", list_classes),
|
||||||
register_command("switchSceneScript", switch_scene_script),
|
register_command("switchSceneScript", switch_scene_script),
|
||||||
register_command("getGodotPath", get_godot_path),
|
register_command("getGodotPath", get_godot_path),
|
||||||
);
|
);
|
||||||
|
|
||||||
set_context("godotFiles", ["gdscript", "gdscene", "gdresource", "gdshader",]);
|
set_context("godotFiles", ["gdscript", "gdscene", "gdresource", "gdshader"]);
|
||||||
set_context("sceneLikeFiles", ["gdscript", "gdscene"]);
|
set_context("sceneLikeFiles", ["gdscript", "gdscene"]);
|
||||||
|
|
||||||
get_project_version().then(async () => {
|
get_project_version().then(async () => {
|
||||||
@@ -116,19 +123,12 @@ export function deactivate(): Thenable<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy_resource_path(uri: vscode.Uri) {
|
async function copy_resource_path(uri: vscode.Uri) {
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
uri = vscode.window.activeTextEditor.document.uri;
|
uri = vscode.window.activeTextEditor.document.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
const project_dir = path.dirname(find_project_file(uri.fsPath));
|
const relative_path = await convert_uri_to_resource_path(uri);
|
||||||
if (project_dir === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative_path = path.normalize(path.relative(project_dir, uri.fsPath));
|
|
||||||
relative_path = relative_path.split(path.sep).join(path.posix.sep);
|
|
||||||
relative_path = "res://" + relative_path;
|
|
||||||
|
|
||||||
vscode.env.clipboard.writeText(relative_path);
|
vscode.env.clipboard.writeText(relative_path);
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ async function open_workspace_with_editor() {
|
|||||||
if (get_configuration("editor.verbose")) {
|
if (get_configuration("editor.verbose")) {
|
||||||
command += " -v";
|
command += " -v";
|
||||||
}
|
}
|
||||||
const existingTerminal = vscode.window.terminals.find(t => t.name === "Godot Editor");
|
const existingTerminal = vscode.window.terminals.find((t) => t.name === "Godot Editor");
|
||||||
if (existingTerminal) {
|
if (existingTerminal) {
|
||||||
existingTerminal.dispose();
|
existingTerminal.dispose();
|
||||||
}
|
}
|
||||||
@@ -195,6 +195,39 @@ async function open_workspace_with_editor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function open_godot_editor_settings() {
|
||||||
|
const dir = get_editor_data_dir();
|
||||||
|
const files = fs.readdirSync(dir).filter((v) => v.endsWith(".tres"));
|
||||||
|
|
||||||
|
const ver = await get_project_version();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.includes(ver)) {
|
||||||
|
files.unshift(files.splice(files.indexOf(file), 1)[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const choices: vscode.QuickPickItem[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const pick: vscode.QuickPickItem = {
|
||||||
|
label: file,
|
||||||
|
description: path.join(dir, file),
|
||||||
|
};
|
||||||
|
choices.push(pick);
|
||||||
|
}
|
||||||
|
|
||||||
|
vscode.window.showQuickPick(choices).then(async (item) => {
|
||||||
|
if (item === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _path = path.join(dir, item.label);
|
||||||
|
const doc = await vscode.workspace.openTextDocument(_path);
|
||||||
|
vscode.window.showTextDocument(doc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the executable path for Godot based on the current project's version.
|
* Returns the executable path for Godot based on the current project's version.
|
||||||
* Created to allow other extensions to get the path without having to go
|
* Created to allow other extensions to get the path without having to go
|
||||||
@@ -202,7 +235,7 @@ async function open_workspace_with_editor() {
|
|||||||
* value (godotTools.editorPath.godot3/4).
|
* value (godotTools.editorPath.godot3/4).
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async function get_godot_path(): Promise<string|undefined> {
|
async function get_godot_path(): Promise<string | undefined> {
|
||||||
const projectVersion = await get_project_version();
|
const projectVersion = await get_project_version();
|
||||||
if (projectVersion === undefined) {
|
if (projectVersion === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -217,7 +250,7 @@ class GodotEditorTerminal implements vscode.Pseudoterminal {
|
|||||||
private closeEmitter = new vscode.EventEmitter<number>();
|
private closeEmitter = new vscode.EventEmitter<number>();
|
||||||
onDidClose?: vscode.Event<number> = this.closeEmitter.event;
|
onDidClose?: vscode.Event<number> = this.closeEmitter.event;
|
||||||
|
|
||||||
constructor(private command: string) { }
|
constructor(private command: string) {}
|
||||||
|
|
||||||
open(initialDimensions: vscode.TerminalDimensions | undefined): void {
|
open(initialDimensions: vscode.TerminalDimensions | undefined): void {
|
||||||
const proc = subProcess("GodotEditor", this.command, { shell: true, detached: true });
|
const proc = subProcess("GodotEditor", this.command, { shell: true, detached: true });
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ func f():
|
|||||||
x %= 1
|
x %= 1
|
||||||
x = 2 ** 2
|
x = 2 ** 2
|
||||||
x = 2 * -1
|
x = 2 * -1
|
||||||
|
x **= 2
|
||||||
|
|
||||||
# bitwise
|
# bitwise
|
||||||
x |= 1
|
x |= 1
|
||||||
@@ -21,4 +22,12 @@ func f():
|
|||||||
|
|
||||||
x = 1 << 1 | 1 >> 3
|
x = 1 << 1 | 1 >> 3
|
||||||
x = 1 << 1 & 1 >> 3
|
x = 1 << 1 & 1 >> 3
|
||||||
x = 1 ^ ~1
|
x = 1 ^ ~1
|
||||||
|
|
||||||
|
print(x == 1)
|
||||||
|
print(x <= 1)
|
||||||
|
print(x >= 1)
|
||||||
|
|
||||||
|
var ij := 1
|
||||||
|
var k := -ij + 1
|
||||||
|
var m := 0 + -ij
|
||||||
|
|||||||
25
src/formatter/snapshots/setters_getters.gd
Normal file
25
src/formatter/snapshots/setters_getters.gd
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# --- IN ---
|
||||||
|
func __get():
|
||||||
|
pass
|
||||||
|
func __set(val):
|
||||||
|
pass
|
||||||
|
|
||||||
|
var a: get = __get, set = __set
|
||||||
|
|
||||||
|
var b:
|
||||||
|
get = __get,
|
||||||
|
set = __set
|
||||||
|
|
||||||
|
var c = '':
|
||||||
|
get: return __get()
|
||||||
|
set(val): __set(val)
|
||||||
|
|
||||||
|
var d = '':
|
||||||
|
get:
|
||||||
|
print('get')
|
||||||
|
return __get()
|
||||||
|
set(val):
|
||||||
|
print('set')
|
||||||
|
__set(val)
|
||||||
|
|
||||||
|
var e = '' setget __get, __set
|
||||||
@@ -186,15 +186,15 @@ function between(tokens: Token[], current: number, options: FormatterOptions) {
|
|||||||
if (prev === "@") return "";
|
if (prev === "@") return "";
|
||||||
|
|
||||||
if (prev === "-" || prev === "+") {
|
if (prev === "-" || prev === "+") {
|
||||||
if (nextToken.identifier) return " ";
|
|
||||||
if (next === "(") return " ";
|
if (next === "(") return " ";
|
||||||
if (current === 1) return "";
|
|
||||||
if (["keyword", "symbol"].includes(tokens[current - 2]?.type)) {
|
if (["keyword", "symbol"].includes(tokens[current - 2]?.type)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if ([",", "(", "["].includes(tokens[current - 2]?.value)) {
|
if ([",", "(", "["].includes(tokens[current - 2]?.value)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if (nextToken.identifier) return " ";
|
||||||
|
if (current === 1) return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prev === ":" && next === "=") return "";
|
if (prev === ":" && next === "=") return "";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ enum ManagerStatus {
|
|||||||
DISCONNECTED = 4,
|
DISCONNECTED = 4,
|
||||||
CONNECTED = 5,
|
CONNECTED = 5,
|
||||||
RETRYING = 6,
|
RETRYING = 6,
|
||||||
|
WRONG_WORKSPACE = 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClientConnectionManager {
|
export class ClientConnectionManager {
|
||||||
@@ -211,6 +212,9 @@ export class ClientConnectionManager {
|
|||||||
case ManagerStatus.RETRYING:
|
case ManagerStatus.RETRYING:
|
||||||
this.show_retrying_prompt();
|
this.show_retrying_prompt();
|
||||||
break;
|
break;
|
||||||
|
case ManagerStatus.WRONG_WORKSPACE:
|
||||||
|
this.retry_connect_client();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +257,10 @@ export class ClientConnectionManager {
|
|||||||
tooltip += `\n${this.connectedVersion}`;
|
tooltip += `\n${this.connectedVersion}`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case ManagerStatus.WRONG_WORKSPACE:
|
||||||
|
text = "$(x) Wrong Project";
|
||||||
|
tooltip = "Disconnected from the GDScript language server.";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.statusWidget.text = text;
|
this.statusWidget.text = text;
|
||||||
this.statusWidget.tooltip = tooltip;
|
this.statusWidget.tooltip = tooltip;
|
||||||
@@ -269,7 +277,7 @@ export class ClientConnectionManager {
|
|||||||
set_context("connectedToLSP", true);
|
set_context("connectedToLSP", true);
|
||||||
this.status = ManagerStatus.CONNECTED;
|
this.status = ManagerStatus.CONNECTED;
|
||||||
if (this.client.needsStart()) {
|
if (this.client.needsStart()) {
|
||||||
this.context.subscriptions.push(this.client.start());
|
this.client.start().then(() => log.info("LSP Client started"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ClientStatus.DISCONNECTED:
|
case ClientStatus.DISCONNECTED:
|
||||||
@@ -285,6 +293,10 @@ export class ClientConnectionManager {
|
|||||||
}
|
}
|
||||||
this.retry = true;
|
this.retry = true;
|
||||||
break;
|
break;
|
||||||
|
case ClientStatus.REJECTED:
|
||||||
|
this.status = ManagerStatus.WRONG_WORKSPACE;
|
||||||
|
this.retry = false;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import EventEmitter from "node:events";
|
import EventEmitter from "node:events";
|
||||||
|
import * as path from "node:path";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import {
|
import {
|
||||||
LanguageClient,
|
LanguageClient,
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
} from "vscode-languageclient/node";
|
} from "vscode-languageclient/node";
|
||||||
|
|
||||||
import { globals } from "../extension";
|
import { globals } from "../extension";
|
||||||
import { createLogger, get_configuration } from "../utils";
|
import { createLogger, get_configuration, get_project_dir } from "../utils";
|
||||||
import { MessageIO } from "./MessageIO";
|
import { MessageIO } from "./MessageIO";
|
||||||
|
|
||||||
const log = createLogger("lsp.client", { output: "Godot LSP" });
|
const log = createLogger("lsp.client", { output: "Godot LSP" });
|
||||||
@@ -19,6 +20,7 @@ export enum ClientStatus {
|
|||||||
PENDING = 0,
|
PENDING = 0,
|
||||||
DISCONNECTED = 1,
|
DISCONNECTED = 1,
|
||||||
CONNECTED = 2,
|
CONNECTED = 2,
|
||||||
|
REJECTED = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TargetLSP {
|
export enum TargetLSP {
|
||||||
@@ -29,7 +31,7 @@ export enum TargetLSP {
|
|||||||
export type Target = {
|
export type Target = {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
type: TargetLSP;
|
type: TargetLSP;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HoverResult = {
|
type HoverResult = {
|
||||||
@@ -55,6 +57,13 @@ type HoverResponseMesssage = {
|
|||||||
result: HoverResult;
|
result: HoverResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ChangeWorkspaceNotification = {
|
||||||
|
method: string;
|
||||||
|
params: {
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default class GDScriptLanguageClient extends LanguageClient {
|
export default class GDScriptLanguageClient extends LanguageClient {
|
||||||
public io: MessageIO = new MessageIO();
|
public io: MessageIO = new MessageIO();
|
||||||
|
|
||||||
@@ -63,6 +72,8 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
|||||||
public port = -1;
|
public port = -1;
|
||||||
public lastPortTried = -1;
|
public lastPortTried = -1;
|
||||||
public sentMessages = new Map();
|
public sentMessages = new Map();
|
||||||
|
private initMessage: RequestMessage;
|
||||||
|
private rejected = false;
|
||||||
|
|
||||||
events = new EventEmitter();
|
events = new EventEmitter();
|
||||||
|
|
||||||
@@ -85,9 +96,6 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
|||||||
{ scheme: "file", language: "gdscript" },
|
{ scheme: "file", language: "gdscript" },
|
||||||
{ scheme: "untitled", language: "gdscript" },
|
{ scheme: "untitled", language: "gdscript" },
|
||||||
],
|
],
|
||||||
synchronize: {
|
|
||||||
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
super("GDScriptLanguageClient", serverOptions, clientOptions);
|
super("GDScriptLanguageClient", serverOptions, clientOptions);
|
||||||
@@ -100,6 +108,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connect(target: TargetLSP = TargetLSP.EDITOR) {
|
connect(target: TargetLSP = TargetLSP.EDITOR) {
|
||||||
|
this.rejected = false;
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.status = ClientStatus.PENDING;
|
this.status = ClientStatus.PENDING;
|
||||||
|
|
||||||
@@ -122,15 +131,38 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
|||||||
this.io.connect(host, port);
|
this.io.connect(host, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async send_request(method: string, params) {
|
||||||
|
try {
|
||||||
|
return this.sendRequest(method, params);
|
||||||
|
} catch {
|
||||||
|
log.warn("sending request failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private request_filter(message: RequestMessage) {
|
private request_filter(message: RequestMessage) {
|
||||||
|
if (this.rejected) {
|
||||||
|
if (message.method === "shutdown") {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this.sentMessages.set(message.id, message);
|
this.sentMessages.set(message.id, message);
|
||||||
|
|
||||||
|
if (!this.initMessage && message.method === "initialize") {
|
||||||
|
this.initMessage = message;
|
||||||
|
}
|
||||||
// discard outgoing messages that we know aren't supported
|
// discard outgoing messages that we know aren't supported
|
||||||
if (message.method === "didChangeWatchedFiles") {
|
// if (message.method === "textDocument/didSave") {
|
||||||
return;
|
// return false;
|
||||||
|
// }
|
||||||
|
// if (message.method === "textDocument/willSaveWaitUntil") {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
if (message.method === "workspace/didChangeWatchedFiles") {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (message.method === "workspace/symbol") {
|
if (message.method === "workspace/symbol") {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
@@ -165,9 +197,19 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async check_workspace(message: ChangeWorkspaceNotification) {
|
||||||
|
const server_path = path.normalize(message.params.path);
|
||||||
|
const client_path = path.normalize(await get_project_dir());
|
||||||
|
if (server_path !== client_path) {
|
||||||
|
log.warn("Connected LSP is a different workspace");
|
||||||
|
this.io.socket.resetAndDestroy();
|
||||||
|
this.rejected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private notification_filter(message: NotificationMessage) {
|
private notification_filter(message: NotificationMessage) {
|
||||||
if (message.method === "gdscript_client/changeWorkspace") {
|
if (message.method === "gdscript_client/changeWorkspace") {
|
||||||
//
|
this.check_workspace(message as ChangeWorkspaceNotification);
|
||||||
}
|
}
|
||||||
if (message.method === "gdscript/capabilities") {
|
if (message.method === "gdscript/capabilities") {
|
||||||
globals.docsProvider.register_capabilities(message);
|
globals.docsProvider.register_capabilities(message);
|
||||||
@@ -194,9 +236,8 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
|||||||
textDocument: { uri: uri.toString() },
|
textDocument: { uri: uri.toString() },
|
||||||
position: { line: position.line, character: position.character },
|
position: { line: position.line, character: position.character },
|
||||||
};
|
};
|
||||||
const response: HoverResult = await this.sendRequest("textDocument/hover", params);
|
const response = await this.send_request("textDocument/hover", params);
|
||||||
|
return this.parse_hover_result(response as HoverResult);
|
||||||
return this.parse_hover_result(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private parse_hover_result(message: HoverResult) {
|
private parse_hover_result(message: HoverResult) {
|
||||||
@@ -233,9 +274,17 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
|||||||
|
|
||||||
const host = get_configuration("lsp.serverHost");
|
const host = get_configuration("lsp.serverHost");
|
||||||
log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
|
log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
|
||||||
|
|
||||||
|
if (this.initMessage) {
|
||||||
|
this.send_request(this.initMessage.method, this.initMessage.params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private on_disconnected() {
|
private on_disconnected() {
|
||||||
|
if (this.rejected) {
|
||||||
|
this.status = ClientStatus.REJECTED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.target === TargetLSP.EDITOR) {
|
if (this.target === TargetLSP.EDITOR) {
|
||||||
const host = get_configuration("lsp.serverHost");
|
const host = get_configuration("lsp.serverHost");
|
||||||
let port = get_configuration("lsp.serverPort");
|
let port = get_configuration("lsp.serverPort");
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ export class MessageIO extends EventEmitter {
|
|||||||
reader = new MessageIOReader(this);
|
reader = new MessageIOReader(this);
|
||||||
writer = new MessageIOWriter(this);
|
writer = new MessageIOWriter(this);
|
||||||
|
|
||||||
requestFilter: (msg: RequestMessage) => RequestMessage = (msg) => msg;
|
requestFilter: (msg: RequestMessage) => RequestMessage | false = (msg) => msg;
|
||||||
responseFilter: (msg: ResponseMessage) => ResponseMessage = (msg) => msg;
|
responseFilter: (msg: ResponseMessage) => ResponseMessage | false = (msg) => msg;
|
||||||
notificationFilter: (msg: NotificationMessage) => NotificationMessage = (msg) => msg;
|
notificationFilter: (msg: NotificationMessage) => NotificationMessage | false = (msg) => msg;
|
||||||
|
|
||||||
socket: Socket = null;
|
socket: Socket = null;
|
||||||
messageCache: string[] = [];
|
messageCache: string[] = [];
|
||||||
@@ -49,7 +49,7 @@ export class MessageIO extends EventEmitter {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
socket.on("data", (chunk: Buffer) => {
|
socket.on("data", (chunk: Buffer) => {
|
||||||
this.emit("data", chunk.toString());
|
this.emit("data", chunk);
|
||||||
});
|
});
|
||||||
// socket.on("end", this.on_disconnected.bind(this));
|
// socket.on("end", this.on_disconnected.bind(this));
|
||||||
socket.on("error", () => {
|
socket.on("error", () => {
|
||||||
@@ -100,7 +100,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
|
|||||||
}
|
}
|
||||||
const json = JSON.parse(msg);
|
const json = JSON.parse(msg);
|
||||||
// allow message to be modified
|
// allow message to be modified
|
||||||
let modified: ResponseMessage | NotificationMessage;
|
let modified: ResponseMessage | NotificationMessage | false;
|
||||||
if ("id" in json) {
|
if ("id" in json) {
|
||||||
modified = this.io.responseFilter(json);
|
modified = this.io.responseFilter(json);
|
||||||
} else if ("method" in json) {
|
} else if ("method" in json) {
|
||||||
@@ -109,7 +109,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
|
|||||||
log.warn("rx [unhandled]:", json);
|
log.warn("rx [unhandled]:", json);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modified) {
|
if (modified === false) {
|
||||||
log.debug("rx [discarded]:", json);
|
log.debug("rx [discarded]:", json);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ export class MessageIOWriter extends AbstractMessageWriter implements MessageWri
|
|||||||
|
|
||||||
async write(msg: RequestMessage) {
|
async write(msg: RequestMessage) {
|
||||||
const modified = this.io.requestFilter(msg);
|
const modified = this.io.requestFilter(msg);
|
||||||
if (!modified) {
|
if (modified === false) {
|
||||||
log.debug("tx [discarded]:", msg);
|
log.debug("tx [discarded]:", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/providers/document_drops.ts
Normal file
123
src/providers/document_drops.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import * as path from "node:path";
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import {
|
||||||
|
CancellationToken,
|
||||||
|
DataTransfer,
|
||||||
|
DocumentDropEdit,
|
||||||
|
DocumentDropEditProvider,
|
||||||
|
ExtensionContext,
|
||||||
|
languages,
|
||||||
|
Position,
|
||||||
|
ProviderResult,
|
||||||
|
Range,
|
||||||
|
TextDocument,
|
||||||
|
Uri,
|
||||||
|
} from "vscode";
|
||||||
|
import { SceneParser } from "../scene_tools/parser";
|
||||||
|
import { createLogger, node_name_to_snake, get_project_version, convert_uri_to_resource_path } from "../utils";
|
||||||
|
import { SceneNode } from "../scene_tools/types";
|
||||||
|
|
||||||
|
const log = createLogger("providers.drops");
|
||||||
|
|
||||||
|
export class GDDocumentDropEditProvider implements DocumentDropEditProvider {
|
||||||
|
public parser = new SceneParser();
|
||||||
|
|
||||||
|
constructor(private context: ExtensionContext) {
|
||||||
|
const dropEditSelector = [
|
||||||
|
{ language: "csharp", scheme: "file" },
|
||||||
|
{ language: "gdscript", scheme: "file" },
|
||||||
|
];
|
||||||
|
context.subscriptions.push(languages.registerDocumentDropEditProvider(dropEditSelector, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async provideDocumentDropEdits(
|
||||||
|
document: TextDocument,
|
||||||
|
position: Position,
|
||||||
|
dataTransfer: DataTransfer,
|
||||||
|
token: CancellationToken,
|
||||||
|
): Promise<DocumentDropEdit> {
|
||||||
|
// log.debug("provideDocumentDropEdits", document, dataTransfer);
|
||||||
|
|
||||||
|
const targetResPath = await convert_uri_to_resource_path(document.uri);
|
||||||
|
|
||||||
|
const originFsPath = dataTransfer.get("godot/scene").value;
|
||||||
|
const originUri = vscode.Uri.file(originFsPath);
|
||||||
|
|
||||||
|
const originDocument = await vscode.workspace.openTextDocument(originUri);
|
||||||
|
const scene = await this.parser.parse_scene(originDocument);
|
||||||
|
|
||||||
|
let scriptId = "";
|
||||||
|
for (const res of scene.externalResources.values()) {
|
||||||
|
if (res.path === targetResPath) {
|
||||||
|
scriptId = res.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodePathOfTarget: SceneNode;
|
||||||
|
if (scriptId) {
|
||||||
|
const find_node = () => {
|
||||||
|
if (scene.root.scriptId === scriptId) {
|
||||||
|
return scene.root;
|
||||||
|
}
|
||||||
|
for (const node of scene.nodes.values()) {
|
||||||
|
if (node.scriptId === scriptId) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
nodePathOfTarget = find_node();
|
||||||
|
}
|
||||||
|
|
||||||
|
const className: string = dataTransfer.get("godot/class")?.value;
|
||||||
|
if (className) {
|
||||||
|
const nodePath: string = dataTransfer.get("godot/path")?.value;
|
||||||
|
let relativePath: string = dataTransfer.get("godot/relativePath")?.value;
|
||||||
|
const unique = dataTransfer.get("godot/unique")?.value === "true";
|
||||||
|
const label: string = dataTransfer.get("godot/label")?.value;
|
||||||
|
|
||||||
|
if (nodePathOfTarget) {
|
||||||
|
const targetPath = path.normalize(path.relative(nodePathOfTarget?.path, nodePath));
|
||||||
|
relativePath = targetPath.split(path.sep).join(path.posix.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the root node, the path is empty and needs to be replaced with the node name
|
||||||
|
let savePath = relativePath || label;
|
||||||
|
|
||||||
|
if (document.languageId === "gdscript") {
|
||||||
|
if (savePath.startsWith(".")) {
|
||||||
|
savePath = `'${savePath}'`;
|
||||||
|
}
|
||||||
|
let qualifiedPath = `$${savePath}`;
|
||||||
|
|
||||||
|
if (unique) {
|
||||||
|
// For unique nodes, we can use the % syntax and drop the full path
|
||||||
|
qualifiedPath = `%${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = document.lineAt(position.line);
|
||||||
|
if (line.text === "") {
|
||||||
|
// We assume that if the user is dropping a node in an empty line, they are at the top of
|
||||||
|
// the script and want to declare an onready variable
|
||||||
|
|
||||||
|
const snippet = new vscode.SnippetString();
|
||||||
|
|
||||||
|
if ((await get_project_version())?.startsWith("4")) {
|
||||||
|
snippet.appendText("@");
|
||||||
|
}
|
||||||
|
snippet.appendText("onready var ");
|
||||||
|
snippet.appendPlaceholder(node_name_to_snake(label));
|
||||||
|
snippet.appendText(`: ${className} = ${qualifiedPath}`);
|
||||||
|
return new vscode.DocumentDropEdit(snippet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In any other place, we assume the user wants to get a reference to the node itself
|
||||||
|
return new vscode.DocumentDropEdit(qualifiedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.languageId === "csharp") {
|
||||||
|
return new vscode.DocumentDropEdit(`GetNode<${className}>("${savePath}")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
|
|||||||
const uri = Uri.from({
|
const uri = Uri.from({
|
||||||
scheme: "file",
|
scheme: "file",
|
||||||
path: path,
|
path: path,
|
||||||
fragment: `${scene.externalResources[id].line},0`,
|
fragment: `${scene.externalResources.get(id).line},0`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const r = this.create_range(document, match);
|
const r = this.create_range(document, match);
|
||||||
@@ -54,7 +54,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
|
|||||||
const uri = Uri.from({
|
const uri = Uri.from({
|
||||||
scheme: "file",
|
scheme: "file",
|
||||||
path: path,
|
path: path,
|
||||||
fragment: `${scene.subResources[id].line},0`,
|
fragment: `${scene.subResources.get(id).line},0`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const r = this.create_range(document, match);
|
const r = this.create_range(document, match);
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
|||||||
symbol_name: className,
|
symbol_name: className,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await globals.lsp.client.sendRequest("textDocument/nativeSymbol", params);
|
const response = await globals.lsp.client.send_request("textDocument/nativeSymbol", params);
|
||||||
|
|
||||||
symbol = response as GodotNativeSymbol;
|
symbol = response as GodotNativeSymbol;
|
||||||
symbol.class_info = this.classInfo.get(symbol.name);
|
symbol.class_info = this.classInfo.get(symbol.name);
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ export class GDHoverProvider implements HoverProvider {
|
|||||||
if (word.startsWith("ExtResource")) {
|
if (word.startsWith("ExtResource")) {
|
||||||
const match = word.match(wordPattern);
|
const match = word.match(wordPattern);
|
||||||
const id = match[1];
|
const id = match[1];
|
||||||
const resource = scene.externalResources[id];
|
const resource = scene.externalResources.get(id);
|
||||||
const definition = scene.externalResources[id].body;
|
const definition = resource.body;
|
||||||
const links = await this.get_links(definition);
|
const links = await this.get_links(definition);
|
||||||
|
|
||||||
const contents = new MarkdownString();
|
const contents = new MarkdownString();
|
||||||
@@ -77,7 +77,7 @@ export class GDHoverProvider implements HoverProvider {
|
|||||||
const match = word.match(wordPattern);
|
const match = word.match(wordPattern);
|
||||||
const id = match[1];
|
const id = match[1];
|
||||||
|
|
||||||
let definition = scene.subResources[id].body;
|
let definition = scene.subResources.get(id).body;
|
||||||
// don't display contents of giant arrays
|
// don't display contents of giant arrays
|
||||||
definition = definition?.replace(/Array\([0-9,\.\- ]*\)/, "Array(...)");
|
definition = definition?.replace(/Array\([0-9,\.\- ]*\)/, "Array(...)");
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
export * from "./completions";
|
export * from "./completions";
|
||||||
export * from "./definition";
|
export * from "./definition";
|
||||||
|
export * from "./document_drops";
|
||||||
export * from "./document_link";
|
export * from "./document_link";
|
||||||
|
export * from "./documentation";
|
||||||
export * from "./hover";
|
export * from "./hover";
|
||||||
export * from "./inlay_hints";
|
export * from "./inlay_hints";
|
||||||
export * from "./semantic_tokens";
|
export * from "./semantic_tokens";
|
||||||
export * from "./documentation";
|
|
||||||
export * from "./tasks";
|
export * from "./tasks";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function fromDetail(detail: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addByHover(document: TextDocument, hoverPosition: vscode.Position, start: vscode.Position): Promise<InlayHint | undefined> {
|
async function addByHover(document: TextDocument, hoverPosition: vscode.Position, start: vscode.Position): Promise<InlayHint | undefined> {
|
||||||
const response = await globals.lsp.client.sendRequest("textDocument/hover", {
|
const response = await globals.lsp.client.send_request("textDocument/hover", {
|
||||||
textDocument: { uri: document.uri.toString() },
|
textDocument: { uri: document.uri.toString() },
|
||||||
position: {
|
position: {
|
||||||
line: hoverPosition.line,
|
line: hoverPosition.line,
|
||||||
@@ -65,10 +65,12 @@ export class GDInlayHintsProvider implements InlayHintsProvider {
|
|||||||
if (!get_configuration("inlayHints.gdscript", true)) {
|
if (!get_configuration("inlayHints.gdscript", true)) {
|
||||||
return hints;
|
return hints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!globals.lsp.client.isRunning()) {
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
||||||
await globals.lsp.client.onReady();
|
const symbolsRequest = await globals.lsp.client.send_request("textDocument/documentSymbol", {
|
||||||
|
|
||||||
const symbolsRequest = await globals.lsp.client.sendRequest("textDocument/documentSymbol", {
|
|
||||||
textDocument: { uri: document.uri.toString() },
|
textDocument: { uri: document.uri.toString() },
|
||||||
}) as unknown[];
|
}) as unknown[];
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ export class GDInlayHintsProvider implements InlayHintsProvider {
|
|||||||
for (const match of text.matchAll(/ExtResource\(\s?"?(\w+)\s?"?\)/g)) {
|
for (const match of text.matchAll(/ExtResource\(\s?"?(\w+)\s?"?\)/g)) {
|
||||||
const id = match[1];
|
const id = match[1];
|
||||||
const end = document.positionAt(match.index + match[0].length);
|
const end = document.positionAt(match.index + match[0].length);
|
||||||
const resource = scene.externalResources[id];
|
const resource = scene.externalResources.get(id);
|
||||||
|
|
||||||
const label = `${resource.type}: "${resource.path}"`;
|
const label = `${resource.type}: "${resource.path}"`;
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ export class GDInlayHintsProvider implements InlayHintsProvider {
|
|||||||
for (const match of text.matchAll(/SubResource\(\s?"?(\w+)\s?"?\)/g)) {
|
for (const match of text.matchAll(/SubResource\(\s?"?(\w+)\s?"?\)/g)) {
|
||||||
const id = match[1];
|
const id = match[1];
|
||||||
const end = document.positionAt(match.index + match[0].length);
|
const end = document.positionAt(match.index + match[0].length);
|
||||||
const resource = scene.subResources[id];
|
const resource = scene.subResources.get(id);
|
||||||
|
|
||||||
const label = `${resource.type}`;
|
const label = `${resource.type}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import { basename, extname } from "node:path";
|
||||||
import { TextDocument, Uri } from "vscode";
|
import { TextDocument, Uri } from "vscode";
|
||||||
import { basename, extname } from "path";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { SceneNode, Scene } from "./types";
|
import { SceneNode, Scene } from "./types";
|
||||||
import { createLogger } from "../utils";
|
import { createLogger } from "../utils";
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export class SceneParser {
|
|||||||
const uid = line.match(/uid="([\w:/]+)"/)?.[1];
|
const uid = line.match(/uid="([\w:/]+)"/)?.[1];
|
||||||
const id = line.match(/ id="?([\w]+)"?/)?.[1];
|
const id = line.match(/ id="?([\w]+)"?/)?.[1];
|
||||||
|
|
||||||
scene.externalResources[id] = {
|
scene.externalResources.set(id, {
|
||||||
body: line,
|
body: line,
|
||||||
path: path,
|
path: path,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -54,7 +54,7 @@ export class SceneParser {
|
|||||||
id: id,
|
id: id,
|
||||||
index: match.index,
|
index: match.index,
|
||||||
line: document.lineAt(document.positionAt(match.index)).lineNumber + 1,
|
line: document.lineAt(document.positionAt(match.index)).lineNumber + 1,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastResource = null;
|
let lastResource = null;
|
||||||
@@ -76,7 +76,7 @@ export class SceneParser {
|
|||||||
lastResource.body = text.slice(lastResource.index, match.index).trimEnd();
|
lastResource.body = text.slice(lastResource.index, match.index).trimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.subResources[id] = resource;
|
scene.subResources.set(id, resource);
|
||||||
lastResource = resource;
|
lastResource = resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +87,9 @@ export class SceneParser {
|
|||||||
const nodeRegex = /\[node.*/g;
|
const nodeRegex = /\[node.*/g;
|
||||||
for (const match of text.matchAll(nodeRegex)) {
|
for (const match of text.matchAll(nodeRegex)) {
|
||||||
const line = match[0];
|
const line = match[0];
|
||||||
const name = line.match(/name="([\w]+)"/)?.[1];
|
const name = line.match(/name="([^.:@/"%]+)"/)?.[1];
|
||||||
const type = line.match(/type="([\w]+)"/)?.[1] ?? "PackedScene";
|
const type = line.match(/type="([\w]+)"/)?.[1] ?? "PackedScene";
|
||||||
let parent = line.match(/parent="([\w\/.]+)"/)?.[1];
|
let parent = line.match(/parent="(([^.:@/"%]|[\/.])+)"/)?.[1];
|
||||||
const instance = line.match(/instance=ExtResource\(\s*"?([\w]+)"?\s*\)/)?.[1];
|
const instance = line.match(/instance=ExtResource\(\s*"?([\w]+)"?\s*\)/)?.[1];
|
||||||
|
|
||||||
// leaving this in case we have a reason to use these node paths in the future
|
// leaving this in case we have a reason to use these node paths in the future
|
||||||
@@ -134,9 +134,10 @@ export class SceneParser {
|
|||||||
scene.nodes.set(_path, node);
|
scene.nodes.set(_path, node);
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
if (instance in scene.externalResources) {
|
const res = scene.externalResources.get(instance);
|
||||||
node.tooltip = scene.externalResources[instance].path;
|
if (res) {
|
||||||
node.resourcePath = scene.externalResources[instance].path;
|
node.tooltip = res.path;
|
||||||
|
node.resourcePath = res.path;
|
||||||
if ([".tscn"].includes(extname(node.resourcePath))) {
|
if ([".tscn"].includes(extname(node.resourcePath))) {
|
||||||
node.contextValue += "openable";
|
node.contextValue += "openable";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,36 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import {
|
import {
|
||||||
|
type CancellationToken,
|
||||||
|
type Event,
|
||||||
|
EventEmitter,
|
||||||
|
type ExtensionContext,
|
||||||
|
type FileDecoration,
|
||||||
|
type ProviderResult,
|
||||||
type TreeDataProvider,
|
type TreeDataProvider,
|
||||||
type TreeDragAndDropController,
|
type TreeDragAndDropController,
|
||||||
type ExtensionContext,
|
|
||||||
EventEmitter,
|
|
||||||
type Event,
|
|
||||||
type TreeView,
|
|
||||||
type ProviderResult,
|
|
||||||
type TreeItem,
|
type TreeItem,
|
||||||
TreeItemCollapsibleState,
|
TreeItemCollapsibleState,
|
||||||
window,
|
type TreeView,
|
||||||
languages,
|
|
||||||
type Uri,
|
type Uri,
|
||||||
type CancellationToken,
|
window,
|
||||||
type FileDecoration,
|
|
||||||
type DocumentDropEditProvider,
|
|
||||||
workspace,
|
workspace,
|
||||||
} from "vscode";
|
} from "vscode";
|
||||||
import * as fs from "node:fs";
|
|
||||||
import {
|
import {
|
||||||
get_configuration,
|
|
||||||
find_file,
|
|
||||||
set_context,
|
|
||||||
convert_resource_path_to_uri,
|
convert_resource_path_to_uri,
|
||||||
register_command,
|
|
||||||
createLogger,
|
createLogger,
|
||||||
|
find_file,
|
||||||
|
get_configuration,
|
||||||
make_docs_uri,
|
make_docs_uri,
|
||||||
node_name_to_snake,
|
register_command,
|
||||||
|
set_context,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { SceneParser } from "./parser";
|
import { SceneParser } from "./parser";
|
||||||
import type { SceneNode, Scene } from "./types";
|
import type { Scene, SceneNode } from "./types";
|
||||||
|
|
||||||
const log = createLogger("scenes.preview");
|
const log = createLogger("scenes.preview");
|
||||||
|
|
||||||
export class ScenePreviewProvider
|
export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDragAndDropController<SceneNode> {
|
||||||
implements TreeDataProvider<SceneNode>, TreeDragAndDropController<SceneNode>, DocumentDropEditProvider
|
|
||||||
{
|
|
||||||
public dropMimeTypes = [];
|
public dropMimeTypes = [];
|
||||||
public dragMimeTypes = [];
|
public dragMimeTypes = [];
|
||||||
private tree: TreeView<SceneNode>;
|
private tree: TreeView<SceneNode>;
|
||||||
@@ -58,10 +53,6 @@ export class ScenePreviewProvider
|
|||||||
dragAndDropController: this,
|
dragAndDropController: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
const selector = [
|
|
||||||
{ language: "csharp", scheme: "file" },
|
|
||||||
{ language: "gdscript", scheme: "file" },
|
|
||||||
];
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
register_command("scenePreview.lock", this.lock_preview.bind(this)),
|
register_command("scenePreview.lock", this.lock_preview.bind(this)),
|
||||||
register_command("scenePreview.unlock", this.unlock_preview.bind(this)),
|
register_command("scenePreview.unlock", this.unlock_preview.bind(this)),
|
||||||
@@ -70,19 +61,26 @@ export class ScenePreviewProvider
|
|||||||
register_command("scenePreview.openScene", this.open_scene.bind(this)),
|
register_command("scenePreview.openScene", this.open_scene.bind(this)),
|
||||||
register_command("scenePreview.openScript", this.open_script.bind(this)),
|
register_command("scenePreview.openScript", this.open_script.bind(this)),
|
||||||
register_command("scenePreview.openCurrentScene", this.open_current_scene.bind(this)),
|
register_command("scenePreview.openCurrentScene", this.open_current_scene.bind(this)),
|
||||||
register_command("scenePreview.openCurrentScript", this.open_main_script.bind(this)),
|
register_command("scenePreview.openMainScript", this.open_main_script.bind(this)),
|
||||||
register_command("scenePreview.goToDefinition", this.go_to_definition.bind(this)),
|
register_command("scenePreview.goToDefinition", this.go_to_definition.bind(this)),
|
||||||
register_command("scenePreview.openDocumentation", this.open_documentation.bind(this)),
|
register_command("scenePreview.openDocumentation", this.open_documentation.bind(this)),
|
||||||
register_command("scenePreview.refresh", this.refresh.bind(this)),
|
register_command("scenePreview.refresh", this.refresh.bind(this)),
|
||||||
window.onDidChangeActiveTextEditor(this.refresh.bind(this)),
|
window.onDidChangeActiveTextEditor(this.text_editor_changed.bind(this)),
|
||||||
window.registerFileDecorationProvider(this.uniqueDecorator),
|
window.registerFileDecorationProvider(this.uniqueDecorator),
|
||||||
window.registerFileDecorationProvider(this.scriptDecorator),
|
window.registerFileDecorationProvider(this.scriptDecorator),
|
||||||
languages.registerDocumentDropEditProvider(selector, this),
|
|
||||||
this.watcher.onDidChange(this.on_file_changed.bind(this)),
|
this.watcher.onDidChange(this.on_file_changed.bind(this)),
|
||||||
this.watcher,
|
this.watcher,
|
||||||
this.tree.onDidChangeSelection(this.tree_selection_changed),
|
this.tree.onDidChangeSelection(this.tree_selection_changed),
|
||||||
this.tree,
|
this.tree,
|
||||||
);
|
);
|
||||||
|
const result: string | undefined = this.context.workspaceState.get("godotTools.scenePreview.lockedScene");
|
||||||
|
if (result) {
|
||||||
|
if (fs.existsSync(result)) {
|
||||||
|
set_context("scenePreview.locked", true);
|
||||||
|
this.scenePreviewLocked = true;
|
||||||
|
this.currentScene = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@@ -92,59 +90,15 @@ export class ScenePreviewProvider
|
|||||||
data: vscode.DataTransfer,
|
data: vscode.DataTransfer,
|
||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
): void | Thenable<void> {
|
): void | Thenable<void> {
|
||||||
data.set("godot/path", new vscode.DataTransferItem(source[0].relativePath));
|
data.set("godot/scene", new vscode.DataTransferItem(this.currentScene));
|
||||||
|
data.set("godot/node", new vscode.DataTransferItem(source[0]));
|
||||||
|
data.set("godot/path", new vscode.DataTransferItem(source[0].path));
|
||||||
|
data.set("godot/relativePath", new vscode.DataTransferItem(source[0].relativePath));
|
||||||
data.set("godot/class", new vscode.DataTransferItem(source[0].className));
|
data.set("godot/class", new vscode.DataTransferItem(source[0].className));
|
||||||
data.set("godot/unique", new vscode.DataTransferItem(source[0].unique));
|
data.set("godot/unique", new vscode.DataTransferItem(source[0].unique));
|
||||||
data.set("godot/label", new vscode.DataTransferItem(source[0].label));
|
data.set("godot/label", new vscode.DataTransferItem(source[0].label));
|
||||||
}
|
}
|
||||||
|
|
||||||
public provideDocumentDropEdits(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
dataTransfer: vscode.DataTransfer,
|
|
||||||
token: vscode.CancellationToken,
|
|
||||||
): vscode.ProviderResult<vscode.DocumentDropEdit> {
|
|
||||||
const path: string = dataTransfer.get("godot/path").value;
|
|
||||||
const className: string = dataTransfer.get("godot/class").value;
|
|
||||||
const line = document.lineAt(position.line);
|
|
||||||
const unique = dataTransfer.get("godot/unique").value === "true";
|
|
||||||
const label: string = dataTransfer.get("godot/label").value;
|
|
||||||
|
|
||||||
// TODO: compare the source scene to the target file
|
|
||||||
// What should happen when you drag a node into a script that isn't the
|
|
||||||
// "main" script for that scene?
|
|
||||||
// Attempt to calculate a relative path that resolves correctly?
|
|
||||||
|
|
||||||
if (className) {
|
|
||||||
// For the root node, the path is empty and needs to be replaced with the node name
|
|
||||||
const savePath = path || label;
|
|
||||||
|
|
||||||
if (document.languageId === "gdscript") {
|
|
||||||
let qualifiedPath = `$${savePath}`;
|
|
||||||
|
|
||||||
if (unique) {
|
|
||||||
// For unique nodes, we can use the % syntax and drop the full path
|
|
||||||
qualifiedPath = `%${label}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.text === "") {
|
|
||||||
// We assume that if the user is dropping a node in an empty line, they are at the top of
|
|
||||||
// the script and want to declare an onready variable
|
|
||||||
return new vscode.DocumentDropEdit(
|
|
||||||
`@onready var ${node_name_to_snake(label)}: ${className} = ${qualifiedPath}\n`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In any other place, we assume the user wants to get a reference to the node itself
|
|
||||||
return new vscode.DocumentDropEdit(qualifiedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.languageId === "csharp") {
|
|
||||||
return new vscode.DocumentDropEdit(`GetNode<${className}>("${savePath}")`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async on_file_changed(uri: vscode.Uri) {
|
public async on_file_changed(uri: vscode.Uri) {
|
||||||
if (!uri.fsPath.endsWith(".tscn")) {
|
if (!uri.fsPath.endsWith(".tscn")) {
|
||||||
return;
|
return;
|
||||||
@@ -159,11 +113,10 @@ export class ScenePreviewProvider
|
|||||||
}, 20);
|
}, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refresh() {
|
public async text_editor_changed() {
|
||||||
if (this.scenePreviewLocked) {
|
if (this.scenePreviewLocked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = vscode.window.activeTextEditor;
|
const editor = vscode.window.activeTextEditor;
|
||||||
if (editor) {
|
if (editor) {
|
||||||
let fileName = editor.document.uri.fsPath;
|
let fileName = editor.document.uri.fsPath;
|
||||||
@@ -196,24 +149,34 @@ export class ScenePreviewProvider
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const document = await vscode.workspace.openTextDocument(fileName);
|
|
||||||
this.scene = this.parser.parse_scene(document);
|
|
||||||
|
|
||||||
this.tree.message = this.scene.title;
|
|
||||||
this.currentScene = fileName;
|
this.currentScene = fileName;
|
||||||
|
this.refresh();
|
||||||
this.changeTreeEvent.fire();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async refresh() {
|
||||||
|
if (!fs.existsSync(this.currentScene)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = await vscode.workspace.openTextDocument(this.currentScene);
|
||||||
|
this.scene = this.parser.parse_scene(document);
|
||||||
|
|
||||||
|
this.tree.message = this.scene.title;
|
||||||
|
|
||||||
|
this.changeTreeEvent.fire();
|
||||||
|
}
|
||||||
|
|
||||||
private lock_preview() {
|
private lock_preview() {
|
||||||
this.scenePreviewLocked = true;
|
this.scenePreviewLocked = true;
|
||||||
set_context("scenePreview.locked", true);
|
set_context("scenePreview.locked", true);
|
||||||
|
this.context.workspaceState.update("godotTools.scenePreview.lockedScene", this.currentScene);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unlock_preview() {
|
private unlock_preview() {
|
||||||
this.scenePreviewLocked = false;
|
this.scenePreviewLocked = false;
|
||||||
set_context("scenePreview.locked", false);
|
set_context("scenePreview.locked", false);
|
||||||
|
this.context.workspaceState.update("godotTools.scenePreview.lockedScene", "");
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +200,7 @@ export class ScenePreviewProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async open_script(item: SceneNode) {
|
private async open_script(item: SceneNode) {
|
||||||
const path = this.scene.externalResources[item.scriptId].path;
|
const path = this.scene.externalResources.get(item.scriptId).path;
|
||||||
|
|
||||||
const uri = await convert_resource_path_to_uri(path);
|
const uri = await convert_resource_path_to_uri(path);
|
||||||
if (uri) {
|
if (uri) {
|
||||||
@@ -256,7 +219,7 @@ export class ScenePreviewProvider
|
|||||||
if (this.currentScene) {
|
if (this.currentScene) {
|
||||||
const root = this.scene.root;
|
const root = this.scene.root;
|
||||||
if (root?.hasScript) {
|
if (root?.hasScript) {
|
||||||
const path = this.scene.externalResources[root.scriptId].path;
|
const path = this.scene.externalResources.get(root.scriptId).path;
|
||||||
const uri = await convert_resource_path_to_uri(path);
|
const uri = await convert_resource_path_to_uri(path);
|
||||||
if (uri) {
|
if (uri) {
|
||||||
vscode.window.showTextDocument(uri, { preview: true });
|
vscode.window.showTextDocument(uri, { preview: true });
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class SceneNode extends TreeItem {
|
|||||||
this.scriptId = line.match(/script = ExtResource\(\s*"?([\w]+)"?\s*\)/)[1];
|
this.scriptId = line.match(/script = ExtResource\(\s*"?([\w]+)"?\s*\)/)[1];
|
||||||
this.contextValue += "hasScript";
|
this.contextValue += "hasScript";
|
||||||
}
|
}
|
||||||
if (line != "") {
|
if (line !== "") {
|
||||||
newLines.push(line);
|
newLines.push(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ export class Scene {
|
|||||||
public title: string;
|
public title: string;
|
||||||
public mtime: number;
|
public mtime: number;
|
||||||
public root: SceneNode | undefined;
|
public root: SceneNode | undefined;
|
||||||
public externalResources: {[key: string]: GDResource} = {};
|
public externalResources: Map<string, GDResource> = new Map();
|
||||||
public subResources: {[key: string]: GDResource} = {};
|
public subResources: Map<string, GDResource> = new Map();
|
||||||
public nodes: Map<string, SceneNode> = new Map();
|
public nodes: Map<string, SceneNode> = new Map();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ import * as fs from "node:fs";
|
|||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
export function get_editor_data_dir(): string {
|
||||||
|
// from: https://stackoverflow.com/a/26227660
|
||||||
|
const appdata =
|
||||||
|
process.env.APPDATA ||
|
||||||
|
(process.platform === "darwin"
|
||||||
|
? `${process.env.HOME}/Library/Preferences`
|
||||||
|
: `${process.env.HOME}/.local/share`);
|
||||||
|
|
||||||
|
return path.join(appdata, "Godot");
|
||||||
|
}
|
||||||
|
|
||||||
let projectDir: string | undefined = undefined;
|
let projectDir: string | undefined = undefined;
|
||||||
let projectFile: string | undefined = undefined;
|
let projectFile: string | undefined = undefined;
|
||||||
|
|
||||||
@@ -33,10 +44,10 @@ export async function get_project_dir(): Promise<string | undefined> {
|
|||||||
}
|
}
|
||||||
projectFile = file;
|
projectFile = file;
|
||||||
projectDir = path.dirname(file);
|
projectDir = path.dirname(file);
|
||||||
if (os.platform() === "win32") {
|
if (os.platform() === "win32") {
|
||||||
// capitalize the drive letter in windows absolute paths
|
// capitalize the drive letter in windows absolute paths
|
||||||
projectDir = projectDir[0].toUpperCase() + projectDir.slice(1);
|
projectDir = projectDir[0].toUpperCase() + projectDir.slice(1);
|
||||||
}
|
}
|
||||||
return projectDir;
|
return projectDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +116,17 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
|
|||||||
return vscode.Uri.joinPath(vscode.Uri.file(dir), resPath.substring("res://".length));
|
return vscode.Uri.joinPath(vscode.Uri.file(dir), resPath.substring("res://".length));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function convert_uri_to_resource_path(uri: vscode.Uri): Promise<string | null> {
|
||||||
|
const project_dir = path.dirname(find_project_file(uri.fsPath));
|
||||||
|
if (project_dir === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let relative_path = path.normalize(path.relative(project_dir, uri.fsPath));
|
||||||
|
relative_path = relative_path.split(path.sep).join(path.posix.sep);
|
||||||
|
return `res://${relative_path}`;
|
||||||
|
}
|
||||||
|
|
||||||
export type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
|
export type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
|
||||||
export type VERIFY_RESULT = {
|
export type VERIFY_RESULT = {
|
||||||
status: VERIFY_STATUS;
|
status: VERIFY_STATUS;
|
||||||
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./project_utils";
|
export * from "./godot_utils";
|
||||||
export * from "./settings_updater";
|
export * from "./settings_updater";
|
||||||
export * from "./vscode_utils";
|
export * from "./vscode_utils";
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
},
|
},
|
||||||
"expression": {
|
"expression": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "include": "#base_expression" },
|
|
||||||
{ "include": "#getter_setter_godot4" },
|
{ "include": "#getter_setter_godot4" },
|
||||||
|
{ "include": "#base_expression" },
|
||||||
{ "include": "#assignment_operator" },
|
{ "include": "#assignment_operator" },
|
||||||
{ "include": "#annotations" },
|
{ "include": "#annotations" },
|
||||||
{ "include": "#class_name" },
|
{ "include": "#class_name" },
|
||||||
@@ -74,6 +74,7 @@
|
|||||||
{ "include": "#square_braces" },
|
{ "include": "#square_braces" },
|
||||||
{ "include": "#round_braces" },
|
{ "include": "#round_braces" },
|
||||||
{ "include": "#function_call" },
|
{ "include": "#function_call" },
|
||||||
|
{ "include": "#region"},
|
||||||
{ "include": "#comment" },
|
{ "include": "#comment" },
|
||||||
{ "include": "#self" },
|
{ "include": "#self" },
|
||||||
{ "include": "#func" },
|
{ "include": "#func" },
|
||||||
@@ -83,6 +84,10 @@
|
|||||||
{ "include": "#line_continuation" }
|
{ "include": "#line_continuation" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"match": "#(end)?region.*$\\n?",
|
||||||
|
"name": "keyword.language.region.gdscript"
|
||||||
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"match": "(##|#).*$\\n?",
|
"match": "(##|#).*$\\n?",
|
||||||
"name": "comment.line.number-sign.gdscript",
|
"name": "comment.line.number-sign.gdscript",
|
||||||
@@ -170,7 +175,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ "include": "#base_expression" }
|
{ "include": "#expression" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"self": {
|
"self": {
|
||||||
@@ -229,7 +234,7 @@
|
|||||||
"name": "keyword.operator.comparison.gdscript"
|
"name": "keyword.operator.comparison.gdscript"
|
||||||
},
|
},
|
||||||
"arithmetic_operator": {
|
"arithmetic_operator": {
|
||||||
"match": "->|\\+=|-=|\\*=|\\^=|/=|%=|&=|~=|\\|=|\\*\\*|\\*|/|%|\\+|-",
|
"match": "->|\\+=|-=|\\*\\*=|\\*=|\\^=|/=|%=|&=|~=|\\|=|\\*\\*|\\*|/|%|\\+|-",
|
||||||
"name": "keyword.operator.arithmetic.gdscript"
|
"name": "keyword.operator.arithmetic.gdscript"
|
||||||
},
|
},
|
||||||
"assignment_operator": {
|
"assignment_operator": {
|
||||||
@@ -245,7 +250,7 @@
|
|||||||
"captures": { "1": { "name": "keyword.control.gdscript" } }
|
"captures": { "1": { "name": "keyword.control.gdscript" } }
|
||||||
},
|
},
|
||||||
"keywords": {
|
"keywords": {
|
||||||
"match": "\\b(?:class|class_name|abstract|is|onready|tool|static|export|as|void|enum|assert|breakpoint|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace)\\b",
|
"match": "\\b(?:class|class_name|abstract|is|onready|tool|static|export|as|void|enum|assert|breakpoint|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace|super)\\b",
|
||||||
"name": "keyword.language.gdscript"
|
"name": "keyword.language.gdscript"
|
||||||
},
|
},
|
||||||
"letter": {
|
"letter": {
|
||||||
@@ -267,7 +272,11 @@
|
|||||||
"name": "constant.numeric.float.gdscript"
|
"name": "constant.numeric.float.gdscript"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"match": "([0-9][0-9_]*)?\\.[0-9_]*([eE][+-]?[0-9_]+)?",
|
"match": "([0-9][0-9_]*)\\.[0-9_]*([eE][+-]?[0-9_]+)?",
|
||||||
|
"name": "constant.numeric.float.gdscript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "([0-9][0-9_]*)?\\.[0-9_]*([eE][+-]?[0-9_]+)",
|
||||||
"name": "constant.numeric.float.gdscript"
|
"name": "constant.numeric.float.gdscript"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -293,7 +302,7 @@
|
|||||||
"match": "(:)?\\s*(set|get)\\s+=\\s+([a-zA-Z_]\\w*)",
|
"match": "(:)?\\s*(set|get)\\s+=\\s+([a-zA-Z_]\\w*)",
|
||||||
"captures": {
|
"captures": {
|
||||||
"1": { "name": "punctuation.separator.annotation.gdscript" },
|
"1": { "name": "punctuation.separator.annotation.gdscript" },
|
||||||
"2": { "name": "keyword.language.gdscript storage.type.const.gdscript" },
|
"2": { "name": "entity.name.function.gdscript" },
|
||||||
"3": { "name": "entity.name.function.gdscript" }
|
"3": { "name": "entity.name.function.gdscript" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -311,7 +320,7 @@
|
|||||||
{
|
{
|
||||||
"match": "(setget)\\s+([a-zA-Z_]\\w*)(?:[,]\\s*([a-zA-Z_]\\w*))?",
|
"match": "(setget)\\s+([a-zA-Z_]\\w*)(?:[,]\\s*([a-zA-Z_]\\w*))?",
|
||||||
"captures": {
|
"captures": {
|
||||||
"1": { "name": "keyword.language.gdscript storage.type.const.gdscript" },
|
"1": { "name": "keyword.language.gdscript" },
|
||||||
"2": { "name": "entity.name.function.gdscript" },
|
"2": { "name": "entity.name.function.gdscript" },
|
||||||
"3": { "name": "entity.name.function.gdscript" }
|
"3": { "name": "entity.name.function.gdscript" }
|
||||||
}
|
}
|
||||||
@@ -326,18 +335,23 @@
|
|||||||
"getter_setter_godot4": {
|
"getter_setter_godot4": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"match": "\\b(get):",
|
"name": "meta.variable.declaration.getter.gdscript",
|
||||||
"captures": { "1": { "name": "entity.name.function.gdscript" } }
|
"match": "(get)\\s*(:)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "entity.name.function.gdscript" },
|
||||||
|
"2": { "name": "punctuation.separator.annotation.gdscript" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "meta.function.gdscript",
|
"name": "meta.variable.declaration.setter.gdscript",
|
||||||
"begin": "(?x) \\s+\n (set) \\s*\n (?=\\()",
|
"match": "(set)\\s*(\\()\\s*([A-Za-z_]\\w*)\\s*(\\))\\s*(:)",
|
||||||
"end": "(:|(?=[#'\"\\n]))",
|
"captures": {
|
||||||
"beginCaptures": { "1": { "name": "entity.name.function.gdscript" } },
|
"1": { "name": "entity.name.function.gdscript" },
|
||||||
"patterns": [
|
"2": { "name": "punctuation.definition.arguments.begin.gdscript" },
|
||||||
{ "include": "#parameters" },
|
"3": { "name": "variable.other.gdscript" },
|
||||||
{ "include": "#line_continuation" }
|
"4": { "name": "punctuation.definition.arguments.end.gdscript" },
|
||||||
]
|
"5": { "name": "punctuation.separator.annotation.gdscript" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -437,7 +451,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"builtin_classes": {
|
"builtin_classes": {
|
||||||
"match": "(?<![^.]\\.|:)\\b(Vector2|Vector2i|Vector3|Vector3i|Vector4|Vector4i|Color|Rect2|Rect2i|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|Transform3D|AABB|String|Color|NodePath|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray|bool|int|float|Signal|Callable|StringName|Quaternion|Projection|PackedByteArray|PackedInt32Array|PackedInt64Array|PackedFloat32Array|PackedFloat64Array|PackedStringArray|PackedVector2Array|PackedVector2iArray|PackedVector3Array|PackedVector3iArray|PackedVector4Array|PackedColorArray|super)\\b",
|
"match": "(?<![^.]\\.|:)\\b(Vector2|Vector2i|Vector3|Vector3i|Vector4|Vector4i|Color|Rect2|Rect2i|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|Transform3D|AABB|String|Color|NodePath|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray|bool|int|float|Signal|Callable|StringName|Quaternion|Projection|PackedByteArray|PackedInt32Array|PackedInt64Array|PackedFloat32Array|PackedFloat64Array|PackedStringArray|PackedVector2Array|PackedVector2iArray|PackedVector3Array|PackedVector3iArray|PackedVector4Array|PackedColorArray|JSON|UPNP|OS|IP|JSONRPC|XRVRS)\\b",
|
||||||
"name": "entity.name.type.class.builtin.gdscript"
|
"name": "entity.name.type.class.builtin.gdscript"
|
||||||
},
|
},
|
||||||
"const_vars": {
|
"const_vars": {
|
||||||
@@ -530,7 +544,7 @@
|
|||||||
"end": "(,)|(?=\\))",
|
"end": "(,)|(?=\\))",
|
||||||
"beginCaptures": { "1": { "name": "keyword.operator.gdscript" } },
|
"beginCaptures": { "1": { "name": "keyword.operator.gdscript" } },
|
||||||
"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
|
"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
|
||||||
"patterns": [ { "include": "#base_expression" } ]
|
"patterns": [ { "include": "#expression" } ]
|
||||||
},
|
},
|
||||||
"annotated_parameter": {
|
"annotated_parameter": {
|
||||||
"begin": "(?x)\n \\s* ([a-zA-Z_]\\w*) \\s* (:)\\s* ([a-zA-Z_]\\w*)? \n",
|
"begin": "(?x)\n \\s* ([a-zA-Z_]\\w*) \\s* (:)\\s* ([a-zA-Z_]\\w*)? \n",
|
||||||
@@ -542,7 +556,7 @@
|
|||||||
"end": "(,)|(?=\\))",
|
"end": "(,)|(?=\\))",
|
||||||
"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
|
"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "include": "#base_expression" },
|
{ "include": "#expression" },
|
||||||
{
|
{
|
||||||
"name": "keyword.operator.assignment.gdscript",
|
"name": "keyword.operator.assignment.gdscript",
|
||||||
"match": "=(?!=)"
|
"match": "=(?!=)"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bl7k8rh4vgbma
|
||||||
@@ -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_classA2 = ClassA.new()
|
||||||
var local_classB = ClassB.new()
|
var local_classB2 = ClassB.new()
|
||||||
local_classA.member_classB = local_classB
|
local_classA2.member_classB = local_classB2
|
||||||
local_classB.member_classA = local_classA
|
local_classB2.member_classA = local_classA2
|
||||||
|
local_classA2.test_function(delta);
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://jj6y8lb0lkij
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ca1f5tmqgm6hu
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c4ypojhmiyhhf
|
||||||
1
test_projects/test-dap-project-godot4/Node1.gd.uid
Normal file
1
test_projects/test-dap-project-godot4/Node1.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bxlldk7s267hd
|
||||||
@@ -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
test_projects/test-dap-project-godot4/NodeVars.gd.uid
Normal file
1
test_projects/test-dap-project-godot4/NodeVars.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ciokiqoyaox13
|
||||||
@@ -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 str_var := "ScopeVars::_ready::local::str_var"
|
||||||
var local2 = GlobalScript.globalMember
|
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
test_projects/test-dap-project-godot4/ScopeVars.gd.uid
Normal file
1
test_projects/test-dap-project-godot4/ScopeVars.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cbgugy44s0uia
|
||||||
1
test_projects/test-dap-project-godot4/TestClassA.gd.uid
Normal file
1
test_projects/test-dap-project-godot4/TestClassA.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ct5jeingo4ge
|
||||||
Reference in New Issue
Block a user