Compare commits

...

42 Commits

Author SHA1 Message Date
dependabot[bot]
0115b328de Bump actions/setup-node from 5.0.0 to 6.1.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5.0.0...v6.1.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 15:01:06 +00:00
dependabot[bot]
dadf188b98 Bump actions/setup-node from 4.4.0 to 5.0.0 (#917)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 5.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.4.0...v5.0.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-04 11:31:26 -04:00
Andreas Fehn
eb90637e39 Use cached values for project version and directory (#910) 2025-08-24 15:12:22 -04:00
Oasin Lyu
9d7187970a Recognize pascal-case identifiers that ends with 2 or more upper case letters as pascal_case_class (#908)
Co-authored-by: Seth <seth_lyu@aoki7studio.com>
2025-08-20 15:33:15 -04:00
dependabot[bot]
e1d80ad159 Bump actions/checkout from 4 to 5 (#902)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 16:25:15 -04:00
dependabot[bot]
73bf27ab8e Bump tmp from 0.2.1 to 0.2.4 (#900)
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.1 to 0.2.4.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.1...v0.2.4)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 14:47:56 -04:00
David Kincaid
fed2a2edab Inlay hints fix (#896)
* Fix issue displaying enums incorrectly

* Make inlay hints retrigger when the LSP connects

* Add "doubleclick to insert" to inlay hints
2025-08-02 10:19:50 -04:00
David Kincaid
37bb1116fb Debugger Tool Improvements (#848)
A variety of debugger internal fixes + linter/style improvements
2025-07-31 15:17:33 -04:00
HolonProduction
dfe97cb952 Recreate LSP Client to prevent out of sync state (#872) 2025-07-31 15:12:09 -04:00
Alexander Peck
bf5fcea38c Added ability to specify editorPath using environment variable (#807) (#856)
* Added ability to specify editorPath using environment variable

* Fix indentation

* Build the regex in the idiomatic way

* Add env syntax to configuration descriptions

* Add missing import

---------

Co-authored-by: David Kincaid <daelonsuzuka@gmail.com>
2025-07-26 16:39:49 -04:00
Danil Alexeev
45db62bfa3 Update GDScript syntax highlighter (#877) 2025-06-29 13:29:35 -04:00
HolonProduction
4bca5d71a6 Remove smart resolve from readme (#873) 2025-06-16 11:31:28 -04:00
dependabot[bot]
4b41776b16 Bump tar-fs from 2.1.2 to 2.1.3 (#869)
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.2 to 2.1.3.
- [Commits](https://github.com/mafintosh/tar-fs/commits)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-08 14:10:43 -04:00
EF
29734ea849 Fix extension soft lock when inspecting Dictionary variables with Variant key types (#854)
Co-authored-by: Pawel Miniszewski <pawel.miniszewski@gmail.com>
2025-05-11 16:28:23 -04:00
Tom Moertel
af6df23306 Teach formatter to optionally add two spaces before end-of-line comments. (#855)
* Teach formatter to add two spaces before end-of-line comments.
2025-05-11 16:19:19 -04:00
anthonyme00
4d00f9f41a Add support for uid:// references to hovers and document links (#841) 2025-04-26 16:39:46 -04:00
dependabot[bot]
6a3b1b6274 Bump prismjs from 1.29.0 to 1.30.0 (#819)
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.29.0 to 1.30.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.29.0...v1.30.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-26 16:30:00 -04:00
Joseph Straceski
0a7eb9c0e4 Remove exception guards (#839)
* Bump tar-fs from 2.1.1 to 2.1.2

Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.1 to 2.1.2.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.1...v2.1.2)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Removing exception tracking from debug_session.

* Replicate changes for Godot 3

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Kincaid <daelonsuzuka@gmail.com>
2025-04-26 16:25:20 -04:00
Asaf Shilo
911a34fda4 Fix GDScript Syntax Highlighting for "self" Keyword (#846)
* Rewrite rules for highlighting "self"

---------

Co-authored-by: David Kincaid <daelonsuzuka@gmail.com>
2025-04-26 16:00:15 -04:00
dependabot[bot]
d14e2ee280 Bump actions/upload-artifact from 4.6.1 to 4.6.2 (#828)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.1...v4.6.2)

---
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-04-20 13:15:22 -04:00
dependabot[bot]
60cd57767b Bump actions/setup-node from 4.3.0 to 4.4.0 (#847)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.3.0...v4.4.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-20 13:14:38 -04:00
testledjones
87e033e6ba Change debug_current_file error message (#836)
* Change debug_current_file error message

Currently, the error message in debug_current_file doesn't tell the user that the scene file and script file must share the same name. This fixes that

* Change message text

---------

Co-authored-by: David Kincaid <daelonsuzuka@gmail.com>
2025-04-20 13:12:07 -04:00
dependabot[bot]
1cc738bf9b Bump actions/setup-node from 4.2.0 to 4.3.0 (#826)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-18 12:38:53 -04:00
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
76 changed files with 3722 additions and 1898 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

@@ -11,12 +11,12 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v6.1.0
with:
node-version: 16.x
node-version: 22.x
- name: Install Godot (Ubuntu)
if: matrix.os == 'ubuntu-latest'
@@ -58,10 +58,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v6.1.0
with:
node-version: 16.x
@@ -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.2
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'],

View File

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

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,10 +204,19 @@ 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?
- GDScript is a dynamically typed script language. The language server can't
- GDScript is a gradually typed script language. The language server can't
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**.
- To increase the number of results displayed, use static typing in your scripts.
### 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,8 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
"vcs": {
"defaultBranch": "master"
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
@@ -6,18 +10,22 @@
"indentWidth": 4,
"lineWidth": 120,
"lineEnding": "lf",
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts", "tools/**/*.ts"]
},
"files": {
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "tools/**/*.ts"],
"ignore": ["node_modules"]
},
"linter": {
"rules": {
"style": {
"noUselessElse": "off",
"useImportType": "off"
}
"useImportType": "off",
"noParameterAssign": "warn"
},
"suspicious": {
"noExplicitAny": "off"
}
}
}
}

1372
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,8 +31,9 @@
],
"main": "./out/extension.js",
"scripts": {
"format": "biome format --write --changed src",
"compile": "tsc -p ./",
"lint": "eslint ./src --quiet",
"lint": "biome lint src",
"watch": "tsc -watch -p ./",
"package": "vsce package",
"vscode:prepublish": "npm run esbuild-base -- --minify",
@@ -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."
@@ -266,12 +272,12 @@
"godotTools.editorPath.godot3": {
"type": "string",
"default": "godot3",
"description": "Path to the Godot 3 editor executable"
"description": "Path to the Godot 3 editor executable. Supports environment variables using '${env:VAR_NAME}'."
},
"godotTools.editorPath.godot4": {
"type": "string",
"default": "godot",
"description": "Path to the Godot 4 editor executable"
"description": "Path to the Godot 4 editor executable. Supports environment variables using '${env:VAR_NAME}'."
},
"godotTools.editor.verbose": {
"type": "boolean",
@@ -301,6 +307,19 @@
"default": false,
"description": "Whether extra space should be removed from function parameter lists"
},
"godotTools.formatter.spacesBeforeEndOfLineComment": {
"type": "string",
"enum": [
"1",
"2"
],
"enumDescriptions": [
"1 space before EOL comments # Like this.",
"2 spaces before EOL comments  # Like this."
],
"default": "1",
"description": "Number of spaces before an end-of-line comment"
},
"godotTools.lsp.serverHost": {
"type": "string",
"default": "127.0.0.1",
@@ -636,32 +655,35 @@
"views": {
"debug": [
{
"id": "activeSceneTree",
"name": "Active Scene Tree"
"id": "godotTools.activeSceneTree",
"name": "Active Scene Tree",
"icon": "resources/godot_icon.svg"
},
{
"id": "inspectNode",
"name": "Inspector"
"id": "godotTools.nodeInspector",
"name": "Inspector",
"icon": "resources/godot_icon.svg"
}
],
"godotTools": [
{
"id": "scenePreview",
"name": "Scene Preview"
"id": "godotTools.scenePreview",
"name": "Scene Preview",
"icon": "resources/godot_icon.svg"
}
]
},
"viewsWelcome": [
{
"view": "activeSceneTree",
"view": "godotTools.activeSceneTree",
"contents": "Scene Tree data has not been requested"
},
{
"view": "inspectNode",
"view": "godotTools.nodeInspector",
"contents": "Node has not been inspected"
},
{
"view": "scenePreview",
"view": "godotTools.scenePreview",
"contents": "Open a Scene to see a preview of its structure"
}
],
@@ -715,92 +737,92 @@
"view/title": [
{
"command": "godotTools.debugger.refreshSceneTree",
"when": "view == activeSceneTree",
"when": "view == godotTools.activeSceneTree",
"group": "navigation"
},
{
"command": "godotTools.debugger.refreshInspector",
"when": "view == inspectNode",
"when": "view == godotTools.nodeInspector",
"group": "navigation"
},
{
"command": "godotTools.scenePreview.lock",
"when": "view == scenePreview && !godotTools.context.scenePreview.locked",
"when": "view == godotTools.scenePreview && !godotTools.context.scenePreview.locked",
"group": "navigation@1"
},
{
"command": "godotTools.scenePreview.unlock",
"when": "view == scenePreview && godotTools.context.scenePreview.locked",
"when": "view == godotTools.scenePreview && godotTools.context.scenePreview.locked",
"group": "navigation@1"
},
{
"command": "godotTools.scenePreview.refresh",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "navigation@2"
},
{
"command": "godotTools.scenePreview.openMainScript",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "navigation@3"
},
{
"command": "godotTools.scenePreview.openCurrentScene",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "navigation@4"
}
],
"view/item/context": [
{
"command": "godotTools.debugger.inspectNode",
"when": "view == activeSceneTree",
"when": "view == godotTools.activeSceneTree",
"group": "inline"
},
{
"command": "godotTools.debugger.inspectNode",
"when": "view == inspectNode && viewItem == remote_object",
"when": "view == godotTools.nodeInspector && viewItem == remote_object",
"group": "inline"
},
{
"command": "godotTools.debugger.editValue",
"when": "view == inspectNode && viewItem == editable_value",
"when": "view == godotTools.nodeInspector && viewItem == editable_value",
"group": "inline"
},
{
"command": "godotTools.scenePreview.goToDefinition",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "1@1"
},
{
"command": "godotTools.scenePreview.openDocumentation",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "1@1"
},
{
"command": "godotTools.scenePreview.copyNodePath",
"when": "view == scenePreview"
"when": "view == godotTools.scenePreview"
},
{
"command": "godotTools.scenePreview.copyResourcePath",
"when": "view == scenePreview && viewItem =~ /hasResourcePath/"
"when": "view == godotTools.scenePreview && viewItem =~ /hasResourcePath/"
},
{
"command": "godotTools.scenePreview.openScene",
"when": "view == scenePreview && viewItem =~ /openable/",
"when": "view == godotTools.scenePreview && viewItem =~ /openable/",
"group": "1@2"
},
{
"command": "godotTools.scenePreview.openScript",
"when": "view == scenePreview && viewItem =~ /hasScript/",
"when": "view == godotTools.scenePreview && viewItem =~ /hasScript/",
"group": "1@2"
},
{
"command": "godotTools.scenePreview.openScene",
"when": "view == scenePreview && viewItem =~ /openable/",
"when": "view == godotTools.scenePreview && viewItem =~ /openable/",
"group": "inline"
},
{
"command": "godotTools.scenePreview.openScript",
"when": "view == scenePreview && viewItem =~ /hasScript/",
"when": "view == godotTools.scenePreview && viewItem =~ /hasScript/",
"group": "inline"
}
],
@@ -870,25 +892,30 @@
}
},
"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/sinon": "^17.0.4",
"@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 +925,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",
"prismjs": "^1.30.0",
"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

@@ -1,30 +1,32 @@
import * as fs from "fs";
import * as fs from "node:fs";
import { InvalidatedEvent } from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import {
CancellationToken,
DebugAdapterDescriptor,
DebugAdapterDescriptorFactory,
DebugAdapterInlineImplementation,
DebugConfiguration,
DebugConfigurationProvider,
DebugSession,
EventEmitter,
ExtensionContext,
FileDecoration,
FileDecorationProvider,
ProviderResult,
Uri,
WorkspaceFolder,
debug,
window,
workspace,
ExtensionContext,
DebugConfigurationProvider,
WorkspaceFolder,
DebugAdapterInlineImplementation,
DebugAdapterDescriptorFactory,
DebugConfiguration,
DebugAdapterDescriptor,
DebugSession,
CancellationToken,
ProviderResult,
FileDecoration,
FileDecorationProvider,
Uri,
EventEmitter,
Event,
} from "vscode";
import { DebugProtocol } from "@vscode/debugprotocol";
import { createLogger, get_project_version, register_command, set_context } from "../utils";
import { GodotVariable } from "./debug_runtime";
import { GodotDebugSession as Godot3DebugSession } from "./godot3/debug_session";
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 { GodotObject } from "./godot4/variables/godot_object_promise";
import { InspectorProvider, RemoteProperty } from "./inspector_provider";
import { SceneNode, SceneTreeProvider } from "./scene_tree_provider";
const log = createLogger("debugger", { output: "Godot Debugger" });
@@ -58,37 +60,12 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum
export let pinnedScene: Uri;
export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfigurationProvider, FileDecorationProvider {
public session?: Godot3DebugSession | Godot4DebugSession;
public inspectorProvider = new InspectorProvider();
public sceneTreeProvider = new SceneTreeProvider();
class GDFileDecorationProvider implements FileDecorationProvider {
private emitter = new EventEmitter<Uri>();
onDidChangeFileDecorations = this.emitter.event;
private _onDidChangeFileDecorations = new EventEmitter<Uri>();
get onDidChangeFileDecorations(): Event<Uri> {
return this._onDidChangeFileDecorations.event;
}
constructor(private context: ExtensionContext) {
log.info("Initializing Godot Debugger");
this.restore_pinned_file();
context.subscriptions.push(
debug.registerDebugConfigurationProvider("godot", this),
debug.registerDebugAdapterDescriptorFactory("godot", this),
window.registerTreeDataProvider("inspectNode", this.inspectorProvider),
window.registerTreeDataProvider("activeSceneTree", this.sceneTreeProvider),
window.registerFileDecorationProvider(this),
register_command("debugger.inspectNode", this.inspect_node.bind(this)),
register_command("debugger.refreshSceneTree", this.refresh_scene_tree.bind(this)),
register_command("debugger.refreshInspector", this.refresh_inspector.bind(this)),
register_command("debugger.editValue", this.edit_value.bind(this)),
register_command("debugger.debugCurrentFile", this.debug_current_file.bind(this)),
register_command("debugger.debugPinnedFile", this.debug_pinned_file.bind(this)),
register_command("debugger.pinFile", this.pin_file.bind(this)),
register_command("debugger.unpinFile", this.unpin_file.bind(this)),
register_command("debugger.openPinnedFile", this.open_pinned_file.bind(this)),
);
update(uri: Uri) {
this.emitter.fire(uri);
}
provideFileDecoration(uri: Uri, token: CancellationToken): FileDecoration | undefined {
@@ -99,6 +76,37 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
};
}
}
}
export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfigurationProvider {
public session?: Godot3DebugSession | Godot4DebugSession;
public sceneTree = new SceneTreeProvider();
public inspector = new InspectorProvider();
fileDecorations = new GDFileDecorationProvider();
constructor(private context: ExtensionContext) {
log.info("Initializing Godot Debugger");
this.restore_pinned_file();
context.subscriptions.push(
debug.registerDebugConfigurationProvider("godot", this),
debug.registerDebugAdapterDescriptorFactory("godot", this),
window.registerFileDecorationProvider(this.fileDecorations),
register_command("debugger.inspectNode", this.inspect_node.bind(this)),
register_command("debugger.refreshSceneTree", this.refresh_scene_tree.bind(this)),
register_command("debugger.refreshInspector", this.refresh_inspector.bind(this)),
register_command("debugger.editValue", this.edit_value.bind(this)),
register_command("debugger.debugCurrentFile", this.debug_current_file.bind(this)),
register_command("debugger.debugPinnedFile", this.debug_pinned_file.bind(this)),
register_command("debugger.pinFile", this.pin_file.bind(this)),
register_command("debugger.unpinFile", this.unpin_file.bind(this)),
register_command("debugger.openPinnedFile", this.open_pinned_file.bind(this)),
this.inspector.view,
this.sceneTree.view,
);
}
public async createDebugAdapterDescriptor(session: DebugSession): Promise<DebugAdapterDescriptor> {
log.info("Creating debug session");
@@ -106,20 +114,25 @@ 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();
}
this.context.subscriptions.push(this.session);
this.session.sceneTree = this.sceneTreeProvider;
this.session.sceneTree = this.sceneTree;
this.session.inspector = this.inspector;
this.sceneTree.clear();
this.inspector.clear();
return new DebugAdapterInlineImplementation(this.session);
}
public resolveDebugConfiguration(
folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
token?: CancellationToken
token?: CancellationToken,
): ProviderResult<DebugConfiguration> {
// request is actually a required field according to vscode
// however, setting it here lets us catch a possible misconfiguration
@@ -154,7 +167,9 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
public debug_current_file() {
log.info("Attempting to debug current file");
const configs: DebugConfiguration[] = workspace.getConfiguration("launch", window.activeTextEditor.document.uri).get("configurations");
const configs: DebugConfiguration[] = workspace
.getConfiguration("launch", window.activeTextEditor.document.uri)
.get("configurations");
const launches = configs.filter((c) => c.request === "launch");
const currents = configs.filter((c) => c.scene === "current");
@@ -162,8 +177,9 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
if (path.endsWith(".gd")) {
const scenePath = path.replace(".gd", ".tscn");
if (!fs.existsSync(scenePath)) {
log.warn(`Can't find associated scene for '${path}', aborting debug`);
window.showWarningMessage(`Can't find associated scene file for '${path}'`);
const message = `Can't launch debug session: no associated scene for '${path}'. (Script and scene file must have the same name.)`;
log.warn(message);
window.showWarningMessage(message);
return;
}
path = scenePath;
@@ -219,17 +235,18 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
}
public pin_file(uri: Uri) {
let _uri = uri;
if (uri === undefined) {
uri = window.activeTextEditor.document.uri;
_uri = window.activeTextEditor.document.uri;
}
log.info(`Pinning debug target file: '${uri.fsPath}'`);
set_context("pinnedScene", [uri.fsPath]);
log.info(`Pinning debug target file: '${_uri.fsPath}'`);
set_context("pinnedScene", [_uri.fsPath]);
if (pinnedScene) {
this._onDidChangeFileDecorations.fire(pinnedScene);
this.fileDecorations.update(pinnedScene);
}
pinnedScene = uri;
pinnedScene = _uri;
this.context.workspaceState.update("pinnedScene", pinnedScene);
this._onDidChangeFileDecorations.fire(uri);
this.fileDecorations.update(_uri);
}
public unpin_file(uri: Uri) {
@@ -238,7 +255,7 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
const previousPinnedScene = pinnedScene;
pinnedScene = undefined;
this.context.workspaceState.update("pinnedScene", pinnedScene);
this._onDidChangeFileDecorations.fire(previousPinnedScene);
this.fileDecorations.update(previousPinnedScene);
}
public restore_pinned_file() {
@@ -256,121 +273,98 @@ 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_inspector(element);
}
private async fill_inspector(element: SceneNode | RemoteProperty, force_refresh = false) {
if (this.session instanceof Godot4DebugSession) {
const godot_object = await this.session.variables_manager?.get_godot_object(
BigInt(element.object_id),
force_refresh,
);
if (!godot_object) {
return;
}
const va = this.create_godot_variable(godot_object);
this.inspector.fill_tree(element.label, godot_object.type, Number(godot_object.godot_id), va);
} else {
this.session?.controller.request_inspect_object(BigInt(element.object_id));
this.session?.inspect_callbacks.set(BigInt(element.object_id), (class_name, variable) => {
this.inspector.fill_tree(element.label, class_name, Number(element.object_id), variable);
});
}
}
private create_godot_variable(godot_object: GodotObject): GodotVariable {
return {
value: {
type_name: () => godot_object.type,
stringify_value: () => `<${godot_object.godot_id}>`,
sub_values: () => 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));
this.session?.inspect_callbacks.set(
BigInt(id),
(class_name, variable) => {
this.inspectorProvider.fill_tree(
name,
class_name,
id,
variable
);
},
);
public async refresh_inspector() {
if (this.inspector.has_tree()) {
const item = this.inspector.get_top_item();
await this.fill_inspector(item, /*force_refresh*/ true);
}
}
public edit_value(property: RemoteProperty) {
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.inspector.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 item = this.inspector.get_top_item();
await this.fill_inspector(item, /*force_refresh*/ true);
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
);
},
);
});
// const res = await debug.activeDebugSession?.customRequest("refreshVariables"); // refresh vscode.debug variables
this.session.sendEvent(new InvalidatedEvent(["variables"]));
}
}

View File

@@ -1,3 +1,4 @@
import * as fs from "node:fs";
import {
Breakpoint,
InitializedEvent,
@@ -8,12 +9,11 @@ import {
} from "@vscode/debugadapter";
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 { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
import { InspectorProvider } from "../inspector_provider";
import { SceneTreeProvider } from "../scene_tree_provider";
import { is_variable_built_in_type, parse_variable } from "./helpers";
import { ServerController } from "./server_controller";
@@ -32,7 +32,7 @@ export class GodotDebugSession extends LoggingDebugSession {
public controller = new ServerController(this);
public debug_data = new GodotDebugData(this);
public sceneTree: SceneTreeProvider;
private exception = false;
public inspector: InspectorProvider;
private got_scope: Subject = new Subject();
private ongoing_inspections: bigint[] = [];
private previous_inspections: bigint[] = [];
@@ -88,7 +88,6 @@ export class GodotDebugSession extends LoggingDebugSession {
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
@@ -99,7 +98,6 @@ export class GodotDebugSession extends LoggingDebugSession {
this.mode = "attach";
this.exception = false;
await this.controller.attach(args);
this.sendResponse(response);
@@ -114,11 +112,9 @@ export class GodotDebugSession extends LoggingDebugSession {
}
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
@@ -149,17 +145,13 @@ export class GodotDebugSession extends LoggingDebugSession {
}
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
this.controller.next();
this.sendResponse(response);
}
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
this.controller.break();
this.sendResponse(response);
}
protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
@@ -234,17 +226,13 @@ export class GodotDebugSession extends LoggingDebugSession {
}
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
this.controller.step();
this.sendResponse(response);
}
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
if (!this.exception) {
this.controller.step_out();
this.sendResponse(response);
}
this.controller.step_out();
this.sendResponse(response);
}
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
@@ -303,10 +291,6 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
public set_exception(exception: boolean) {
this.exception = true;
}
public set_scopes(stackVars: GodotStackVars) {
this.all_scopes = [
undefined,
@@ -407,7 +391,7 @@ export class GodotDebugSession extends LoggingDebugSession {
if (!root) {
if (!expression.includes("self")) {
expression = "self." + expression;
expression = `self.${expression}`;
}
root = this.all_scopes.find((x) => x && x.name === "self");

View File

@@ -30,8 +30,8 @@ export function split_buffers(buffer: Buffer) {
}
export function is_variable_built_in_type(va: GodotVariable) {
var type = typeof va.value;
return ["number", "bigint", "boolean", "string"].some(x => x == type);
const type = typeof va.value;
return ["number", "bigint", "boolean", "string"].some(x => x === type);
}
export function build_sub_values(va: GodotVariable) {
@@ -45,7 +45,7 @@ export function build_sub_values(va: GodotVariable) {
});
} else if (value instanceof Map) {
subValues = Array.from(value.keys()).map((va) => {
if (typeof va["stringify_value"] === "function") {
if (typeof va.stringify_value === "function") {
return {
name: `${va.type_name()}${va.stringify_value()}`,
value: value.get(va),
@@ -57,7 +57,7 @@ export function build_sub_values(va: GodotVariable) {
} as GodotVariable;
}
});
} else if (value && typeof value["sub_values"] === "function") {
} else if (value && typeof value.sub_values === "function") {
subValues = value.sub_values().map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
});
@@ -79,7 +79,7 @@ export function parse_variable(va: GodotVariable, i?: number) {
if (Number.isInteger(value)) {
rendered_value = `${value}`;
} else {
rendered_value = `${parseFloat(value.toFixed(5))}`;
rendered_value = `${Number.parseFloat(value.toFixed(5))}`;
}
} else if (
typeof value === "bigint" ||
@@ -96,7 +96,7 @@ export function parse_variable(va: GodotVariable, i?: number) {
array_type = "indexed";
reference = i ? i : 0;
} else if (value instanceof Map) {
rendered_value = value["class_name"] ?? `Dictionary[${value.size}]`;
rendered_value = value.get("class_name") ?? `Dictionary[${value.size}]`;
array_size = value.size;
array_type = "named";
reference = i ? i : 0;

View File

@@ -23,16 +23,19 @@ 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 = "";
public paramCount: number = -1;
public command = "";
public paramCount = -1;
public parameters: any[] = [];
public complete: boolean = false;
public threadId: number = 0;
public complete = false;
public threadId = 0;
}
export class ServerController {
@@ -45,7 +48,7 @@ export class ServerController {
private socket?: net.Socket;
private steppingOut = false;
private currentCommand: Command = undefined;
private didFirstOutput: boolean = false;
private didFirstOutput = false;
private connectedVersion = "";
public constructor(public session: GodotDebugSession) {}
@@ -427,9 +430,10 @@ 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) {
for (const line of output[0].split("\n")) {
debug.activeDebugConsole.appendLine(bbcodeParser.parse(line));
}
}
break;
}
@@ -587,7 +591,6 @@ export class ServerController {
if (this.exception.length === 0) {
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
} else {
this.session.set_exception(true);
this.session.sendEvent(new StoppedEvent("exception", 0, this.exception));
}
}
@@ -631,6 +634,7 @@ export class ServerController {
stackVars.globals.push({ name: parameters[i++], value: parameters[i++] });
}
// biome-ignore lint/complexity/noForEach: <custom forEach impl>
stackVars.forEach((item) => build_sub_values(item));
this.session.set_scopes(stackVars);

View File

@@ -89,7 +89,7 @@ export class VariantDecoder {
public get_dataset(buffer: Buffer) {
const len = buffer.readUInt32LE(0);
if (buffer.length != len + 4) {
if (buffer.length !== len + 4) {
return undefined;
}
const model: BufferModel = {

View File

@@ -125,6 +125,7 @@ export class VariantEncoder {
private encode_Array(arr: any[], model: BufferModel) {
const size = arr.length;
this.encode_UInt32(size, model);
// biome-ignore lint/complexity/noForEach: <explanation>
arr.forEach((e) => {
this.encode_variant(e, model);
});
@@ -151,6 +152,7 @@ export class VariantEncoder {
const size = dict.size;
this.encode_UInt32(size, model);
const keys = Array.from(dict.keys());
// biome-ignore lint/complexity/noForEach: <explanation>
keys.forEach((key) => {
const value = dict.get(key);
this.encode_variant(key, model);
@@ -239,6 +241,7 @@ export class VariantEncoder {
private size_Dictionary(dict: Map<any, any>): number {
let size = this.size_UInt32();
const keys = Array.from(dict.keys());
// biome-ignore lint/complexity/noForEach: <explanation>
keys.forEach((key) => {
const value = dict.get(key);
size += this.size_variant(key);
@@ -266,6 +269,7 @@ export class VariantEncoder {
private size_array(arr: any[]): number {
let size = this.size_UInt32();
// biome-ignore lint/complexity/noForEach: <explanation>
arr.forEach((e) => {
size += this.size_variant(e);
});
@@ -316,6 +320,7 @@ export class VariantEncoder {
size += this.size_Dictionary(value);
break;
} else {
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
switch (value["__type__"]) {
case "Vector2":
size += this.size_UInt32() * 2;

View File

@@ -1,44 +1,44 @@
import { GodotVariable } from "../../debug_runtime";
export enum GDScriptTypes {
NIL,
NIL = 0,
// atomic types
BOOL,
INT,
REAL,
STRING,
BOOL = 1,
INT = 2,
REAL = 3,
STRING = 4,
// math types
VECTOR2, // 5
RECT2,
VECTOR3,
TRANSFORM2D,
PLANE,
QUAT, // 10
AABB,
BASIS,
TRANSFORM,
VECTOR2 = 5,
RECT2 = 6,
VECTOR3 = 7,
TRANSFORM2D = 8,
PLANE = 9,
QUAT = 10,
AABB = 11,
BASIS = 12,
TRANSFORM = 13,
// misc types
COLOR,
NODE_PATH, // 15
_RID,
OBJECT,
DICTIONARY,
ARRAY,
COLOR = 14,
NODE_PATH = 15,
_RID = 16,
OBJECT = 17,
DICTIONARY = 18,
ARRAY = 19,
// arrays
POOL_BYTE_ARRAY, // 20
POOL_INT_ARRAY,
POOL_REAL_ARRAY,
POOL_STRING_ARRAY,
POOL_VECTOR2_ARRAY,
POOL_VECTOR3_ARRAY, // 25
POOL_COLOR_ARRAY,
POOL_BYTE_ARRAY = 20,
POOL_INT_ARRAY = 21,
POOL_REAL_ARRAY = 22,
POOL_STRING_ARRAY = 23,
POOL_VECTOR2_ARRAY = 24,
POOL_VECTOR3_ARRAY = 25,
POOL_COLOR_ARRAY = 26,
VARIANT_MAX,
VARIANT_MAX = 27,
}
export interface BufferModel {
@@ -59,9 +59,9 @@ function clean_number(value: number) {
export class Vector3 implements GDObject {
constructor(
public x: number = 0.0,
public y: number = 0.0,
public z: number = 0.0
public x = 0.0,
public y = 0.0,
public z = 0.0
) {}
public stringify_value(): string {
@@ -84,7 +84,7 @@ export class Vector3 implements GDObject {
}
export class Vector2 implements GDObject {
constructor(public x: number = 0.0, public y: number = 0.0) {}
constructor(public x = 0.0, public y = 0.0) {}
public stringify_value(): string {
return `(${clean_number(this.x)}, ${clean_number(this.y)})`;
@@ -146,7 +146,7 @@ export class Color implements GDObject {
public r: number,
public g: number,
public b: number,
public a: number = 1.0
public a = 1.0
) {}
public stringify_value(): string {

View File

@@ -1,50 +1,41 @@
import {
Breakpoint,
InitializedEvent,
LoggingDebugSession,
Source,
TerminatedEvent,
Thread,
Breakpoint,
InitializedEvent,
LoggingDebugSession,
Source,
TerminatedEvent,
Thread,
} from "@vscode/debugadapter";
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 { InspectorProvider } from "../inspector_provider";
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[] = [];
public inspector: InspectorProvider;
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 = new VariablesManager(this.controller);
public constructor(projectVersion: string) {
super();
this.setDebuggerLinesStartAt1(false);
this.setDebuggerColumnsStartAt1(false);
this.controller.setProjectVersion(projectVersion);
}
public dispose() {
@@ -90,7 +81,6 @@ export class GodotDebugSession extends LoggingDebugSession {
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
@@ -102,7 +92,7 @@ export class GodotDebugSession extends LoggingDebugSession {
this.mode = "attach";
this.exception = false;
this.debug_data.projectPath = args.project;
await this.controller.attach(args);
this.sendResponse(response);
@@ -119,69 +109,20 @@ export class GodotDebugSession extends LoggingDebugSession {
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
log.info("continueRequest", args);
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
}
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,
};
}
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
log.info("nextRequest", args);
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
this.controller.next();
this.sendResponse(response);
}
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
log.info("pauseRequest", args);
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
}
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.controller.break();
this.sendResponse(response);
}
@@ -225,6 +166,34 @@ export class GodotDebugSession extends LoggingDebugSession {
}
}
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
log.info("stepInRequest", args);
this.controller.step();
this.sendResponse(response);
}
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
log.info("stepOutRequest", args);
this.controller.step_out();
this.sendResponse(response);
}
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
log.info("terminateRequest", args);
if (this.mode === "launch") {
this.controller.stop();
this.sendEvent(new TerminatedEvent());
}
this.sendResponse(response);
}
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) {
@@ -241,37 +210,33 @@ export class GodotDebugSession extends LoggingDebugSession {
}),
};
}
log.info("stackTraceRequest response", response);
this.sendResponse(response);
}
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
log.info("stepInRequest", args);
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
}
protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
log.info("scopesRequest", args);
// this.variables_manager.variablesFrameId = args.frameId;
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
log.info("stepOutRequest", args);
if (!this.exception) {
this.controller.step_out();
this.sendResponse(response);
}
}
// 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 },
];
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
log.info("terminateRequest", args);
if (this.mode === "launch") {
this.controller.stop();
this.sendEvent(new TerminatedEvent());
}
this.sendResponse(response);
}
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 },
// ],
};
protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
log.info("threadsRequest");
response.body = { threads: [new Thread(0, "thread_1")] };
log.info("scopesRequest response", response);
this.sendResponse(response);
}
@@ -280,291 +245,44 @@ 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[];
if (!reference.sub_values) {
variables = [];
} else {
variables = reference.sub_values.map((va) => {
const sva = this.all_scopes.find(
(sva) => sva && sva.scope_path === va.scope_path && sva.name === va.name,
);
if (sva) {
return parse_variable(
sva,
this.all_scopes.findIndex(
(va_idx) =>
va_idx &&
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
va_idx.name === va.name,
),
);
}
});
}
response.body = {
variables: variables,
};
log.info("variablesRequest response", response);
this.sendResponse(response);
}
public set_exception(exception: boolean) {
this.exception = true;
}
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
log.info("evaluateRequest", args);
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);
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,
};
}
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);
});
}
log.info("evaluateRequest response", response);
this.sendResponse(response);
}
}

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,20 +41,16 @@ 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;
}
});
} else if (typeof value["sub_values"] === "function") {
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

@@ -1,10 +1,11 @@
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import * as fs from "node:fs";
import * as net from "node:net";
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import BBCodeToAnsi from "bbcode-to-ansi";
import { debug, window } from "vscode";
import {
VERIFY_RESULT,
ansi,
convert_resource_path_to_uri,
createLogger,
@@ -12,11 +13,10 @@ import {
get_free_port,
get_project_version,
verify_godot_version,
VERIFY_RESULT,
} 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";
@@ -26,13 +26,42 @@ import { RawObject } from "./variables/variants";
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 = "";
public paramCount: number = -1;
public parameters: any[] = [];
public complete: boolean = false;
public threadId: number = 0;
public command = "";
public paramCount = -1;
public parameters = [];
public complete = false;
public threadId = 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 {
@@ -46,11 +75,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 +131,14 @@ 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 +212,7 @@ export class ServerController {
}
}
this.connectedVersion = result.version;
this.setProjectVersion(result.version);
let command = `"${godotPath}" --path "${args.project}"`;
const address = args.address.replace("tcp://", "");
@@ -345,7 +389,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,6 +406,7 @@ export class ServerController {
this.set_exception("");
}
this.request_stack_dump();
this.request_scene_tree();
break;
}
case "debug_exit":
@@ -381,14 +426,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 +441,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 +473,72 @@ 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;
}
const name: string = command.parameters[0];
const scope: 0 | 1 | 2 = command.parameters[1]; // 0 = locals, 1 = members, 2 = globals
const type: number = command.parameters[2];
const value: any = command.parameters[3];
const 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 +546,11 @@ 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);
const console = debug.activeDebugConsole;
for (const output of command.parameters[0]) {
for (const line of output.split("\n")) {
console.appendLine(bbcodeParser.parse(line));
}
}
break;
}
@@ -605,18 +712,17 @@ export class ServerController {
if (this.exception.length === 0) {
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
} else {
this.session.set_exception(true);
this.session.sendEvent(new StoppedEvent("exception", 0, this.exception));
}
}
private send_command(command: string, parameters?: any[]) {
const commandArray: any[] = [command];
if (this.connectedVersion[2] >= "2") {
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 +738,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,468 @@
import { promises as fs } from "node:fs";
import * as path from "node:path";
import * as vscode from "vscode";
import { DebugProtocol } from "@vscode/debugprotocol";
import chai from "chai";
import chaiSubset from "chai-subset";
const 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 "node:util";
import { execFile } from "node:child_process";
import { clean_godot_path } from "../../../utils";
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 (true) {
match = breakpointRegex.exec(script_content);
if (match === null) {
break;
}
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 = 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 = 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 = 0,
Members = 1,
Globals = 2,
}
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 = 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}`;
}
let 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);
const godot4_path = clean_godot_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 !== "") {
// TODO: was preventing tests from running
// throw new Error(exec_res.stderr);
console.log(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("should return correct scopes", async () => {
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 (let 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 () => {
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 () => {
/** {@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 () => {
/** {@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 () => {
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 () => {
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
const 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: sinon.SinonFakeTimers;
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,285 @@
import { DebugProtocol } from "@vscode/debugprotocol";
import { GodotVariable } from "../../debug_runtime";
import { ServerController } from "../server_controller";
import { GodotIdToVscodeIdMapper, GodotIdWithPath } from "./godot_id_to_vscode_id_mapper";
import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
import { ObjectId } from "./variants";
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 {
let 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
}
}
}
}
let 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(".");
let parent_id: bigint;
for (let 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 (const 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) {
parent_id = godot_id;
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,
parent_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 = `${Number.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) {
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
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

@@ -210,6 +210,9 @@ export class VariantDecoder {
private decode_ContainerTypeFlag(model: BufferModel, type: GDScriptTypes, bitOffset: number) {
const shiftedType = (type >> bitOffset) & 0b11;
if (shiftedType === ContainerTypeFlags.NONE) {
return 0;
}
if (shiftedType === ContainerTypeFlags.BUILTIN) {
return this.decode_UInt32(model);
} else {

View File

@@ -154,6 +154,7 @@ export class VariantEncoder {
private encode_Array(arr: any[], model: BufferModel) {
const size = arr.length;
this.encode_UInt32(size, model);
// biome-ignore lint/complexity/noForEach: <explanation>
arr.forEach((e) => {
this.encode_variant(e, model);
});
@@ -180,6 +181,7 @@ export class VariantEncoder {
const size = dict.size;
this.encode_UInt32(size, model);
const keys = Array.from(dict.keys());
// biome-ignore lint/complexity/noForEach: <explanation>
keys.forEach((key) => {
const value = dict.get(key);
this.encode_variant(key, model);
@@ -314,6 +316,7 @@ export class VariantEncoder {
private size_Dictionary(dict: Map<any, any>): number {
let size = this.size_UInt32();
const keys = Array.from(dict.keys());
// biome-ignore lint/complexity/noForEach: <explanation>
keys.forEach((key) => {
const value = dict.get(key);
size += this.size_variant(key);
@@ -341,6 +344,7 @@ export class VariantEncoder {
private size_array(arr: any[]): number {
let size = this.size_UInt32();
// biome-ignore lint/complexity/noForEach: <explanation>
arr.forEach((e) => {
size += this.size_variant(e);
});
@@ -395,6 +399,7 @@ export class VariantEncoder {
size += this.size_String(value.value);
break;
} else {
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
switch (value["__type__"]) {
case "Vector2":
case "Vector2i":

View File

@@ -1,55 +1,55 @@
import { GodotVariable } from "../../debug_runtime";
export enum GDScriptTypes {
NIL,
NIL = 0,
// atomic types
BOOL,
INT,
FLOAT,
STRING,
BOOL = 1,
INT = 2,
FLOAT = 3,
STRING = 4,
// math types
VECTOR2,
VECTOR2I,
RECT2,
RECT2I,
VECTOR3,
VECTOR3I,
TRANSFORM2D,
VECTOR4,
VECTOR4I,
PLANE,
QUATERNION,
AABB,
BASIS,
TRANSFORM3D,
PROJECTION,
VECTOR2 = 5,
VECTOR2I = 6,
RECT2 = 7,
RECT2I = 8,
VECTOR3 = 9,
VECTOR3I = 10,
TRANSFORM2D = 11,
VECTOR4 = 12,
VECTOR4I = 13,
PLANE = 14,
QUATERNION = 15,
AABB = 16,
BASIS = 17,
TRANSFORM3D = 18,
PROJECTION = 19,
// misc types
COLOR,
STRING_NAME,
NODE_PATH,
RID,
OBJECT,
CALLABLE,
SIGNAL,
DICTIONARY,
ARRAY,
COLOR = 20,
STRING_NAME = 21,
NODE_PATH = 22,
RID = 23,
OBJECT = 24,
CALLABLE = 25,
SIGNAL = 26,
DICTIONARY = 27,
ARRAY = 28,
// typed arrays
PACKED_BYTE_ARRAY,
PACKED_INT32_ARRAY,
PACKED_INT64_ARRAY,
PACKED_FLOAT32_ARRAY,
PACKED_FLOAT64_ARRAY,
PACKED_STRING_ARRAY,
PACKED_VECTOR2_ARRAY,
PACKED_VECTOR3_ARRAY,
PACKED_COLOR_ARRAY,
PACKED_VECTOR4_ARRAY,
PACKED_BYTE_ARRAY = 29,
PACKED_INT32_ARRAY = 30,
PACKED_INT64_ARRAY = 31,
PACKED_FLOAT32_ARRAY = 32,
PACKED_FLOAT64_ARRAY = 33,
PACKED_STRING_ARRAY = 34,
PACKED_VECTOR2_ARRAY = 35,
PACKED_VECTOR3_ARRAY = 36,
PACKED_COLOR_ARRAY = 37,
PACKED_VECTOR4_ARRAY = 38,
VARIANT_MAX
VARIANT_MAX = 39
}
export const ENCODE_FLAG_64 = 1 << 16;
@@ -58,6 +58,7 @@ export const ENCODE_FLAG_TYPED_ARRAY_MASK = 0b11 << 16;
export const ENCODE_FLAG_TYPED_DICT_MASK = 0b1111 << 16;
export enum ContainerTypeFlags {
NONE = 0,
BUILTIN = 1,
CLASS_NAME = 2,
SCRIPT = 3,
@@ -81,9 +82,9 @@ function clean_number(value: number) {
export class Vector3 implements GDObject {
constructor(
public x: number = 0.0,
public y: number = 0.0,
public z: number = 0.0
public x = 0.0,
public y = 0.0,
public z = 0.0
) {}
public stringify_value(): string {
@@ -114,10 +115,10 @@ export class Vector3i extends Vector3 {
export class Vector4 implements GDObject {
constructor(
public x: number = 0.0,
public y: number = 0.0,
public z: number = 0.0,
public w: number = 0.0
public x = 0.0,
public y = 0.0,
public z = 0.0,
public w = 0.0
) {}
public stringify_value(): string {
@@ -147,7 +148,7 @@ export class Vector4i extends Vector4 {
}
export class Vector2 implements GDObject {
constructor(public x: number = 0.0, public y: number = 0.0) {}
constructor(public x = 0.0, public y = 0.0) {}
public stringify_value(): string {
return `(${clean_number(this.x)}, ${clean_number(this.y)})`;
@@ -216,7 +217,7 @@ export class Color implements GDObject {
public r: number,
public g: number,
public b: number,
public a: number = 1.0
public a = 1.0
) {}
public stringify_value(): string {
@@ -283,7 +284,7 @@ export class ObjectId implements GDObject {
}
public type_name(): string {
return "Object";
return "ObjectId";
}
}

View File

@@ -1,38 +1,46 @@
import { TreeDataProvider, EventEmitter, Event, ProviderResult, TreeItem, TreeItemCollapsibleState } from "vscode";
import { GodotVariable, RawObject, ObjectId } from "./debug_runtime";
import { EventEmitter, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, window } from "vscode";
import { GodotVariable, ObjectId, RawObject } from "./debug_runtime";
export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
private _on_did_change_tree_data: EventEmitter<RemoteProperty | undefined> = new EventEmitter<
RemoteProperty | undefined
>();
private tree: RemoteProperty | undefined;
private changeTreeEvent = new EventEmitter<RemoteProperty>();
onDidChangeTreeData = this.changeTreeEvent.event;
public readonly onDidChangeTreeData: Event<RemoteProperty> | undefined = this._on_did_change_tree_data.event;
private root: RemoteProperty | undefined;
public view: TreeView<RemoteProperty>;
public clean_up() {
if (this.tree) {
this.tree = undefined;
this._on_did_change_tree_data.fire(undefined);
constructor() {
this.view = window.createTreeView("godotTools.nodeInspector", {
treeDataProvider: this,
});
}
public clear() {
this.view.description = undefined;
this.view.message = undefined;
if (this.root) {
this.root = undefined;
this.changeTreeEvent.fire(undefined);
}
}
public fill_tree(element_name: string, class_name: string, object_id: number, variable: GodotVariable) {
this.tree = this.parse_variable(variable, object_id);
this.tree.label = element_name;
this.tree.collapsibleState = TreeItemCollapsibleState.Expanded;
this.tree.description = class_name;
this._on_did_change_tree_data.fire(undefined);
this.root = this.parse_variable(variable, object_id);
this.root.label = element_name;
this.root.collapsibleState = TreeItemCollapsibleState.Expanded;
this.root.description = class_name;
this.changeTreeEvent.fire(undefined);
}
public getChildren(element?: RemoteProperty): ProviderResult<RemoteProperty[]> {
if (!this.tree) {
return Promise.resolve([]);
public getChildren(element?: RemoteProperty): RemoteProperty[] {
if (!this.root) {
return [];
}
if (!element) {
return Promise.resolve([this.tree]);
return [this.root];
} else {
return Promise.resolve(element.properties);
return element.properties;
}
}
@@ -57,25 +65,18 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
return value;
}
public get_top_id(): number {
if (this.tree) {
return this.tree.object_id;
}
return undefined;
}
public get_top_name() {
if (this.tree) {
return this.tree.label;
public get_top_item(): RemoteProperty {
if (this.root) {
return this.root;
}
return undefined;
}
public has_tree() {
return this.tree !== undefined;
return this.root !== undefined;
}
private parse_variable(va: GodotVariable, object_id?: number) {
private parse_variable(va: GodotVariable, object_id?: number): RemoteProperty {
const value = va.value;
let rendered_value = "";

View File

@@ -1,42 +1,46 @@
import {
TreeDataProvider,
EventEmitter,
Event,
ProviderResult,
TreeItem,
TreeItemCollapsibleState,
Uri
} from "vscode";
import path = require("path");
import * as path from "node:path";
import { EventEmitter, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri, window } from "vscode";
import { get_extension_uri } from "../utils";
const iconDir = get_extension_uri("resources", "godot_icons").fsPath;
export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
private _on_did_change_tree_data: EventEmitter<
SceneNode | undefined
> = new EventEmitter<SceneNode | undefined>();
private tree: SceneNode | undefined;
private changeTreeEvent = new EventEmitter<SceneNode>();
onDidChangeTreeData = this.changeTreeEvent.event;
public readonly onDidChangeTreeData: Event<SceneNode> | undefined = this
._on_did_change_tree_data.event;
private root: SceneNode | undefined;
public view: TreeView<SceneNode>;
constructor() { }
public fill_tree(tree: SceneNode) {
this.tree = tree;
this._on_did_change_tree_data.fire(undefined);
constructor() {
this.view = window.createTreeView("godotTools.activeSceneTree", {
treeDataProvider: this,
});
}
public getChildren(element?: SceneNode): ProviderResult<SceneNode[]> {
if (!this.tree) {
return Promise.resolve([]);
public clear() {
this.view.description = undefined;
this.view.message = undefined;
if (this.root) {
this.root = undefined;
this.changeTreeEvent.fire(undefined);
}
}
public fill_tree(node: SceneNode) {
this.root = node;
this.changeTreeEvent.fire(undefined);
}
public getChildren(element?: SceneNode): SceneNode[] {
if (!this.root) {
return [];
}
if (!element) {
return Promise.resolve([this.tree]);
return [this.root];
} else {
return Promise.resolve(element.children);
return element.children;
}
}
@@ -45,10 +49,10 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
const tree_item: TreeItem = new TreeItem(
element.label,
has_children
? element === this.tree
? element === this.root
? TreeItemCollapsibleState.Expanded
: TreeItemCollapsibleState.Collapsed
: TreeItemCollapsibleState.None
: TreeItemCollapsibleState.None,
);
tree_item.description = element.class_name;
@@ -79,7 +83,7 @@ export class SceneNode extends TreeItem {
) {
super(label);
const iconName = class_name + ".svg";
const iconName = `${class_name}.svg`;
this.iconPath = {
light: Uri.file(path.join(iconDir, "light", iconName)),

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 });
@@ -226,14 +259,14 @@ class GodotEditorTerminal implements vscode.Pseudoterminal {
proc.stdout.on("data", (data) => {
const out = data.toString().trim();
if (out) {
this.writeEmitter.fire(data + "\r\n");
this.writeEmitter.fire(`${data}\r\n`);
}
});
proc.stderr.on("data", (data) => {
const out = data.toString().trim();
if (out) {
this.writeEmitter.fire(data + "\r\n");
this.writeEmitter.fire(`${data}\r\n`);
}
});

View File

@@ -17,6 +17,7 @@ function normalizeLineEndings(str: string) {
const defaultOptions: FormatterOptions = {
maxEmptyLines: 2,
denseFunctionParameters: false,
spacesBeforeEndOfLineComment: 1,
};
function get_options(folder: fs.Dirent) {

View File

@@ -1,3 +1,6 @@
# --- IN ---
var c = 0
func f():
const a = preload("res://a.gd")
const b = load("res://b.gd")
@@ -8,3 +11,7 @@ func f():
andigin.x = 1
print(a)
self.c = 1
print(self.c + 2)
print(func() return self.c + 2)

View File

@@ -1,10 +0,0 @@
func f():
const a = preload("res://a.gd")
const b = load("res://b.gd")
var origin: Vector2 = Vector2.ZERO
origin.x = 1
var andigin: Vector2 = Vector2.ZERO
andigin.x = 1
print(a)

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

@@ -0,0 +1,47 @@
# --- IN ---
pass # Comment 1.
pass ## Comment 2.
# --- IN ---
pass # Comment 3.
pass ## Comment 4.
# --- OUT ---
pass # Comment 3.
pass ## Comment 4.
# --- CONFIG ALL ---
{"spacesBeforeEndOfLineComment": 1}
# --- IN ---
pass # Comment 5.
pass ## Comment 6.
# --- IN ---
pass # Comment 7.
pass ## Comment 8.
# --- OUT ---
pass # Comment 7.
pass ## Comment 8.
# --- CONFIG ALL ---
{"spacesBeforeEndOfLineComment": 2}
# --- IN ---
pass # Comment 9.
pass ## Comment A.
# --- OUT ---
pass # Comment 9.
pass ## Comment A.
# --- IN ---
pass # Comment B.
pass ## Comment C.
# --- IN ---
pass # Comment D.
pass ## Comment E.
# --- OUT ---
pass # Comment D.
pass ## Comment E.

View File

@@ -56,12 +56,14 @@ interface Token {
export interface FormatterOptions {
maxEmptyLines: 0 | 1 | 2;
denseFunctionParameters: boolean;
spacesBeforeEndOfLineComment: 1 | 2;
}
function get_formatter_options() {
const options: FormatterOptions = {
maxEmptyLines: get_configuration("formatter.maxEmptyLines") === "1" ? 1 : 2,
denseFunctionParameters: get_configuration("formatter.denseFunctionParameters"),
spacesBeforeEndOfLineComment: get_configuration("formatter.spacesBeforeEndOfLineComment") === "1" ? 1 : 2,
};
return options;
@@ -135,8 +137,8 @@ function between(tokens: Token[], current: number, options: FormatterOptions) {
if (!prev) return "";
if (next === "##") return " ";
if (next === "#") return " ";
if (next === "##") return options.spacesBeforeEndOfLineComment === 2 ? " " : " ";
if (next === "#") return options.spacesBeforeEndOfLineComment === 2 ? " " : " ";
if (prevToken.skip && nextToken.skip) return "";
if (prev === "(") return "";
@@ -186,15 +188,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

@@ -14,10 +14,11 @@ import {
import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
import { killSubProcesses, subProcess } from "../utils/subspawn";
import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient";
import { EventEmitter } from "vscode";
const log = createLogger("lsp.manager", { output: "Godot LSP" });
enum ManagerStatus {
export enum ManagerStatus {
INITIALIZING = 0,
INITIALIZING_LSP = 1,
PENDING = 2,
@@ -25,11 +26,15 @@ enum ManagerStatus {
DISCONNECTED = 4,
CONNECTED = 5,
RETRYING = 6,
WRONG_WORKSPACE = 7,
}
export class ClientConnectionManager {
public client: GDScriptLanguageClient = null;
private statusChanged = new EventEmitter<ManagerStatus>();
onStatusChanged = this.statusChanged.event;
private reconnectionAttempts = 0;
private target: TargetLSP = TargetLSP.EDITOR;
@@ -39,8 +44,7 @@ export class ClientConnectionManager {
private connectedVersion = "";
constructor(private context: vscode.ExtensionContext) {
this.client = new GDScriptLanguageClient();
this.client.events.on("status", this.on_client_status_changed.bind(this));
this.create_new_client();
setInterval(() => {
this.retry_callback();
@@ -69,6 +73,14 @@ export class ClientConnectionManager {
this.connect_to_language_server();
}
private create_new_client() {
const port = this.client?.port ?? -1;
this.client?.events?.removeAllListeners();
this.client = new GDScriptLanguageClient();
this.client.port = port;
this.client.events.on("status", this.on_client_status_changed.bind(this));
}
private async connect_to_language_server() {
this.client.port = -1;
this.target = TargetLSP.EDITOR;
@@ -211,6 +223,9 @@ export class ClientConnectionManager {
case ManagerStatus.RETRYING:
this.show_retrying_prompt();
break;
case ManagerStatus.WRONG_WORKSPACE:
this.retry_connect_client();
break;
}
}
@@ -239,7 +254,7 @@ export class ClientConnectionManager {
text = "$(check) Connected";
tooltip = `Connected to the GDScript language server.\n${lspTarget}`;
if (this.connectedVersion) {
tooltip += `\n${this.connectedVersion}`;
tooltip += `\nGodot version: ${this.connectedVersion}`;
}
break;
case ManagerStatus.DISCONNECTED:
@@ -253,6 +268,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,11 +288,13 @@ 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:
set_context("connectedToLSP", false);
// Disconnection is unrecoverable, since the server will not know that the reconnected client is the same.
// Create a new client with a clean state to prevent de-sync e.g. of client managed files.
this.create_new_client();
if (this.retry) {
if (this.client.port !== -1) {
this.status = ManagerStatus.INITIALIZING_LSP;
@@ -285,9 +306,14 @@ export class ClientConnectionManager {
}
this.retry = true;
break;
case ClientStatus.REJECTED:
this.status = ManagerStatus.WRONG_WORKSPACE;
this.retry = false;
break;
default:
break;
}
this.statusChanged.fire(this.status);
this.update_status_widget();
}

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,33 @@ type HoverResponseMesssage = {
result: HoverResult;
};
type ChangeWorkspaceNotification = {
method: string;
params: {
path: string;
};
};
type DocumentLinkResult = {
range: {
end: {
character: number;
line: number;
};
start: {
character: number;
line: number;
};
};
target: string;
};
type DocumentLinkResponseMessage = {
id: number;
jsonrpc: string;
result: DocumentLinkResult[];
};
export default class GDScriptLanguageClient extends LanguageClient {
public io: MessageIO = new MessageIO();
@@ -63,6 +93,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
public port = -1;
public lastPortTried = -1;
public sentMessages = new Map();
private rejected = false;
events = new EventEmitter();
@@ -85,9 +116,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 +128,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
connect(target: TargetLSP = TargetLSP.EDITOR) {
this.rejected = false;
this.target = target;
this.status = ClientStatus.PENDING;
@@ -122,15 +151,68 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.io.connect(host, port);
}
async send_request<R>(method: string, params): Promise<R> {
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);
// 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;
// Fixed on server side since Godot 4.5
return false;
}
return message;
@@ -160,14 +242,50 @@ export default class GDScriptLanguageClient extends LanguageClient {
(message as HoverResponseMesssage).result.contents.value = value;
}
} else if (sentMessage.method === "textDocument/documentLink") {
const results: DocumentLinkResult[] = (
message as DocumentLinkResponseMessage
).result;
if (!results) {
return message;
}
const final_result: DocumentLinkResult[] = [];
// at this point, Godot's LSP server does not
// return a valid path for resources identified
// by "uid://""
//
// this is a dirty hack to remove any "uid://"
// document links.
//
// to provide links for these, we will be relying on
// the internal DocumentLinkProvider instead.
for (const result of results) {
if (!result.target.startsWith("uid://")) {
final_result.push(result);
}
}
(message as DocumentLinkResponseMessage).result = final_result;
}
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);
@@ -189,14 +307,16 @@ export default class GDScriptLanguageClient extends LanguageClient {
return message;
}
public async get_symbol_at_position(uri: vscode.Uri, position: vscode.Position) {
public async get_symbol_at_position(
uri: vscode.Uri,
position: vscode.Position
) {
const params = {
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) {
@@ -236,6 +356,10 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
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

@@ -1 +1 @@
export { ClientConnectionManager } from "./ClientConnectionManager";
export { ClientConnectionManager, ManagerStatus } from "./ClientConnectionManager";

View File

@@ -8,6 +8,7 @@ import {
Definition,
DefinitionProvider,
ExtensionContext,
TextLine,
} from "vscode";
import { make_docs_uri, createLogger } from "../utils";
import { globals } from "../extension";
@@ -23,7 +24,7 @@ export class GDDefinitionProvider implements DefinitionProvider {
];
context.subscriptions.push(
vscode.languages.registerDefinitionProvider(selector, this),
vscode.languages.registerDefinitionProvider(selector, this), //
);
}
@@ -37,8 +38,8 @@ export class GDDefinitionProvider implements DefinitionProvider {
return new Location(uri, new Position(0, 0));
} else {
let i = 0;
let line;
let match;
let line: TextLine;
let match: RegExpMatchArray | null;
do {
line = document.lineAt(position.line - i++);

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

@@ -9,7 +9,7 @@ import {
type ExtensionContext,
} from "vscode";
import { SceneParser } from "../scene_tools";
import { convert_resource_path_to_uri, createLogger } from "../utils";
import { convert_resource_path_to_uri, convert_uids_to_uris, createLogger } from "../utils";
const log = createLogger("providers.document_links");
@@ -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);
@@ -70,6 +70,22 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
}
}
const uids: Set<string> = new Set();
const uid_matches: Array<[string, Range]> = [];
for (const match of text.matchAll(/uid:\/\/([0-9a-z]*)/g)) {
const r = this.create_range(document, match);
uids.add(match[0]);
uid_matches.push([match[0], r]);
}
const uid_map = await convert_uids_to_uris(Array.from(uids));
for (const uid of uid_matches) {
const uri = uid_map.get(uid[0]);
if (uri instanceof vscode.Uri) {
links.push(new DocumentLink(uid[1], uri));
}
}
return links;
}

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

@@ -322,14 +322,14 @@ function make_link(classname: string, symbol: string) {
function make_codeblock(code: string, language: string) {
const lines = code.split("\n");
const indent = lines[0].match(/^\s*/)[0].length;
code = lines.map((line) => line.slice(indent)).join("\n");
return marked.parse(`\`\`\`${language}\n${code}\n\`\`\``);
const _code = lines.map((line) => line.slice(indent)).join("\n");
return marked.parse(`\`\`\`${language}\n${_code}\n\`\`\``);
}
function format_documentation(bbcode: string, classname: string) {
// ya-bbcode doesn't parse [code skip-lint] as a [code] tag
bbcode = bbcode.replaceAll("[code skip-lint]", "[code]");
let html = parser.parse(bbcode.trim());
const _bbcode = bbcode.replaceAll("[code skip-lint]", "[code]");
let html = parser.parse(_bbcode.trim());
html = html.replaceAll(/\[\/?codeblocks\](<br\/>)?/g, "");
html = html.replaceAll("&quot;", '"');

View File

@@ -10,7 +10,7 @@ import {
Hover,
} from "vscode";
import { SceneParser } from "../scene_tools";
import { convert_resource_path_to_uri, createLogger } from "../utils";
import { convert_resource_path_to_uri, createLogger, convert_uid_to_uri, convert_uri_to_resource_path } from "../utils";
const log = createLogger("providers.hover");
@@ -36,6 +36,12 @@ export class GDHoverProvider implements HoverProvider {
links += `* [${match[0]}](${uri})\n`;
}
}
for (const match of text.matchAll(/uid:\/\/[0-9a-z]*/g)) {
const uri = await convert_uid_to_uri(match[0]);
if (uri instanceof Uri) {
links += `* [${match[0]}](${uri})\n`;
}
}
return links;
}
@@ -49,8 +55,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 +83,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(...)");
@@ -88,7 +94,15 @@ export class GDHoverProvider implements HoverProvider {
}
}
const link = document.getText(document.getWordRangeAtPosition(position, /res:\/\/[^"^']*/));
let link = document.getText(document.getWordRangeAtPosition(position, /res:\/\/[^"^']*/));
if (!link.startsWith("res://")) {
link = document.getText(document.getWordRangeAtPosition(position, /uid:\/\/[0-9a-z]*/));
if (link.startsWith("uid://")) {
const uri = await convert_uid_to_uri(link);
link = await convert_uri_to_resource_path(uri);
}
}
if (link.startsWith("res://")) {
let type = "";
if (link.endsWith(".gd")) {

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

@@ -1,17 +1,22 @@
import * as vscode from "vscode";
import {
Range,
TextDocument,
CancellationToken,
DocumentSymbol,
Event,
EventEmitter,
ExtensionContext,
InlayHint,
ProviderResult,
InlayHintKind,
InlayHintsProvider,
ExtensionContext,
Position,
Range,
TextDocument,
TextEdit,
} from "vscode";
import { globals } from "../extension";
import { ManagerStatus } from "../lsp";
import { SceneParser } from "../scene_tools";
import { createLogger, get_configuration } from "../utils";
import { globals } from "../extension";
const log = createLogger("providers.inlay_hints");
@@ -20,67 +25,104 @@ const log = createLogger("providers.inlay_hints");
* E.g. `var a: int` gets parsed to ` int `.
*/
function fromDetail(detail: string): string {
const labelRegex = /: ([\w\d_]+)/;
const labelRegex = /: ([\w\d_.]+)/;
const labelMatch = detail.match(labelRegex);
const label = labelMatch ? labelMatch[1] : "unknown";
return ` ${label} `;
let label = labelMatch ? labelMatch[1] : "unknown";
// fix when detail includes a script name
if (label.includes(".gd.")) {
label = label.split(".gd.")[1];
}
return `${label}`;
}
async function addByHover(document: TextDocument, hoverPosition: vscode.Position, start: vscode.Position): Promise<InlayHint | undefined> {
const response = await globals.lsp.client.sendRequest("textDocument/hover", {
type HoverResult = {
contents: {
kind: string;
value: string;
};
};
async function addByHover(document: TextDocument, hoverPosition: vscode.Position): Promise<string | undefined> {
const response = (await globals.lsp.client.send_request("textDocument/hover", {
textDocument: { uri: document.uri.toString() },
position: {
line: hoverPosition.line,
character: hoverPosition.character,
}
});
},
})) as HoverResult;
// check if contents is an empty array; if it is, we have no hover information
if (Array.isArray(response["contents"]) && response["contents"].length === 0) {
if (Array.isArray(response.contents) && response.contents.length === 0) {
return undefined;
}
return new InlayHint(start, fromDetail(response["contents"].value), InlayHintKind.Type);
return response.contents.value;
}
export class GDInlayHintsProvider implements InlayHintsProvider {
public parser = new SceneParser();
private _onDidChangeInlayHints = new EventEmitter<void>();
get onDidChangeInlayHints(): Event<void> {
return this._onDidChangeInlayHints.event;
}
constructor(private context: ExtensionContext) {
const selector = [
{ language: "gdresource", scheme: "file" },
{ language: "gdscene", scheme: "file" },
{ language: "gdscript", scheme: "file" },
];
context.subscriptions.push(
vscode.languages.registerInlayHintsProvider(selector, this),
);
context.subscriptions.push(vscode.languages.registerInlayHintsProvider(selector, this));
globals.lsp.onStatusChanged((status) => {
this._onDidChangeInlayHints.fire();
if (status === ManagerStatus.CONNECTED) {
setTimeout(() => {
this._onDidChangeInlayHints.fire();
}, 250);
}
});
}
buildHint(start: Position, detail: string): InlayHint {
const label = fromDetail(detail);
const hint = new InlayHint(start, label, InlayHintKind.Type);
hint.paddingLeft = true;
hint.paddingRight = true;
// hint.tooltip = "tooltip";
hint.textEdits = [TextEdit.insert(start, ` ${label} `)];
return hint;
}
async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise<InlayHint[]> {
const hints: InlayHint[] = [];
const text = document.getText(range);
log.debug("Inlay Hints: provideInlayHints");
if (document.fileName.endsWith(".gd")) {
if (!get_configuration("inlayHints.gdscript", true)) {
return hints;
}
await globals.lsp.client.onReady();
if (!globals.lsp.client.isRunning()) {
return hints;
}
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[];
})) as DocumentSymbol[];
if (symbolsRequest.length === 0) {
return hints;
}
const symbols = (typeof symbolsRequest[0] === "object" && "children" in symbolsRequest[0])
? (symbolsRequest[0].children as unknown[]) // godot 4.0+ returns an array of children
: symbolsRequest; // godot 3.2 and below returns an array of symbols
const symbols =
typeof symbolsRequest[0] === "object" && "children" in symbolsRequest[0]
? (symbolsRequest[0].children as DocumentSymbol[]) // godot 4.0+ returns an array of children
: symbolsRequest; // godot 3.2 and below returns an array of symbols
const hasDetail = symbols.some((s: any) => s.detail);
const hasDetail = symbols.some((s) => s.detail);
// TODO: make sure godot reports the correct location for variable declaration symbols
// (allowing the use of regex only on ranges provided by the LSP (textDocument/documentSymbol))
@@ -88,31 +130,30 @@ export class GDInlayHintsProvider implements InlayHintsProvider {
// since neither LSP or the grammar know whether a variable is inferred or not,
// we still need to use regex to find all inferred variable declarations.
const regex = /((var|const)\s+)([\w\d_]+)\s*:=/g;
for (const match of text.matchAll(regex)) {
if (token.isCancellationRequested) break;
if (token.isCancellationRequested) {
break;
}
// TODO: until godot supports nested document symbols, we need to send
// a hover request for each variable declaration that is nested
const start = document.positionAt(match.index + match[0].length - 1);
const hoverPosition = document.positionAt(match.index + match[1].length);
if (hasDetail) {
const symbol = symbols.find((s: any) => s.name === match[3]);
if (symbol && symbol["detail"]) {
const hint = new InlayHint(start, fromDetail(symbol["detail"]), InlayHintKind.Type);
hints.push(hint);
} else {
const hint = await addByHover(document, hoverPosition, start);
if (hint) {
hints.push(hint);
}
}
} else {
const hint = await addByHover(document, hoverPosition, start);
if (hint) {
const symbol = symbols.find((s) => s.name === match[3]);
if (symbol?.detail) {
const hint = this.buildHint(start, symbol.detail);
hints.push(hint);
continue;
}
}
const hoverPosition = document.positionAt(match.index + match[1].length);
const detail = await addByHover(document, hoverPosition);
if (detail) {
const hint = this.buildHint(start, detail);
hints.push(hint);
}
}
return hints;
}
@@ -126,7 +167,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 +179,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>;
@@ -48,20 +43,14 @@ export class ScenePreviewProvider
scriptDecorator = new ScriptDecorationProvider(this);
private changeTreeEvent = new EventEmitter<void>();
public get onDidChangeTreeData(): Event<void> {
return this.changeTreeEvent.event;
}
onDidChangeTreeData = this.changeTreeEvent.event;
constructor(private context: ExtensionContext) {
this.tree = vscode.window.createTreeView("scenePreview", {
this.tree = vscode.window.createTreeView("godotTools.scenePreview", {
treeDataProvider: this,
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 +59,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 +88,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 +111,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 +147,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 +198,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 +217,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 });
@@ -302,17 +263,19 @@ export class ScenePreviewProvider
element.collapsibleState = TreeItemCollapsibleState.None;
}
this.uniqueDecorator.changeDecorationsEvent.fire(element.resourceUri);
this.scriptDecorator.changeDecorationsEvent.fire(element.resourceUri);
this.uniqueDecorator.update(element.resourceUri);
this.scriptDecorator.update(element.resourceUri);
return element;
}
}
class UniqueDecorationProvider implements vscode.FileDecorationProvider {
public changeDecorationsEvent = new EventEmitter<Uri>();
get onDidChangeFileDecorations(): Event<Uri> {
return this.changeDecorationsEvent.event;
public emitter = new EventEmitter<Uri>();
onDidChangeFileDecorations = this.emitter.event;
update(uri: Uri) {
this.emitter.fire(uri);
}
constructor(private previewer: ScenePreviewProvider) {}
@@ -330,9 +293,11 @@ class UniqueDecorationProvider implements vscode.FileDecorationProvider {
}
class ScriptDecorationProvider implements vscode.FileDecorationProvider {
public changeDecorationsEvent = new EventEmitter<Uri>();
get onDidChangeFileDecorations(): Event<Uri> {
return this.changeDecorationsEvent.event;
public emitter = new EventEmitter<Uri>();
onDidChangeFileDecorations = this.emitter.event;
update(uri: Uri) {
this.emitter.fire(uri);
}
constructor(private previewer: ScenePreviewProvider) {}

View File

@@ -4,7 +4,7 @@ import {
MarkdownString,
Uri
} from "vscode";
import * as path from "path";
import * as path from "node:path";
import { get_extension_uri } from "../utils";
const iconDir = get_extension_uri("resources", "godot_icons").fsPath;
@@ -17,9 +17,9 @@ export class SceneNode extends TreeItem {
public text: string;
public position: number;
public body: string;
public unique: boolean = false;
public hasScript: boolean = false;
public scriptId: string = "";
public unique = false;
public hasScript = false;
public scriptId = "";
public children: SceneNode[] = [];
constructor(
@@ -29,7 +29,7 @@ export class SceneNode extends TreeItem {
) {
super(label, collapsibleState);
const iconName = className + ".svg";
const iconName = `${className}.svg`;
this.iconPath = {
light: Uri.file(path.join(iconDir, "light", iconName)),
@@ -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,10 +4,25 @@ 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;
export async function get_project_dir(): Promise<string | undefined> {
if (projectDir && projectFile) {
return projectDir;
}
let file = "";
if (vscode.workspace.workspaceFolders !== undefined) {
const files = await vscode.workspace.findFiles("**/project.godot", null);
@@ -33,10 +48,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;
}
@@ -50,6 +65,10 @@ export async function get_project_file(): Promise<string | undefined> {
let projectVersion: string | undefined = undefined;
export async function get_project_version(): Promise<string | undefined> {
if (projectVersion) {
return projectVersion;
}
if (projectDir === undefined || projectFile === undefined) {
await get_project_dir();
}
@@ -105,6 +124,80 @@ 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}`;
}
const uidCache: Map<string, vscode.Uri | null> = new Map();
export async function convert_uids_to_uris(uids: string[]): Promise<Map<string, vscode.Uri>> {
const not_found_uids: string[] = [];
const uris: Map<string, vscode.Uri> = new Map();
let found_all = true;
for (const uid of uids) {
if (!uid.startsWith("uid://")) {
continue;
}
if (uidCache.has(uid)) {
const uri = uidCache.get(uid);
if (fs.existsSync(uri.fsPath)) {
uris.set(uid, uri);
continue;
}
uidCache.delete(uid);
}
found_all = false;
not_found_uids.push(uid);
}
if (found_all) {
return uris;
}
const files = await vscode.workspace.findFiles("**/*.uid", null);
for (const file of files) {
const document = await vscode.workspace.openTextDocument(file);
const text = document.getText();
const match = text.match(/uid:\/\/([0-9a-z]*)/);
if (!match) {
continue;
}
const found_match = not_found_uids.indexOf(match[0]) >= 0;
const file_path = file.fsPath.substring(0, file.fsPath.length - ".uid".length);
if (!fs.existsSync(file_path)) {
continue;
}
const file_uri = vscode.Uri.file(file_path);
uidCache.set(match[0], file_uri);
if (found_match) {
uris.set(match[0], file_uri);
}
}
return uris;
}
export async function convert_uid_to_uri(uid: string): Promise<vscode.Uri | undefined> {
const uris = await convert_uids_to_uris([uid]);
return uris.get(uid);
}
export type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
export type VERIFY_RESULT = {
status: VERIFY_STATUS;
@@ -143,8 +236,22 @@ export function verify_godot_version(godotPath: string, expectedVersion: "3" | "
}
export function clean_godot_path(godotPath: string): string {
let target = godotPath.replace(/^"/, "").replace(/"$/, "");
let pathToClean = godotPath;
// check for environment variable syntax
// looking for: ${env:FOOBAR}
// extracts "FOOBAR"
const pattern = /\$\{env:(.+?)\}/;
const match = godotPath.match(pattern);
if (match && match.length >= 2) {
pathToClean = process.env[match[1]];
}
// strip leading and trailing quotes
let target = pathToClean.replace(/^"/, "").replace(/"$/, "");
// try to fix macos paths
if (os.platform() === "darwin" && target.endsWith(".app")) {
target = path.join(target, "Contents", "MacOS", "Godot");
}

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

@@ -2,16 +2,16 @@ import { LogOutputChannel, window } from "vscode";
import { is_debug_mode } from ".";
export enum LOG_LEVEL {
SILENT,
ERROR,
WARNING,
INFO,
DEBUG,
TRACE,
SILENT = 0,
ERROR = 1,
WARNING = 2,
INFO = 3,
DEBUG = 4,
TRACE = 5,
}
const LOG_LEVEL_NAMES = [
"SILENT",
"SILENT", //
"ERROR",
"WARN ",
"INFO ",
@@ -31,16 +31,16 @@ const LOG_COLORS = [
];
export interface LoggerOptions {
level?: LOG_LEVEL
level?: LOG_LEVEL;
time?: boolean;
output?: string;
}
export class Logger {
private level: LOG_LEVEL = LOG_LEVEL.DEBUG;
private show_tag: boolean = true;
private show_time: boolean;
private show_level: boolean = false;
private show_tag = true;
private show_time = false;
private show_level = false;
private output?: LogOutputChannel;
constructor(
@@ -61,10 +61,10 @@ export class Logger {
prefix += `[${new Date().toISOString()}]`;
}
if (this.show_level) {
prefix += "[" + LOG_COLORS[level] + LOG_LEVEL_NAMES[level] + RESET + "]";
prefix += `[${LOG_COLORS[level]}${LOG_LEVEL_NAMES[level]}${RESET}]`;
}
if (this.show_tag) {
prefix += "[" + LOG_COLORS[level] + this.tag + RESET + "]";
prefix += `[${LOG_COLORS[level]}${this.tag}${RESET}]`;
}
console.log(prefix, ...messages);

View File

@@ -4,7 +4,7 @@ import { set_configuration } from ".";
export function prompt_for_reload() {
const message = "Reload VSCode to apply settings";
vscode.window.showErrorMessage(message, "Reload").then(item => {
if (item == "Reload") {
if (item === "Reload") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
@@ -26,10 +26,10 @@ export function select_godot_executable(settingName: string) {
export function prompt_for_godot_executable(message: string, settingName: string) {
vscode.window.showErrorMessage(message, "Select Godot executable", "Open Settings", "Ignore").then(item => {
if (item == "Select Godot executable") {
if (item === "Select Godot executable") {
select_godot_executable(settingName);
}
if (item == "Open Settings") {
if (item === "Open Settings") {
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
}
});

View File

@@ -5,7 +5,7 @@ Original library copyright (c) 2022 Craig Wardman
I had to vendor this library to fix the API in a couple places.
*/
import { ChildProcess, execSync, spawn, SpawnOptions } from "child_process";
import { ChildProcess, execSync, spawn, SpawnOptions } from "node:child_process";
import { createLogger } from ".";
const log = createLogger("subspawn");
@@ -20,7 +20,7 @@ export function killSubProcesses(owner: string) {
return;
}
children[owner].forEach((c) => {
for (const c of children[owner]) {
try {
if (c.pid) {
if (process.platform === "win32") {
@@ -34,13 +34,17 @@ export function killSubProcesses(owner: string) {
} catch {
log.error(`couldn't kill task ${owner}`);
}
});
}
children[owner] = [];
}
process.on("exit", () => {
Object.keys(children).forEach((owner) => killSubProcesses(owner));
for (const owner of Object.keys(children)) {
killSubProcesses(owner);
}
// Object.keys(children).forEach((owner) => killSubProcesses(owner));
});
function gracefulExitHandler() {

View File

@@ -22,7 +22,7 @@ export function set_context(name: string, value: any) {
}
export function register_command(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
return vscode.commands.registerCommand(`${EXTENSION_PREFIX}.${command}`, callback);
return vscode.commands.registerCommand(`${EXTENSION_PREFIX}.${command}`, callback, thisArg);
}
export function get_extension_uri(...paths: string[]) {

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,8 +74,8 @@
{ "include": "#square_braces" },
{ "include": "#round_braces" },
{ "include": "#function_call" },
{ "include": "#region"},
{ "include": "#comment" },
{ "include": "#self" },
{ "include": "#func" },
{ "include": "#letter" },
{ "include": "#numbers" },
@@ -83,6 +83,10 @@
{ "include": "#line_continuation" }
]
},
"region": {
"match": "#(end)?region.*$\\n?",
"name": "keyword.language.region.gdscript"
},
"comment": {
"match": "(##|#).*$\\n?",
"name": "comment.line.number-sign.gdscript",
@@ -162,24 +166,20 @@
{
"begin": "(\"|')",
"end": "\\1",
"name": "string.quoted.gdscript meta.literal.nodepath.gdscript constant.character.escape",
"name": "string.quoted.gdscript meta.literal.nodepath.gdscript constant.character.escape.gdscript",
"patterns": [
{
"match": "%",
"name": "keyword.control.flow"
"name": "keyword.control.flow.gdscript"
}
]
},
{ "include": "#base_expression" }
{ "include": "#expression" }
]
},
"self": {
"match": "\\bself\\b",
"name": "variable.language.gdscript"
},
"func": {
"match": "\\bfunc\\b",
"name": "keyword.language.gdscript"
"name": "keyword.language.gdscript storage.type.function.gdscript"
},
"in_keyword": {
"patterns": [
@@ -229,7 +229,7 @@
"name": "keyword.operator.comparison.gdscript"
},
"arithmetic_operator": {
"match": "->|\\+=|-=|\\*=|\\^=|/=|%=|&=|~=|\\|=|\\*\\*|\\*|/|%|\\+|-",
"match": "->|\\+=|-=|\\*\\*=|\\*=|\\^=|/=|%=|&=|~=|\\|=|\\*\\*|\\*|/|%|\\+|-",
"name": "keyword.operator.arithmetic.gdscript"
},
"assignment_operator": {
@@ -245,7 +245,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|is|onready|tool|static|export|as|enum|assert|breakpoint|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace|super|self)\\b",
"name": "keyword.language.gdscript"
},
"letter": {
@@ -267,7 +267,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 +297,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 +315,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 +330,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" }
}
}
]
},
@@ -430,14 +439,14 @@
]
},
"annotations": {
"match": "(@)(export|export_group|export_color_no_alpha|export_custom|export_dir|export_enum|export_exp_easing|export_file|export_flags|export_flags_2d_navigation|export_flags_2d_physics|export_flags_2d_render|export_flags_3d_navigation|export_flags_3d_physics|export_flags_3d_render|export_global_dir|export_global_file|export_multiline|export_node_path|export_placeholder|export_range|export_storage|icon|onready|rpc|tool|warning_ignore|static_unload)\\b",
"match": "(@)(abstract|export|export_category|export_color_no_alpha|export_custom|export_dir|export_enum|export_exp_easing|export_file|export_file_path|export_flags|export_flags_2d_navigation|export_flags_2d_physics|export_flags_2d_render|export_flags_3d_navigation|export_flags_3d_physics|export_flags_3d_render|export_flags_avoidance|export_global_dir|export_global_file|export_group|export_multiline|export_node_path|export_placeholder|export_range|export_storage|export_subgroup|export_tool_button|icon|onready|rpc|static_unload|tool|warning_ignore|warning_ignore_restore|warning_ignore_start)\\b",
"captures": {
"1": { "name": "entity.name.function.decorator.gdscript" },
"2": { "name": "entity.name.function.decorator.gdscript" }
}
},
"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|Variant|void)\\b",
"name": "entity.name.type.class.builtin.gdscript"
},
"const_vars": {
@@ -445,7 +454,7 @@
"name": "variable.other.constant.gdscript"
},
"pascal_case_class": {
"match": "\\b([A-Z]+[a-z_0-9]*([A-Z]?[a-z_0-9]+)*[A-Z]?)\\b",
"match": "\\b[A-Z]+(?:[a-z]+[A-Za-z0-9_]*)+\\b",
"name": "entity.name.type.class.gdscript"
},
"signal_declaration_bare": {
@@ -480,7 +489,7 @@
"end2": "(\\s*(\\-\\>)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:)",
"endCaptures2": {
"1": { "name": "punctuation.separator.annotation.result.gdscript" },
"2": { "name": "keyword.language.void.gdscript" },
"2": { "name": "entity.name.type.class.builtin.gdscript" },
"3": { "name": "entity.name.type.class.gdscript markup.italic" }
},
"patterns": [
@@ -530,7 +539,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 +551,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