Compare commits

..

19 Commits
2.4.0 ... 2.5.1

Author SHA1 Message Date
Hugo Locurcio
d9ea6245d4 Bump to version 2.5.1 2025-03-15 20:58:11 +01:00
David Kincaid
e38db288b7 Add ability to suppress LSP error messages (#823) 2025-03-15 15:01:35 -04:00
Hugo Locurcio
0da21f23a3 Bump to version 2.5.0 2025-03-11 00:18:18 +01:00
David Kincaid
a04f58c82d Scene Preview Improvements (relative path drag/drop) (#815)
* Improve scene preview lock behavior
* Add convert_uri_to_resource_path utility function
* Implement relative nodepath dropping
* Prevent a possible error when trying to refresh a scene that doesn't exist
* Fix wrong command name being called (scenePreview.openMainScene has actually never worked)
2025-03-10 04:50:00 -04:00
David Kincaid
03606fdb3a Various LSP Client Improvements (#816)
* Fix outgoing LSP messages not actually being discarded
* Resend init message if reconnecting (fixes #818)
* Added wrong project disconnect feature
* Update vscode-languageclient from ^7.0.0 to ^9.0.1
2025-03-10 04:49:05 -04:00
David Kincaid
f4ae73c9a0 Add automatic project formatting (#814)
* Add biome as a dev dependency and add "npm format" script

* Align new debugger code with project style
2025-03-04 19:54:47 -05:00
David Kincaid
1dcbd651df Add snippet/placeholder behavior to Scene Preview file drops (#813) 2025-03-04 19:11:27 -05:00
dependabot[bot]
54f68f15ea Bump esbuild from 0.17.19 to 0.25.0 (#790)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.17.19 to 0.25.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.17.19...v0.25.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-04 19:10:58 -05:00
David Kincaid
0203cec293 Formatting and Highlighting Fixes (#812)
Fixes #805 and #810
2025-03-04 18:56:21 -05:00
Mikhail Zolotukhin
34de1b64f0 Fix scene preview with nodes, that have spaces in names (#806)
Fixes #688
2025-03-04 15:39:58 -05:00
David Kincaid
e528384ea5 Add godotTools.openEditorSettings command (#802)
Add godotTools.openEditorSettingscommand
2025-02-27 12:27:27 -05:00
dependabot[bot]
c5c7aa2ced Bump actions/upload-artifact from 4.6.0 to 4.6.1 (#803)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.0...v4.6.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 13:01:52 -05:00
Arron
0a3c319879 Add region blocks as a language scope for themes (#800) 2025-02-22 13:46:32 -05:00
David Kincaid
3f7a944e96 More Highlighting and Formatting Improvements (#783)
* Fix **= operator being formatted incorrectly

* Fix variables in get_node()-style function calls not being highlighted

* Move super from builtin_classes to keywords

* Fix uppercase builtin classes being highlighted as constants

* Fix setter and getter highlighting/formatting

* Fix variable as default parameter not highlighted in function declaration
2025-02-22 12:51:44 -05:00
Sabs, like "Sobs
51ef0ef0c0 Add Autosave details and Server Port note to README (#771)
* Add Autosave details and Server Port note to README
2025-02-22 12:50:21 -05:00
Saint
b29fbb75a0 Add print_rich() support to Debug Console (#792)
* add bbcode support to debug console
* fix output line for Debug Console
* Update debug console output. Add Godot3.6 support.
2025-02-22 12:36:11 -05:00
Jesse Ward
b986ce0e51 Resolves godotengine/godot-vscode-plugin#796 (#797) 2025-02-22 12:31:57 -05:00
Eric Cosky
035211276d Fixes for attached debugging and related improvements. (#784)
'launch' and 'attach' modes are working with these changes. The root problems were related to the version of the attached project not being detected properly and file paths not being correctly calculated when attached. The networking code that had version-dependent behavior is now a bit more robust and won't break if minor versions were to ever exceed 1 digit.
When using 'attach' mode, the version info wasn't available at all, causing most (all?) network messages to be ignored.
2025-02-22 12:28:38 -05:00
MichaelXt
53f48ede63 DebugAdapter variables overhaul (#793)
- Redesigned the representation of godot objects to match internal structure of godot server
- Lazy evaluation for the godot objects
- Stack frames now can be switched with variables updated
2025-02-22 12:17:55 -05:00
57 changed files with 2970 additions and 1439 deletions

View File

@@ -29,7 +29,7 @@ body:
Use the **Help > About** menu to see your current version.
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.
placeholder: "1.97.2"
placeholder: "1.98.1"
validations:
required: true
@@ -40,7 +40,7 @@ body:
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.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: "2.4.0"
placeholder: "2.5.1"
validations:
required: true

View File

@@ -29,7 +29,7 @@ body:
Use the **Help > About** menu to see your current version.
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.
placeholder: "1.93.1"
placeholder: "1.98.1"
validations:
required: true
@@ -40,7 +40,7 @@ body:
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.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: "2.3.0"
placeholder: "2.5.1"
validations:
required: true

View File

@@ -16,7 +16,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4.2.0
with:
node-version: 16.x
node-version: 22.x
- name: Install Godot (Ubuntu)
if: matrix.os == 'ubuntu-latest'
@@ -75,7 +75,7 @@ jobs:
ls -l godot-tools.vsix
- name: Upload extension VSIX
uses: actions/upload-artifact@v4.6.0
uses: actions/upload-artifact@v4.6.1
with:
name: godot-tools
path: godot-tools.vsix

View File

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

23
.vscode/launch.json vendored
View File

@@ -5,7 +5,6 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
@@ -48,5 +47,27 @@
"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"
}
},
]
}

View File

@@ -1,5 +1,20 @@
# Changelog
### 2.5.1
- [Fix "Request textDocument/documentSymbol failed" error when opening a GDScript file](https://github.com/godotengine/godot-vscode-plugin/pull/823)
### 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
- [**Implement warnings and errors in debug console**](https://github.com/godotengine/godot-vscode-plugin/pull/749)

View File

@@ -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`
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
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
the editor after opening VS Code, you can click the **Retry** button
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?
@@ -205,3 +213,11 @@ see [CONTRIBUTING.md](CONTRIBUTING.md)
infer all variable types.
- To increase the number of results displayed, open the **Editor Settings**,
go to the **Language Server** section then check **Enable Smart Resolve**.
### 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**

View File

@@ -1,4 +1,7 @@
{
"vcs": {
"defaultBranch": "master"
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
@@ -16,7 +19,7 @@
"rules": {
"style": {
"noUselessElse": "off",
"useImportType": "off"
"useImportType": "off"
}
}
}

1326
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "godot-tools",
"displayName": "godot-tools",
"icon": "icon.png",
"version": "2.4.0",
"version": "2.5.1",
"description": "Tools for game development with Godot Engine and GDScript",
"repository": {
"type": "git",
@@ -31,6 +31,7 @@
],
"main": "./out/extension.js",
"scripts": {
"format": "biome format --write --changed src",
"compile": "tsc -p ./",
"lint": "eslint ./src --quiet",
"watch": "tsc -watch -p ./",
@@ -60,6 +61,11 @@
"command": "godotTools.openEditor",
"title": "Open workspace with Godot editor"
},
{
"category": "Godot Tools",
"command": "godotTools.openEditorSettings",
"title": "Open EditorSettings File"
},
{
"category": "Godot Tools",
"command": "godotTools.startLanguageServer",
@@ -258,7 +264,7 @@
"maximum": 200,
"description": "Scale factor (%) to apply to the Godot documentation viewer."
},
"godotTools.documentation.displayMinimap":{
"godotTools.documentation.displayMinimap": {
"type": "boolean",
"default": true,
"description": "Whether to display the minimap for the Godot documentation viewer."
@@ -870,25 +876,29 @@
}
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/chai": "^4.3.11",
"@types/chai-as-promised": "^8.0.1",
"@types/chai-subset": "^1.3.5",
"@types/marked": "^4.0.8",
"@types/mocha": "^10.0.6",
"@types/node": "^18.15.0",
"@types/node": "^18.19.75",
"@types/prismjs": "^1.16.8",
"@types/vscode": "^1.96.0",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/eslint-plugin-tslint": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vscode/test-cli": "^0.0.4",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.3.8",
"@vscode/vsce": "^2.29.0",
"chai": "^4.3.10",
"chai": "^4.5.0",
"chai-as-promised": "^8.0.1",
"chai-subset": "^1.6.0",
"esbuild": "^0.17.15",
"esbuild": "^0.25.0",
"eslint": "^8.37.0",
"mocha": "^10.2.0",
"mocha": "^10.8.2",
"sinon": "^19.0.2",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"tslint": "^5.20.1",
@@ -898,12 +908,13 @@
"@vscode/debugadapter": "^1.68.0",
"@vscode/debugprotocol": "^1.68.0",
"await-notify": "^1.0.1",
"bbcode-to-ansi": "^1.0.0",
"global": "^4.4.0",
"marked": "^4.0.11",
"net": "^1.0.2",
"prismjs": "^1.17.1",
"terminate": "^2.5.0",
"vscode-languageclient": "^7.0.0",
"vscode-languageclient": "^9.0.1",
"vscode-oniguruma": "^2.0.1",
"vscode-textmate": "^9.0.0",
"ws": "^8.17.1",

View File

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

View File

@@ -25,6 +25,9 @@ import { GodotDebugSession as Godot4DebugSession } from "./godot4/debug_session"
import { register_command, set_context, createLogger, get_project_version } from "../utils";
import { SceneTreeProvider, SceneNode } from "./scene_tree_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" });
@@ -106,7 +109,7 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
log.info(`Project version identified as ${projectVersion}`);
if (projectVersion.startsWith("4")) {
this.session = new Godot4DebugSession();
this.session = new Godot4DebugSession(projectVersion);
} else {
this.session = new Godot3DebugSession();
}
@@ -256,38 +259,34 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
}
}
public inspect_node(element: SceneNode | RemoteProperty) {
this.session?.controller.request_inspect_object(BigInt(element.object_id));
this.session?.inspect_callbacks.set(
BigInt(element.object_id),
(class_name, variable) => {
this.inspectorProvider.fill_tree(
element.label,
class_name,
element.object_id,
variable
);
public async inspect_node(element: SceneNode | RemoteProperty) {
await this.fill_provider_tree(element.label, BigInt(element.object_id));
}
private create_godot_variable(godot_object: GodotObject): GodotVariable {
return {
value: {
type_name: function() { return godot_object.type; },
stringify_value: function() { return `<${godot_object.godot_id}>`; },
sub_values: function() {return godot_object.sub_values; },
},
);
} as GodotVariable;
}
public refresh_scene_tree() {
this.session?.controller.request_scene_tree();
}
public refresh_inspector() {
if (this.inspectorProvider.has_tree()) {
const name = this.inspectorProvider.get_top_name();
const id = this.inspectorProvider.get_top_id();
this.session?.controller.request_inspect_object(BigInt(id));
private async fill_provider_tree(label: string, godot_id: bigint, force_refresh = false) {
if (this.session instanceof Godot4DebugSession) {
const godot_object = await this.session.variables_manager.get_godot_object(BigInt(godot_id), force_refresh);
const va = this.create_godot_variable(godot_object);
this.inspectorProvider.fill_tree(label, godot_object.type, Number(godot_object.godot_id), va);
} else {
this.session?.controller.request_inspect_object(BigInt(godot_id));
this.session?.inspect_callbacks.set(
BigInt(id),
BigInt(godot_id),
(class_name, variable) => {
this.inspectorProvider.fill_tree(
name,
label,
class_name,
id,
Number(godot_id),
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 type = typeof previous_value;
const is_float = type === "number" && !Number.isInteger(previous_value);
window
.showInputBox({ value: `${property.description}` })
.then((value) => {
let new_parsed_value: any;
switch (type) {
case "string":
new_parsed_value = value;
break;
case "number":
if (is_float) {
new_parsed_value = Number.parseFloat(value);
if (Number.isNaN(new_parsed_value)) {
return;
}
} else {
new_parsed_value = Number.parseInt(value);
if (Number.isNaN(new_parsed_value)) {
return;
}
}
break;
case "boolean":
if (
value.toLowerCase() === "true" ||
value.toLowerCase() === "false"
) {
new_parsed_value = value.toLowerCase() === "true";
} else if (value === "0" || value === "1") {
new_parsed_value = value === "1";
} else {
return;
}
}
if (property.changes_parent) {
const parents = [property.parent];
let idx = 0;
while (parents[idx].changes_parent) {
parents.push(parents[idx++].parent);
const value = await window.showInputBox({ value: `${property.description}` });
let new_parsed_value: any;
switch (type) {
case "string":
new_parsed_value = value;
break;
case "number":
if (is_float) {
new_parsed_value = Number.parseFloat(value);
if (Number.isNaN(new_parsed_value)) {
return;
}
const changed_value = this.inspectorProvider.get_changed_value(
parents,
property,
new_parsed_value
);
this.session?.controller.set_object_property(
BigInt(property.object_id),
parents[idx].label,
changed_value,
);
} else {
this.session?.controller.set_object_property(
BigInt(property.object_id),
property.label,
new_parsed_value,
);
new_parsed_value = Number.parseInt(value);
if (Number.isNaN(new_parsed_value)) {
return;
}
}
break;
case "boolean":
if (
value.toLowerCase() === "true" ||
value.toLowerCase() === "false"
) {
new_parsed_value = value.toLowerCase() === "true";
} else if (value === "0" || value === "1") {
new_parsed_value = value === "1";
} else {
return;
}
}
if (property.changes_parent) {
const parents = [property.parent];
let idx = 0;
while (parents[idx].changes_parent) {
parents.push(parents[idx++].parent);
}
const changed_value = this.inspectorProvider.get_changed_value(
parents,
property,
new_parsed_value
);
this.session?.controller.set_object_property(
BigInt(property.object_id),
parents[idx].label,
changed_value,
);
} else {
this.session?.controller.set_object_property(
BigInt(property.object_id),
property.label,
new_parsed_value,
);
}
const name = this.inspectorProvider.get_top_name();
const id = this.inspectorProvider.get_top_id();
const label = this.inspectorProvider.get_top_name();
const godot_id = BigInt(this.inspectorProvider.get_top_id());
this.session?.controller.request_inspect_object(BigInt(id));
this.session?.inspect_callbacks.set(
BigInt(id),
(class_name, variable) => {
this.inspectorProvider.fill_tree(
name,
class_name,
id,
variable
);
},
);
});
await this.fill_provider_tree(label, godot_id, /*force_refresh*/ true);
// const res = await debug.activeDebugSession?.customRequest("refreshVariables"); // refresh vscode.debug variables
this.session.sendEvent(new InvalidatedEvent(["variables"]));
console.log("foo");
}
}

View File

@@ -23,9 +23,12 @@ import { build_sub_values, parse_next_scene_node, split_buffers } from "./helper
import { VariantDecoder } from "./variables/variant_decoder";
import { VariantEncoder } from "./variables/variant_encoder";
import { RawObject } from "./variables/variants";
import BBCodeToAnsi from 'bbcode-to-ansi';
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
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 {
public command: string = "";
@@ -427,9 +430,8 @@ export class ServerController {
this.didFirstOutput = true;
// this.request_scene_tree();
}
const lines = command.parameters;
for (const line of lines) {
debug.activeDebugConsole.appendLine(ansi.bright.blue + line[0]);
for (const output of command.parameters){
output[0].split("\n").forEach(line => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
}
break;
}

View File

@@ -9,42 +9,33 @@ import {
import { DebugProtocol } from "@vscode/debugprotocol";
import { Subject } from "await-notify";
import * as fs from "node:fs";
import { debug } from "vscode";
import { createLogger } from "../../utils";
import { GodotDebugData, GodotStackVars, GodotVariable } from "../debug_runtime";
import { GodotDebugData } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
import { SceneTreeProvider } from "../scene_tree_provider";
import { is_variable_built_in_type, parse_variable } from "./helpers";
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" });
interface Variable {
variable: GodotVariable;
index: number;
object_id: number;
}
export class GodotDebugSession extends LoggingDebugSession {
private all_scopes: GodotVariable[];
public controller = new ServerController(this);
public debug_data = new GodotDebugData(this);
public sceneTree: SceneTreeProvider;
private exception = false;
private got_scope: Subject = new Subject();
private ongoing_inspections: bigint[] = [];
private previous_inspections: bigint[] = [];
private configuration_done: Subject = new Subject();
private mode: "launch" | "attach" | "" = "";
public inspect_callbacks: Map<bigint, (class_name: string, variable: GodotVariable) => void> = new Map();
public constructor() {
public variables_manager: VariablesManager;
public constructor(projectVersion: string) {
super();
this.setDebuggerLinesStartAt1(false);
this.setDebuggerColumnsStartAt1(false);
this.controller.setProjectVersion(projectVersion);
}
public dispose() {
@@ -102,6 +93,7 @@ export class GodotDebugSession extends LoggingDebugSession {
this.mode = "attach";
this.debug_data.projectPath = args.project;
this.exception = false;
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) {
log.info("nextRequest", args);
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(
response: DebugProtocol.SetBreakpointsResponse,
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) {
log.info("stepInRequest", args);
if (!this.exception) {
@@ -272,6 +202,53 @@ export class GodotDebugSession extends LoggingDebugSession {
protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
log.info("threadsRequest");
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);
}
@@ -280,291 +257,48 @@ export class GodotDebugSession extends LoggingDebugSession {
args: DebugProtocol.VariablesArguments,
) {
log.info("variablesRequest", args);
if (!this.all_scopes) {
try {
const variables = await this.variables_manager.get_vscode_object(args.variablesReference);
response.body = {
variables: [],
variables: variables,
};
this.sendResponse(response);
return;
} catch (error) {
log.error("variablesRequest", error);
response.success = false;
response.message = error.toString();
}
const reference = this.all_scopes[args.variablesReference];
let variables: DebugProtocol.Variable[];
log.info("variablesRequest response", response);
this.sendResponse(response);
}
if (!reference.sub_values) {
variables = [];
} else {
variables = reference.sub_values.map((va) => {
const sva = this.all_scopes.find(
(sva) => sva && sva.scope_path === va.scope_path && sva.name === va.name,
);
if (sva) {
return parse_variable(
sva,
this.all_scopes.findIndex(
(va_idx) =>
va_idx &&
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
va_idx.name === va.name,
),
);
}
});
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
log.info("evaluateRequest", args);
try {
const parsed_variable = await this.variables_manager.get_vscode_variable_by_name(
args.expression,
args.frameId,
);
response.body = {
result: parsed_variable.value,
variablesReference: parsed_variable.variablesReference,
};
} catch (error) {
response.success = false;
response.message = error.toString();
response.body = {
result: "null",
variablesReference: 0,
};
}
response.body = {
variables: variables,
};
log.info("evaluateRequest response", response);
this.sendResponse(response);
}
public set_exception(exception: boolean) {
this.exception = true;
}
public set_scopes(stackVars: GodotStackVars) {
this.all_scopes = [
undefined,
{
name: "local",
value: undefined,
sub_values: stackVars.locals,
scope_path: "@",
},
{
name: "member",
value: undefined,
sub_values: stackVars.members,
scope_path: "@",
},
{
name: "global",
value: undefined,
sub_values: stackVars.globals,
scope_path: "@",
},
];
for (const va of stackVars.locals) {
va.scope_path = "@.local";
this.append_variable(va);
}
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);
});
}
}
}

View File

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

View File

@@ -1,5 +1,6 @@
import { GodotVariable, } from "../debug_runtime";
import { GodotVariable } from "../debug_runtime";
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 {
const childCount: number = params[ofs.offset++];
@@ -31,12 +32,7 @@ export function split_buffers(buffer: Buffer) {
return buffers;
}
export function is_variable_built_in_type(va: GodotVariable) {
var type = typeof va.value;
return ["number", "bigint", "boolean", "string"].some(x => x == type);
}
export function get_sub_values(value: any) {
export function get_sub_values(value: any): GodotVariable[] {
let subValues: GodotVariable[] = undefined;
if (value) {
@@ -45,19 +41,15 @@ export function get_sub_values(value: any) {
return { name: `${i}`, value: va } as GodotVariable;
});
} else if (value instanceof Map) {
subValues = Array.from(value.keys()).map((va) => {
if (typeof va["stringify_value"] === "function") {
return {
name: `${va.type_name()}${va.stringify_value()}`,
value: value.get(va),
} as GodotVariable;
} else {
return {
name: `${va}`,
value: value.get(va),
} as GodotVariable;
}
});
subValues = [];
for (const [key, val] of value.entries()) {
const name =
typeof key["stringify_value"] === "function"
? `${key.type_name()}${key.stringify_value()}`
: `${key}`;
const godot_id = val instanceof ObjectId ? val.id : undefined;
subValues.push({ id: godot_id, name, value: val } as GodotVariable);
}
} else if (typeof value["sub_values"] === "function") {
subValues = value.sub_values()?.map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
@@ -71,54 +63,3 @@ export function get_sub_values(value: any) {
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,
};
}

View File

@@ -16,16 +16,20 @@ import {
} from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
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 { GodotDebugSession } from "./debug_session";
import { get_sub_values, parse_next_scene_node, split_buffers } from "./helpers";
import { VariantDecoder } from "./variables/variant_decoder";
import { VariantEncoder } from "./variables/variant_encoder";
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 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 {
public command: string = "";
@@ -35,6 +39,33 @@ class Command {
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 {
private commandBuffer: Buffer[] = [];
private encoder = new VariantEncoder();
@@ -46,11 +77,20 @@ export class ServerController {
private socket?: net.Socket;
private steppingOut = false;
private didFirstOutput = false;
private partialStackVars = new GodotStackVars();
private connectedVersion = "";
private partialStackVars: GodotPartialStackVars;
private projectVersionMajor: number;
private projectVersionMinor: number;
private projectVersionPoint: number;
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() {
this.send_command("break");
}
@@ -93,8 +133,17 @@ export class ServerController {
this.send_command("get_stack_dump");
}
public request_stack_frame_vars(frame_id: number) {
this.send_command("get_stack_frame_vars", [frame_id]);
public request_stack_frame_vars(stack_frame_id: number) {
if (this.partialStackVars !== undefined) {
log.warn(
"Partial stack frames have been requested, while existing request hasn't been completed yet." +
`Remaining stack_frames: ${this.partialStackVars.remaining}` +
`Current stack_frame_id: ${this.partialStackVars.stack_frame_id}` +
`Requested stack_frame_id: ${stack_frame_id}`,
);
}
this.partialStackVars = new GodotPartialStackVars(stack_frame_id);
this.send_command("get_stack_frame_vars", [stack_frame_id]);
}
public set_object_property(objectId: bigint, label: string, newParsedValue) {
@@ -168,7 +217,7 @@ export class ServerController {
}
}
this.connectedVersion = result.version;
this.setProjectVersion(result.version);
let command = `"${godotPath}" --path "${args.project}"`;
const address = args.address.replace("tcp://", "");
@@ -259,7 +308,7 @@ export class ServerController {
return;
}
socketLog.debug("rx:", data[0]);
socketLog.debug("rx:", data[0], data[0][2]);
const command = this.parse_message(data[0]);
this.handle_command(command);
}
@@ -345,7 +394,7 @@ export class ServerController {
const command = new Command();
let i = 0;
command.command = dataset[i++];
if (this.connectedVersion[2] >= "2") {
if (this.projectVersionMinor >= 2) {
command.threadId = dataset[i++];
}
command.parameters = dataset[i++];
@@ -362,9 +411,11 @@ export class ServerController {
this.set_exception("");
}
this.request_stack_dump();
this.session.variables_manager = new VariablesManager(this);
break;
}
case "debug_exit":
this.session.variables_manager = undefined;
break;
case "message:click_ctrl":
// TODO: what is this?
@@ -381,14 +432,14 @@ export class ServerController {
break;
}
case "scene:inspect_object": {
let id = BigInt(command.parameters[0]);
let godot_id = BigInt(command.parameters[0]);
const className: string = command.parameters[1];
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,
// thus we need to convert it to its equivalent unsigned value here.
if (id < 0) {
id = id + BigInt(2) ** BigInt(64);
if (godot_id < 0) {
godot_id = godot_id + BigInt(2) ** BigInt(64);
}
const rawObject = new RawObject(className);
@@ -396,14 +447,19 @@ export class ServerController {
rawObject.set(prop[0], prop[5]);
}
const sub_values = get_sub_values(rawObject);
const inspect_callback = this.session.inspect_callbacks.get(BigInt(id));
if (inspect_callback !== undefined) {
const inspectedVariable = { name: "", value: rawObject, sub_values: sub_values } as GodotVariable;
inspect_callback(inspectedVariable.name, inspectedVariable);
this.session.inspect_callbacks.delete(BigInt(id));
// race condition here:
// 0. DebuggerStop1 happens
// 1. the DA may have sent the "inspect_object" message
// 2. the vscode hit "continue"
// 3. new breakpoint hit, DebuggerStop2 happens
// 4. the godot server will return response for `1.` with "scene:inspect_object"
// at this moment there is no way to tell if "scene:inspect_object" is for DebuggerStop1 or DebuggerStop2
try {
this.session.variables_manager?.resolve_variable(godot_id, className, sub_values);
} catch (error) {
log.error("Race condition error error in scene:inspect_object", error);
}
this.session.set_inspection(id, rawObject, sub_values);
break;
}
case "stack_dump": {
@@ -423,17 +479,75 @@ export class ServerController {
break;
}
case "stack_frame_vars": {
this.partialStackVars.reset(command.parameters[0]);
this.session.set_scopes(this.partialStackVars);
/** first response to {@link request_stack_frame_vars} */
if (this.partialStackVars !== undefined) {
log.warn(
"'stack_frame_vars' received again from godot engine before all partial 'stack_frame_var' are received",
);
}
const remaining = command.parameters[0];
// init this.partialStackVars, which will be filled with "stack_frame_var" responses data
this.partialStackVars.reset(remaining);
break;
}
case "stack_frame_var": {
this.do_stack_frame_var(
command.parameters[0],
command.parameters[1],
command.parameters[2],
command.parameters[3],
);
if (this.partialStackVars === undefined) {
log.error("Unexpected 'stack_frame_var' received. Should have received 'stack_frame_vars' first.");
return;
}
if (typeof command.parameters[0] !== "string") {
log.error(
"Unexpected parameter type for 'stack_frame_var'. Expected string for name, got " +
typeof command.parameters[0],
);
return;
}
if (
typeof command.parameters[1] !== "number" ||
(command.parameters[1] !== 0 && command.parameters[1] !== 1 && command.parameters[1] !== 2)
) {
log.error(
"Unexpected parameter type for 'stack_frame_var'. Expected number for scope, got " +
typeof command.parameters[1],
);
return;
}
if (typeof command.parameters[2] !== "number") {
log.error(
"Unexpected parameter type for 'stack_frame_var'. Expected number for type, got " +
typeof command.parameters[2],
);
return;
}
var name: string = command.parameters[0];
var scope: 0 | 1 | 2 = command.parameters[1]; // 0 = locals, 1 = members, 2 = globals
var type: number = command.parameters[2];
var value: any = command.parameters[3];
var subValues: GodotVariable[] = get_sub_values(value);
this.partialStackVars.append(name, scope, type, value, subValues);
if (this.partialStackVars.remaining === 0) {
const stackVars = this.partialStackVars;
this.partialStackVars = undefined;
log.info("All partial 'stack_frame_var' are received.");
// godot server doesn't send the frame_id for the stack_vars, assume the remembered stack_frame_id:
const frame_id = BigInt(stackVars.stack_frame_id);
const local_scopes_godot_id = -frame_id * 3n - 1n;
const member_scopes_godot_id = -frame_id * 3n - 2n;
const global_scopes_godot_id = -frame_id * 3n - 3n;
this.session.variables_manager.resolve_variable(local_scopes_godot_id, "Locals", stackVars.Locals);
this.session.variables_manager.resolve_variable(
member_scopes_godot_id,
"Members",
stackVars.Members,
);
this.session.variables_manager.resolve_variable(
global_scopes_godot_id,
"Globals",
stackVars.Globals,
);
}
break;
}
case "output": {
@@ -441,9 +555,8 @@ export class ServerController {
this.didFirstOutput = true;
// this.request_scene_tree();
}
const lines = command.parameters[0];
for (const line of lines) {
debug.activeDebugConsole.appendLine(ansi.bright.blue + line);
for (const output of command.parameters[0]) {
output.split("\n").forEach((line) => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
}
break;
}
@@ -612,11 +725,11 @@ export class ServerController {
private send_command(command: string, parameters?: any[]) {
const commandArray: any[] = [command];
if (this.connectedVersion[2] >= "2") {
if (this.projectVersionMinor >= 2) {
commandArray.push(this.threadId);
}
commandArray.push(parameters ?? []);
socketLog.debug("tx:", commandArray);
socketLog.debug("tx:", commandArray, commandArray[2]);
const buffer = this.encoder.encode_variant(commandArray);
this.commandBuffer.push(buffer);
this.send_buffer();
@@ -632,26 +745,4 @@ export class ServerController {
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);
}
}
}

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as vscode from "vscode";
import { attemptSettingsUpdate, get_extension_uri, clean_godot_path } from "./utils";
import {
GDInlayHintsProvider,
GDHoverProvider,
GDDocumentDropEditProvider,
GDDocumentLinkProvider,
GDSemanticTokensProvider,
GDCompletionItemProvider,
@@ -21,9 +23,11 @@ import {
find_project_file,
register_command,
set_context,
get_editor_data_dir,
get_project_dir,
get_project_version,
verify_godot_version,
convert_uri_to_resource_path,
} from "./utils";
import { prompt_for_godot_executable } from "./utils/prompts";
import { killSubProcesses, subProcess } from "./utils/subspawn";
@@ -34,6 +38,7 @@ interface Extension {
debug?: GodotDebugger;
scenePreviewProvider?: ScenePreviewProvider;
linkProvider?: GDDocumentLinkProvider;
dropsProvider?: GDDocumentDropEditProvider;
hoverProvider?: GDHoverProvider;
inlayProvider?: GDInlayHintsProvider;
formattingProvider?: FormattingProvider;
@@ -54,6 +59,7 @@ export function activate(context: vscode.ExtensionContext) {
globals.debug = new GodotDebugger(context);
globals.scenePreviewProvider = new ScenePreviewProvider(context);
globals.linkProvider = new GDDocumentLinkProvider(context);
globals.dropsProvider = new GDDocumentDropEditProvider(context);
globals.hoverProvider = new GDHoverProvider(context);
globals.inlayProvider = new GDInlayHintsProvider(context);
globals.formattingProvider = new FormattingProvider(context);
@@ -65,13 +71,14 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
register_command("openEditor", open_workspace_with_editor),
register_command("openEditorSettings", open_godot_editor_settings),
register_command("copyResourcePath", copy_resource_path),
register_command("listGodotClasses", list_classes),
register_command("switchSceneScript", switch_scene_script),
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"]);
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) {
uri = vscode.window.activeTextEditor.document.uri;
}
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);
relative_path = "res://" + relative_path;
const relative_path = await convert_uri_to_resource_path(uri);
vscode.env.clipboard.writeText(relative_path);
}
@@ -166,7 +166,7 @@ async function open_workspace_with_editor() {
if (get_configuration("editor.verbose")) {
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) {
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.
* 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).
* @returns
*/
async function get_godot_path(): Promise<string|undefined> {
async function get_godot_path(): Promise<string | undefined> {
const projectVersion = await get_project_version();
if (projectVersion === undefined) {
return undefined;
@@ -217,7 +250,7 @@ class GodotEditorTerminal implements vscode.Pseudoterminal {
private closeEmitter = new vscode.EventEmitter<number>();
onDidClose?: vscode.Event<number> = this.closeEmitter.event;
constructor(private command: string) { }
constructor(private command: string) {}
open(initialDimensions: vscode.TerminalDimensions | undefined): void {
const proc = subProcess("GodotEditor", this.command, { shell: true, detached: true });

View File

@@ -8,6 +8,7 @@ func f():
x %= 1
x = 2 ** 2
x = 2 * -1
x **= 2
# bitwise
x |= 1
@@ -21,4 +22,12 @@ func f():
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

View 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

View File

@@ -186,15 +186,15 @@ function between(tokens: Token[], current: number, options: FormatterOptions) {
if (prev === "@") return "";
if (prev === "-" || prev === "+") {
if (nextToken.identifier) return " ";
if (next === "(") return " ";
if (current === 1) return "";
if (["keyword", "symbol"].includes(tokens[current - 2]?.type)) {
return "";
}
if ([",", "(", "["].includes(tokens[current - 2]?.value)) {
return "";
}
if (nextToken.identifier) return " ";
if (current === 1) return "";
}
if (prev === ":" && next === "=") return "";

View File

@@ -25,6 +25,7 @@ enum ManagerStatus {
DISCONNECTED = 4,
CONNECTED = 5,
RETRYING = 6,
WRONG_WORKSPACE = 7,
}
export class ClientConnectionManager {
@@ -211,6 +212,9 @@ export class ClientConnectionManager {
case ManagerStatus.RETRYING:
this.show_retrying_prompt();
break;
case ManagerStatus.WRONG_WORKSPACE:
this.retry_connect_client();
break;
}
}
@@ -253,6 +257,10 @@ export class ClientConnectionManager {
tooltip += `\n${this.connectedVersion}`;
}
break;
case ManagerStatus.WRONG_WORKSPACE:
text = "$(x) Wrong Project";
tooltip = "Disconnected from the GDScript language server.";
break;
}
this.statusWidget.text = text;
this.statusWidget.tooltip = tooltip;
@@ -269,7 +277,7 @@ export class ClientConnectionManager {
set_context("connectedToLSP", true);
this.status = ManagerStatus.CONNECTED;
if (this.client.needsStart()) {
this.context.subscriptions.push(this.client.start());
this.client.start().then(() => log.info("LSP Client started"));
}
break;
case ClientStatus.DISCONNECTED:
@@ -285,6 +293,10 @@ export class ClientConnectionManager {
}
this.retry = true;
break;
case ClientStatus.REJECTED:
this.status = ManagerStatus.WRONG_WORKSPACE;
this.retry = false;
break;
default:
break;
}

View File

@@ -1,7 +1,9 @@
import EventEmitter from "node:events";
import * as path from "node:path";
import * as vscode from "vscode";
import {
LanguageClient,
MessageSignature,
type LanguageClientOptions,
type NotificationMessage,
type RequestMessage,
@@ -10,7 +12,7 @@ import {
} from "vscode-languageclient/node";
import { globals } from "../extension";
import { createLogger, get_configuration } from "../utils";
import { createLogger, get_configuration, get_project_dir } from "../utils";
import { MessageIO } from "./MessageIO";
const log = createLogger("lsp.client", { output: "Godot LSP" });
@@ -19,6 +21,7 @@ export enum ClientStatus {
PENDING = 0,
DISCONNECTED = 1,
CONNECTED = 2,
REJECTED = 3,
}
export enum TargetLSP {
@@ -29,7 +32,7 @@ export enum TargetLSP {
export type Target = {
host: string;
port: number;
type: TargetLSP;
type: TargetLSP;
};
type HoverResult = {
@@ -55,6 +58,13 @@ type HoverResponseMesssage = {
result: HoverResult;
};
type ChangeWorkspaceNotification = {
method: string;
params: {
path: string;
};
};
export default class GDScriptLanguageClient extends LanguageClient {
public io: MessageIO = new MessageIO();
@@ -63,6 +73,8 @@ export default class GDScriptLanguageClient extends LanguageClient {
public port = -1;
public lastPortTried = -1;
public sentMessages = new Map();
private initMessage: RequestMessage;
private rejected = false;
events = new EventEmitter();
@@ -85,9 +97,6 @@ export default class GDScriptLanguageClient extends LanguageClient {
{ scheme: "file", language: "gdscript" },
{ scheme: "untitled", language: "gdscript" },
],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
},
};
super("GDScriptLanguageClient", serverOptions, clientOptions);
@@ -100,6 +109,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
connect(target: TargetLSP = TargetLSP.EDITOR) {
this.rejected = false;
this.target = target;
this.status = ClientStatus.PENDING;
@@ -122,15 +132,54 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.io.connect(host, port);
}
async send_request(method: string, params) {
try {
return this.sendRequest(method, params);
} catch {
log.warn("sending request failed!");
}
}
handleFailedRequest<T>(
type: MessageSignature,
token: vscode.CancellationToken | undefined,
error: any,
defaultValue: T,
showNotification?: boolean,
): T {
if (type.method === "textDocument/documentSymbol") {
if (error.message.includes("selectionRange must be contained in fullRange")) {
log.warn(`Request failed for method "${type.method}", suppressing notification - see issue #820`);
return super.handleFailedRequest(type, token, error, defaultValue, false);
}
}
return super.handleFailedRequest(type, token, error, defaultValue, showNotification);
}
private request_filter(message: RequestMessage) {
if (this.rejected) {
if (message.method === "shutdown") {
return message;
}
return false;
}
this.sentMessages.set(message.id, message);
if (!this.initMessage && message.method === "initialize") {
this.initMessage = message;
}
// discard outgoing messages that we know aren't supported
if (message.method === "didChangeWatchedFiles") {
return;
// if (message.method === "textDocument/didSave") {
// return false;
// }
// if (message.method === "textDocument/willSaveWaitUntil") {
// return false;
// }
if (message.method === "workspace/didChangeWatchedFiles") {
return false;
}
if (message.method === "workspace/symbol") {
return;
return false;
}
return message;
@@ -165,9 +214,19 @@ export default class GDScriptLanguageClient extends LanguageClient {
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) {
if (message.method === "gdscript_client/changeWorkspace") {
//
this.check_workspace(message as ChangeWorkspaceNotification);
}
if (message.method === "gdscript/capabilities") {
globals.docsProvider.register_capabilities(message);
@@ -194,9 +253,8 @@ export default class GDScriptLanguageClient extends LanguageClient {
textDocument: { uri: uri.toString() },
position: { line: position.line, character: position.character },
};
const response: HoverResult = await this.sendRequest("textDocument/hover", params);
return this.parse_hover_result(response);
const response = await this.send_request("textDocument/hover", params);
return this.parse_hover_result(response as HoverResult);
}
private parse_hover_result(message: HoverResult) {
@@ -233,9 +291,17 @@ export default class GDScriptLanguageClient extends LanguageClient {
const host = get_configuration("lsp.serverHost");
log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
if (this.initMessage) {
this.send_request(this.initMessage.method, this.initMessage.params);
}
}
private on_disconnected() {
if (this.rejected) {
this.status = ClientStatus.REJECTED;
return;
}
if (this.target === TargetLSP.EDITOR) {
const host = get_configuration("lsp.serverHost");
let port = get_configuration("lsp.serverPort");

View File

@@ -22,9 +22,9 @@ export class MessageIO extends EventEmitter {
reader = new MessageIOReader(this);
writer = new MessageIOWriter(this);
requestFilter: (msg: RequestMessage) => RequestMessage = (msg) => msg;
responseFilter: (msg: ResponseMessage) => ResponseMessage = (msg) => msg;
notificationFilter: (msg: NotificationMessage) => NotificationMessage = (msg) => msg;
requestFilter: (msg: RequestMessage) => RequestMessage | false = (msg) => msg;
responseFilter: (msg: ResponseMessage) => ResponseMessage | false = (msg) => msg;
notificationFilter: (msg: NotificationMessage) => NotificationMessage | false = (msg) => msg;
socket: Socket = null;
messageCache: string[] = [];
@@ -49,7 +49,7 @@ export class MessageIO extends EventEmitter {
resolve();
});
socket.on("data", (chunk: Buffer) => {
this.emit("data", chunk.toString());
this.emit("data", chunk);
});
// socket.on("end", this.on_disconnected.bind(this));
socket.on("error", () => {
@@ -100,7 +100,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
}
const json = JSON.parse(msg);
// allow message to be modified
let modified: ResponseMessage | NotificationMessage;
let modified: ResponseMessage | NotificationMessage | false;
if ("id" in json) {
modified = this.io.responseFilter(json);
} else if ("method" in json) {
@@ -109,7 +109,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
log.warn("rx [unhandled]:", json);
}
if (!modified) {
if (modified === false) {
log.debug("rx [discarded]:", json);
return;
}
@@ -128,7 +128,7 @@ export class MessageIOWriter extends AbstractMessageWriter implements MessageWri
async write(msg: RequestMessage) {
const modified = this.io.requestFilter(msg);
if (!modified) {
if (modified === false) {
log.debug("tx [discarded]:", msg);
return;
}

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

View File

@@ -40,7 +40,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
const uri = Uri.from({
scheme: "file",
path: path,
fragment: `${scene.externalResources[id].line},0`,
fragment: `${scene.externalResources.get(id).line},0`,
});
const r = this.create_range(document, match);
@@ -54,7 +54,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
const uri = Uri.from({
scheme: "file",
path: path,
fragment: `${scene.subResources[id].line},0`,
fragment: `${scene.subResources.get(id).line},0`,
});
const r = this.create_range(document, match);

View File

@@ -103,7 +103,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
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.class_info = this.classInfo.get(symbol.name);

View File

@@ -49,8 +49,8 @@ export class GDHoverProvider implements HoverProvider {
if (word.startsWith("ExtResource")) {
const match = word.match(wordPattern);
const id = match[1];
const resource = scene.externalResources[id];
const definition = scene.externalResources[id].body;
const resource = scene.externalResources.get(id);
const definition = resource.body;
const links = await this.get_links(definition);
const contents = new MarkdownString();
@@ -77,7 +77,7 @@ export class GDHoverProvider implements HoverProvider {
const match = word.match(wordPattern);
const id = match[1];
let definition = scene.subResources[id].body;
let definition = scene.subResources.get(id).body;
// don't display contents of giant arrays
definition = definition?.replace(/Array\([0-9,\.\- ]*\)/, "Array(...)");

View File

@@ -1,8 +1,9 @@
export * from "./completions";
export * from "./definition";
export * from "./document_drops";
export * from "./document_link";
export * from "./documentation";
export * from "./hover";
export * from "./inlay_hints";
export * from "./semantic_tokens";
export * from "./documentation";
export * from "./tasks";

View File

@@ -27,7 +27,7 @@ function fromDetail(detail: string): string {
}
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() },
position: {
line: hoverPosition.line,
@@ -65,10 +65,12 @@ export class GDInlayHintsProvider implements InlayHintsProvider {
if (!get_configuration("inlayHints.gdscript", true)) {
return hints;
}
if (!globals.lsp.client.isRunning()) {
return hints;
}
await globals.lsp.client.onReady();
const symbolsRequest = await globals.lsp.client.sendRequest("textDocument/documentSymbol", {
const symbolsRequest = await globals.lsp.client.send_request("textDocument/documentSymbol", {
textDocument: { uri: document.uri.toString() },
}) as unknown[];
@@ -126,7 +128,7 @@ export class GDInlayHintsProvider implements InlayHintsProvider {
for (const match of text.matchAll(/ExtResource\(\s?"?(\w+)\s?"?\)/g)) {
const id = match[1];
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}"`;
@@ -138,7 +140,7 @@ export class GDInlayHintsProvider implements InlayHintsProvider {
for (const match of text.matchAll(/SubResource\(\s?"?(\w+)\s?"?\)/g)) {
const id = match[1];
const end = document.positionAt(match.index + match[0].length);
const resource = scene.subResources[id];
const resource = scene.subResources.get(id);
const label = `${resource.type}`;

View File

@@ -1,6 +1,6 @@
import * as fs from "node:fs";
import { basename, extname } from "node:path";
import { TextDocument, Uri } from "vscode";
import { basename, extname } from "path";
import * as fs from "fs";
import { SceneNode, Scene } from "./types";
import { createLogger } from "../utils";
@@ -46,7 +46,7 @@ export class SceneParser {
const uid = line.match(/uid="([\w:/]+)"/)?.[1];
const id = line.match(/ id="?([\w]+)"?/)?.[1];
scene.externalResources[id] = {
scene.externalResources.set(id, {
body: line,
path: path,
type: type,
@@ -54,7 +54,7 @@ export class SceneParser {
id: id,
index: match.index,
line: document.lineAt(document.positionAt(match.index)).lineNumber + 1,
};
});
}
let lastResource = null;
@@ -76,7 +76,7 @@ export class SceneParser {
lastResource.body = text.slice(lastResource.index, match.index).trimEnd();
}
scene.subResources[id] = resource;
scene.subResources.set(id, resource);
lastResource = resource;
}
@@ -87,9 +87,9 @@ export class SceneParser {
const nodeRegex = /\[node.*/g;
for (const match of text.matchAll(nodeRegex)) {
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";
let parent = line.match(/parent="([\w\/.]+)"/)?.[1];
let parent = line.match(/parent="(([^.:@/"%]|[\/.])+)"/)?.[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
@@ -134,9 +134,10 @@ export class SceneParser {
scene.nodes.set(_path, node);
if (instance) {
if (instance in scene.externalResources) {
node.tooltip = scene.externalResources[instance].path;
node.resourcePath = scene.externalResources[instance].path;
const res = scene.externalResources.get(instance);
if (res) {
node.tooltip = res.path;
node.resourcePath = res.path;
if ([".tscn"].includes(extname(node.resourcePath))) {
node.contextValue += "openable";
}

View File

@@ -1,41 +1,36 @@
import * as fs from "node:fs";
import * as vscode from "vscode";
import {
type CancellationToken,
type Event,
EventEmitter,
type ExtensionContext,
type FileDecoration,
type ProviderResult,
type TreeDataProvider,
type TreeDragAndDropController,
type ExtensionContext,
EventEmitter,
type Event,
type TreeView,
type ProviderResult,
type TreeItem,
TreeItemCollapsibleState,
window,
languages,
type TreeView,
type Uri,
type CancellationToken,
type FileDecoration,
type DocumentDropEditProvider,
window,
workspace,
} from "vscode";
import * as fs from "node:fs";
import {
get_configuration,
find_file,
set_context,
convert_resource_path_to_uri,
register_command,
createLogger,
find_file,
get_configuration,
make_docs_uri,
node_name_to_snake,
register_command,
set_context,
} from "../utils";
import { SceneParser } from "./parser";
import type { SceneNode, Scene } from "./types";
import type { Scene, SceneNode } from "./types";
const log = createLogger("scenes.preview");
export class ScenePreviewProvider
implements TreeDataProvider<SceneNode>, TreeDragAndDropController<SceneNode>, DocumentDropEditProvider
{
export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDragAndDropController<SceneNode> {
public dropMimeTypes = [];
public dragMimeTypes = [];
private tree: TreeView<SceneNode>;
@@ -58,10 +53,6 @@ export class ScenePreviewProvider
dragAndDropController: this,
});
const selector = [
{ language: "csharp", scheme: "file" },
{ language: "gdscript", scheme: "file" },
];
context.subscriptions.push(
register_command("scenePreview.lock", this.lock_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.openScript", this.open_script.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.openDocumentation", this.open_documentation.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.scriptDecorator),
languages.registerDocumentDropEditProvider(selector, this),
this.watcher.onDidChange(this.on_file_changed.bind(this)),
this.watcher,
this.tree.onDidChangeSelection(this.tree_selection_changed),
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();
}
@@ -92,59 +90,15 @@ export class ScenePreviewProvider
data: vscode.DataTransfer,
token: vscode.CancellationToken,
): 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/unique", new vscode.DataTransferItem(source[0].unique));
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) {
if (!uri.fsPath.endsWith(".tscn")) {
return;
@@ -159,11 +113,10 @@ export class ScenePreviewProvider
}, 20);
}
public async refresh() {
public async text_editor_changed() {
if (this.scenePreviewLocked) {
return;
}
const editor = vscode.window.activeTextEditor;
if (editor) {
let fileName = editor.document.uri.fsPath;
@@ -196,24 +149,34 @@ export class ScenePreviewProvider
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.changeTreeEvent.fire();
this.refresh();
}
}
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() {
this.scenePreviewLocked = true;
set_context("scenePreview.locked", true);
this.context.workspaceState.update("godotTools.scenePreview.lockedScene", this.currentScene);
}
private unlock_preview() {
this.scenePreviewLocked = false;
set_context("scenePreview.locked", false);
this.context.workspaceState.update("godotTools.scenePreview.lockedScene", "");
this.refresh();
}
@@ -237,7 +200,7 @@ export class ScenePreviewProvider
}
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);
if (uri) {
@@ -256,7 +219,7 @@ export class ScenePreviewProvider
if (this.currentScene) {
const root = this.scene.root;
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);
if (uri) {
vscode.window.showTextDocument(uri, { preview: true });

View File

@@ -53,7 +53,7 @@ export class SceneNode extends TreeItem {
this.scriptId = line.match(/script = ExtResource\(\s*"?([\w]+)"?\s*\)/)[1];
this.contextValue += "hasScript";
}
if (line != "") {
if (line !== "") {
newLines.push(line);
}
}
@@ -79,7 +79,7 @@ export class Scene {
public title: string;
public mtime: number;
public root: SceneNode | undefined;
public externalResources: {[key: string]: GDResource} = {};
public subResources: {[key: string]: GDResource} = {};
public externalResources: Map<string, GDResource> = new Map();
public subResources: Map<string, GDResource> = new Map();
public nodes: Map<string, SceneNode> = new Map();
}

View File

@@ -4,6 +4,17 @@ import * as fs from "node:fs";
import * as os from "node:os";
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 projectFile: string | undefined = undefined;
@@ -33,10 +44,10 @@ export async function get_project_dir(): Promise<string | undefined> {
}
projectFile = file;
projectDir = path.dirname(file);
if (os.platform() === "win32") {
// capitalize the drive letter in windows absolute paths
projectDir = projectDir[0].toUpperCase() + projectDir.slice(1);
}
if (os.platform() === "win32") {
// capitalize the drive letter in windows absolute paths
projectDir = projectDir[0].toUpperCase() + projectDir.slice(1);
}
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));
}
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_RESULT = {
status: VERIFY_STATUS;

View File

@@ -4,7 +4,7 @@ import * as path from "node:path";
import * as vscode from "vscode";
export * from "./logger";
export * from "./project_utils";
export * from "./godot_utils";
export * from "./settings_updater";
export * from "./vscode_utils";

View File

@@ -35,8 +35,8 @@
},
"expression": {
"patterns": [
{ "include": "#base_expression" },
{ "include": "#getter_setter_godot4" },
{ "include": "#base_expression" },
{ "include": "#assignment_operator" },
{ "include": "#annotations" },
{ "include": "#class_name" },
@@ -74,6 +74,7 @@
{ "include": "#square_braces" },
{ "include": "#round_braces" },
{ "include": "#function_call" },
{ "include": "#region"},
{ "include": "#comment" },
{ "include": "#self" },
{ "include": "#func" },
@@ -83,6 +84,10 @@
{ "include": "#line_continuation" }
]
},
"region": {
"match": "#(end)?region.*$\\n?",
"name": "keyword.language.region.gdscript"
},
"comment": {
"match": "(##|#).*$\\n?",
"name": "comment.line.number-sign.gdscript",
@@ -170,7 +175,7 @@
}
]
},
{ "include": "#base_expression" }
{ "include": "#expression" }
]
},
"self": {
@@ -229,7 +234,7 @@
"name": "keyword.operator.comparison.gdscript"
},
"arithmetic_operator": {
"match": "->|\\+=|-=|\\*=|\\^=|/=|%=|&=|~=|\\|=|\\*\\*|\\*|/|%|\\+|-",
"match": "->|\\+=|-=|\\*\\*=|\\*=|\\^=|/=|%=|&=|~=|\\|=|\\*\\*|\\*|/|%|\\+|-",
"name": "keyword.operator.arithmetic.gdscript"
},
"assignment_operator": {
@@ -245,7 +250,7 @@
"captures": { "1": { "name": "keyword.control.gdscript" } }
},
"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"
},
"letter": {
@@ -267,7 +272,11 @@
"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"
},
{
@@ -293,7 +302,7 @@
"match": "(:)?\\s*(set|get)\\s+=\\s+([a-zA-Z_]\\w*)",
"captures": {
"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" }
}
},
@@ -311,7 +320,7 @@
{
"match": "(setget)\\s+([a-zA-Z_]\\w*)(?:[,]\\s*([a-zA-Z_]\\w*))?",
"captures": {
"1": { "name": "keyword.language.gdscript storage.type.const.gdscript" },
"1": { "name": "keyword.language.gdscript" },
"2": { "name": "entity.name.function.gdscript" },
"3": { "name": "entity.name.function.gdscript" }
}
@@ -326,18 +335,23 @@
"getter_setter_godot4": {
"patterns": [
{
"match": "\\b(get):",
"captures": { "1": { "name": "entity.name.function.gdscript" } }
"name": "meta.variable.declaration.getter.gdscript",
"match": "(get)\\s*(:)",
"captures": {
"1": { "name": "entity.name.function.gdscript" },
"2": { "name": "punctuation.separator.annotation.gdscript" }
}
},
{
"name": "meta.function.gdscript",
"begin": "(?x) \\s+\n (set) \\s*\n (?=\\()",
"end": "(:|(?=[#'\"\\n]))",
"beginCaptures": { "1": { "name": "entity.name.function.gdscript" } },
"patterns": [
{ "include": "#parameters" },
{ "include": "#line_continuation" }
]
"name": "meta.variable.declaration.setter.gdscript",
"match": "(set)\\s*(\\()\\s*([A-Za-z_]\\w*)\\s*(\\))\\s*(:)",
"captures": {
"1": { "name": "entity.name.function.gdscript" },
"2": { "name": "punctuation.definition.arguments.begin.gdscript" },
"3": { "name": "variable.other.gdscript" },
"4": { "name": "punctuation.definition.arguments.end.gdscript" },
"5": { "name": "punctuation.separator.annotation.gdscript" }
}
}
]
},
@@ -437,7 +451,7 @@
}
},
"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"
},
"const_vars": {
@@ -530,7 +544,7 @@
"end": "(,)|(?=\\))",
"beginCaptures": { "1": { "name": "keyword.operator.gdscript" } },
"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
"patterns": [ { "include": "#base_expression" } ]
"patterns": [ { "include": "#expression" } ]
},
"annotated_parameter": {
"begin": "(?x)\n \\s* ([a-zA-Z_]\\w*) \\s* (:)\\s* ([a-zA-Z_]\\w*)? \n",
@@ -542,7 +556,7 @@
"end": "(,)|(?=\\))",
"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
"patterns": [
{ "include": "#base_expression" },
{ "include": "#expression" },
{
"name": "keyword.operator.assignment.gdscript",
"match": "=(?!=)"

View File

@@ -17,8 +17,11 @@ func _ready() -> void:
var simple_array = [1, 2, 3]
var nested_dict = {
"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 int32_array = PackedInt32Array([100, 200, 300])
var color_var = Color(1, 0, 0, 1) # Red color

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,25 @@ extends Node
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:
var local1 := TestClassA.new()
var local2 = GlobalScript.globalMember
var str_var := "ScopeVars::_ready::local::str_var"
var self_var := self
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)

View File

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

View File

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