Compare commits

...

69 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
Hugo Locurcio
7844979c90 Bump to version 2.4.0 2025-02-22 00:07:17 +01:00
Saint
9297920d73 Add setting to enable/disable documentation minimap (#786) 2025-02-13 11:47:23 -05:00
btarg
8059ba89c2 Add some useful GDScript snippets for Godot 4 (#794) 2025-02-13 10:57:29 -05:00
MichaelXt
2490d0cdad Implement Godot-in-the-loop test suite and fix debugger errors (#788)
Fixes for get variables issues
1. Same reference but different variable override fix, which resulted in variables being lost
** Now different GodotVariable instances are used for different variables with same reference
** const replacement = {value: rawObject, sub_values: sub_values } as GodotVariable;
2. 'Signal' type handling crash and string representation fix
** value.sub_values()?.map
3. Empty scopes return fix
** if (this.ongoing_inspections.length === 0  && stackVars.remaining == 0)
4. Various splice from findIndex fixes (where findIndex can return -1)
5. Added variables tests
**  updated vscode types to version 1.96 to use `onDidChangeActiveStackItem` for breakpoint hit detection in tests
1 & 3 should fix https://github.com/godotengine/godot-vscode-plugin/issues/779
2025-02-10 16:56:13 -05:00
dependabot[bot]
7c70ac2753 Bump actions/setup-node from 4.1.0 to 4.2.0 (#785)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.1.0...v4.2.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-01-27 12:40:23 -05:00
MichaelXt
e7b9530a7f Fix debugger watch window freeze caused by missing responses (#781) 2025-01-26 13:33:50 -05:00
dependabot[bot]
002cfa18a3 Bump actions/upload-artifact from 4.5.0 to 4.6.0 (#774)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  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-01-10 11:03:54 -05:00
David Kincaid
489db36e85 Add debugger support for Typed Dictionaries (#764)
Add decoding support for Typed Dictionaries
2025-01-03 21:40:31 -05:00
KIM JISU
996a7aefe6 Add newline when dropping nodes into editor (#754)
Co-authored-by: k-expon <kimexpon@pm.me>
2025-01-03 21:02:58 -05:00
dependabot[bot]
2e9117870d Bump actions/upload-artifact from 4.4.3 to 4.5.0 (#766)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.3 to 4.5.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.4.3...v4.5.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  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>
2024-12-18 11:38:21 -05:00
David Kincaid
aee83dd2a4 Update float syntax rules and formatting (#756)
* Update float syntax rules and formatting

* Add missing export_group annotation

* Moved abstract from an annotation to a keyword

* Add an example of the other type of string interpolation.
2024-12-18 10:45:05 -05:00
David Kincaid
6ddf05d4a1 Fix VBox and HBox docs not opening (#755) 2024-11-18 11:25:30 -05:00
David Kincaid
f648c37353 Various Formatter Improvements (#746)
* add new style of formatter snapshot tests
* add many new test cases
* fix several open issues( #728, #624, #657, #717, #734, likely more)
2024-11-18 11:16:16 -05:00
David Kincaid
709fa1bbad Implement warnings and errors in Debug Console (#749) 2024-11-18 11:11:30 -05:00
David Kincaid
694feea1bc Overhaul LSP Client (#752)
* Simplify LSP Client internals
* Streamline control flow between Client, IO, and Buffer classes
* Create canonical, obvious place to implement filters on incoming and outgoing LSP messages
* Remove legacy WS LSP support
2024-11-18 10:53:59 -05:00
dependabot[bot]
fd637d0641 Bump actions/setup-node from 4.0.4 to 4.1.0 (#741)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.4 to 4.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.4...v4.1.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>
2024-10-24 12:31:37 -04:00
Hugo Locurcio
c33982d38e Remove OS, GDScript and Object from the list of builtins in syntax highlighting (#739) 2024-10-21 09:03:45 -04:00
ProgramGamer
43bb36ca30 Fixed the textmate grammar erroneously tagging enum members and const variables as language constants (#737) 2024-10-19 14:40:23 -04:00
Hugo Locurcio
96510971f4 Add @static_unload annotation and Godot 4.3 Variant types to syntax highlighting (#738) 2024-10-19 14:39:34 -04:00
Mikael Hermansson
0a632d62b5 Fix typed arrays of scripts not being decoded properly (#731) 2024-10-11 11:03:34 -04:00
dependabot[bot]
4404b76006 Bump actions/upload-artifact from 4.4.2 to 4.4.3 (#733)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.2 to 4.4.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.4.2...v4.4.3)

---
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>
2024-10-11 16:38:38 +02:00
dependabot[bot]
1c32bbb1cb Bump actions/upload-artifact from 4.4.0 to 4.4.2 (#732)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.0 to 4.4.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.4.0...v4.4.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>
2024-10-09 11:24:47 -04:00
Hugo Locurcio
86ae182088 Bump to version 2.3.0 2024-10-07 17:52:11 +02:00
David Kincaid
658270e742 Capitalize the drive letter in windows absolute paths (#727) 2024-09-25 17:19:59 -04:00
David Kincaid
170d3d4819 Suppress "workspace/symbol" not found error (#723)
* Discard outgoing "workspace/symbol" LSP messages
2024-09-23 13:56:45 -04:00
David Kincaid
1a84a57647 Add documentation page scaling feature (#722)
* Add documentation.pageScale mechanism

* Fix issue with div of map() function catching the minimap style
2024-09-23 13:56:25 -04:00
Hugo Locurcio
9b16946ba9 Update feature request issue template placeholders to match the bug report template 2024-09-23 19:07:41 +02:00
112 changed files with 6032 additions and 2971 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.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.2.0"
placeholder: "2.5.1"
validations:
required: true

View File

@@ -18,7 +18,7 @@ body:
description: >
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: 4.2.2.stable, 4.3.rc (88d932506)
placeholder: 4.3.stable, 4.4.dev1 (28a72fa43)
validations:
required: true
@@ -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.91.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.1.0"
placeholder: "2.5.1"
validations:
required: true

View File

@@ -5,21 +5,48 @@ jobs:
test:
name: Test
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4.0.4
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'
run: |
wget https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_linux.x86_64.zip
unzip Godot_v4.3-stable_linux.x86_64.zip
sudo mv Godot_v4.3-stable_linux.x86_64 /usr/local/bin/godot
chmod +x /usr/local/bin/godot
- name: Install Godot (macOS)
if: matrix.os == 'macos-latest'
run: |
curl -L -o Godot.zip https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_macos.universal.zip
unzip Godot.zip
sudo mv Godot.app /Applications/Godot.app
sudo ln -s /Applications/Godot.app/Contents/MacOS/Godot /usr/local/bin/godot
- name: Install Godot (Windows)
if: matrix.os == 'windows-latest'
run: |
Invoke-WebRequest -Uri "https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_win64.exe.zip" -OutFile "Godot.zip"
Expand-Archive -Path "Godot.zip" -DestinationPath "C:\Godot43"
"C:\Godot43\Godot_v4.3-stable_win64.exe %*" | Out-File -Encoding ascii -FilePath ([Environment]::SystemDirectory+"\godot.cmd")
- name: Install dependencies
run: npm install
- name: Godot init project
run: godot --import test_projects/test-dap-project-godot4/project.godot --headless
- name: Run headless test
uses: coactions/setup-xvfb@v1
with:
@@ -31,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.0.4
uses: actions/setup-node@v6.1.0
with:
node-version: 16.x
@@ -48,7 +75,7 @@ jobs:
ls -l godot-tools.vsix
- name: Upload extension VSIX
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.6.2
with:
name: godot-tools
path: godot-tools.vsix

2
.gitignore vendored
View File

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

View File

@@ -2,8 +2,10 @@ 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'],
workspaceFolder: './test_projects/test-dap-project-godot4',
}
);

6
.vscode/extensions.json vendored Normal file
View File

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

34
.vscode/launch.json vendored
View File

@@ -16,9 +16,13 @@
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"skipFiles": [
"**/extensionHostProcess.js",
"<node_internals>/**/*.js"
],
"preLaunchTask": "npm: watch",
"env": {
"VSCODE_DEBUG_MODE": true
"VSCODE_DEBUG_MODE": "true"
}
},
{
@@ -26,6 +30,28 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--profile=temp",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}/test_projects/test-dap-project-godot4"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"skipFiles": [
"**/extensionHostProcess.js",
"<node_internals>/**/*.js"
],
"preLaunchTask": "npm: watch",
"env": {
"VSCODE_DEBUG_MODE": "true"
}
},
{
"name": "Run Extension with local workspace file",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--profile=temp",
"--extensionDevelopmentPath=${workspaceFolder}",
@@ -34,9 +60,13 @@
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"skipFiles": [
"**/extensionHostProcess.js",
"<node_internals>/**/*.js"
],
"preLaunchTask": "npm: watch",
"env": {
"VSCODE_DEBUG_MODE": true
"VSCODE_DEBUG_MODE": "true"
}
},
]

48
.vscode/test_files.code-snippets vendored Normal file
View File

@@ -0,0 +1,48 @@
{
"# --- IN ---": {
"scope": "gdscript",
"prefix": "#IN",
"body": [
"# --- IN ---"
],
"description": "Snapshot Test #IN block"
},
"# --- OUT ---": {
"scope": "gdscript",
"prefix": "#OUT",
"body": [
"# --- OUT ---"
],
"description": "Snapshot Test #OUT block"
},
"# --- END ---": {
"scope": "gdscript",
"prefix": "#END",
"body": [
"# --- END ---"
],
"description": "Snapshot Test #END block"
},
"# --- CONFIG ---": {
"scope": "gdscript",
"prefix": [
"#CO",
"#CONFIG"
],
"body": [
"# --- CONFIG ---"
],
"description": "Snapshot Test #CONFIG block"
},
"# --- CONFIG ALL ---": {
"scope": "gdscript",
"prefix": [
"#CA",
"#CONFIG ALL"
],
"body": [
"# --- CONFIG ALL ---"
],
"description": "Snapshot Test #CONFIG ALL block"
},
}

View File

@@ -1,5 +1,52 @@
# 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)
- The items are expandable/collapsible, and the links on the right side of the panel work for any file inside the user's project
- [**Improve GDScript formatter**](https://github.com/godotengine/godot-vscode-plugin/pull/746)
- Add new style of formatter snapshot tests
- Add many new test cases
- Fix several issues ([#728](https://github.com/godotengine/godot-vscode-plugin/pull/728), [#624](https://github.com/godotengine/godot-vscode-plugin/pull/624), [#657](https://github.com/godotengine/godot-vscode-plugin/pull/657), [#717](https://github.com/godotengine/godot-vscode-plugin/pull/717), [#734](https://github.com/godotengine/godot-vscode-plugin/pull/734), likely more)
- [**Add debugger support for typed Dictionaries**](https://github.com/godotengine/godot-vscode-plugin/pull/764)
- [Add some useful GDScript snippets for Godot 4](https://github.com/godotengine/godot-vscode-plugin/pull/794)
- [Add setting to enable/disable documentation minimap](https://github.com/godotengine/godot-vscode-plugin/pull/786)
- [Add newline when dropping nodes into editor](https://github.com/godotengine/godot-vscode-plugin/pull/754)
- [Add `@static_unload` annotation and Godot 4.3 Variant types to syntax highlighting](https://github.com/godotengine/godot-vscode-plugin/pull/738)
- [Overhaul LSP client](https://github.com/godotengine/godot-vscode-plugin/pull/752)
- Simplify LSP client internals
- Streamline control flow between Client, IO, and Buffer classes
- Create canonical, obvious place to implement filters on incoming and outgoing LSP messages
- Remove legacy WebSockets-based LSP support
- [Update float syntax rules and formatting to better support complex cases](https://github.com/godotengine/godot-vscode-plugin/pull/756)
- [Implement Godot-in-the-loop test suite and fix debugger errors](https://github.com/godotengine/godot-vscode-plugin/pull/788)
- [Remove OS, GDScript and Object from the list of builtins in syntax highlighting](https://github.com/godotengine/godot-vscode-plugin/pull/739)
- [Fix typed arrays of scripts not being decoded properly](https://github.com/godotengine/godot-vscode-plugin/pull/731)
- [Fix debugger watch window freeze caused by missing responses](https://github.com/godotengine/godot-vscode-plugin/pull/781)
- [Fix the TextMate grammar erroneously tagging enum members and const variables as language constants](https://github.com/godotengine/godot-vscode-plugin/pull/737)
- [Fix VBoxContainer and HBoxContainer documentation not opening](https://github.com/godotengine/godot-vscode-plugin/pull/755)
### 2.3.0
- [Add documentation page scaling feature](https://github.com/godotengine/godot-vscode-plugin/pull/722)
- [Suppress "workspace/symbol" not found error](https://github.com/godotengine/godot-vscode-plugin/pull/723)
- [Capitalize the drive letter in Windows absolute paths](https://github.com/godotengine/godot-vscode-plugin/pull/727)
### 2.2.0
- [Add partial debugger support for new types (such as typed arrays)](https://github.com/godotengine/godot-vscode-plugin/pull/715)

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,17 +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"
}
"noUselessElse": "off",
"useImportType": "off",
"noParameterAssign": "warn"
},
"suspicious": {
"noExplicitAny": "off"
}
}
}
}

View File

@@ -6,14 +6,12 @@
"\t$3"
]
},
"Print messages to console": {
"prefix": "pr",
"body": [
"print($1)"
]
},
"_ready method of Node": {
"prefix": "ready",
"body": [
@@ -21,7 +19,6 @@
"\t${1:pass}"
]
},
"_init method of Object": {
"prefix": "init",
"body": [
@@ -29,195 +26,260 @@
"\t${1:pass}"
]
},
"_process method of Node": {
"prefix": "process",
"body": [
"func _process(delta):",
"\t${1:pass}"
]
"func _process(delta):",
"\t${1:pass}"
]
},
"_physics_process method of Node": {
"prefix": "physics",
"body": [
"func _physics_process(delta):",
"\t${1:pass}"
]
"func _physics_process(delta):",
"\t${1:pass}"
]
},
"_input method of Node": {
"prefix": "input",
"body": [
"func _input(event):",
"\t${1:pass}"
]
"func _input(event):",
"\t${1:pass}"
]
},
"_input_event method of Node": {
"_input_event method of Node": {
"prefix": "inpute",
"body": [
"func _input_event(event):",
"\t${1:pass}"
]
},
"_unhandled_input method of Node": {
"prefix": "uinput",
"body": [
"func _unhandled_input(event):",
"\t${1:pass}"
]
},
"_draw method of Node": {
"prefix": "draw",
"body": [
"func _draw():",
"\t${1:pass}"
]
},
"_gui_input method of Node": {
"prefix": "guii",
"body": [
"func _gui_input(event):",
"\t${1:pass}"
]
},
"func _input_event(event):",
"\t${1:pass}"
]
},
"_unhandled_input method of Node": {
"prefix": "uinput",
"body": [
"func _unhandled_input(event):",
"\t${1:pass}"
]
},
"_draw method of Node": {
"prefix": "draw",
"body": [
"func _draw():",
"\t${1:pass}"
]
},
"_gui_input method of Node": {
"prefix": "guii",
"body": [
"func _gui_input(event):",
"\t${1:pass}"
]
},
"for loop": {
"prefix": "for",
"body": [
"for $1 in $2:",
"\t${3:pass}"
]
"for $1 in $2:",
"\t${3:pass}"
]
},
"for range loop": {
"for range loop": {
"prefix": "for",
"body": [
"for $1 in range(${2:start}{$3:,end}):",
"\t${4:pass}"
]
"for $1 in range(${2:start}{$3:,end}):",
"\t${4:pass}"
]
},
"if elif else": {
"prefix": "if",
"if elif else": {
"prefix": "if",
"body": [
"if ${1:condition}:",
"\t${3:pass}",
"elif ${2:condition}:",
"\t${4:pass}",
"else:",
"\t${5:pass}"
]
},
"if else": {
"prefix": "if",
"if ${1:condition}:",
"\t${3:pass}",
"elif ${2:condition}:",
"\t${4:pass}",
"else:",
"\t${5:pass}"
]
},
"if else": {
"prefix": "if",
"body": [
"if ${1:condition}:",
"\t${2:pass}",
"else:",
"\t${3:pass}"
]
},
"if": {
"prefix": "if",
"if ${1:condition}:",
"\t${2:pass}",
"else:",
"\t${3:pass}"
]
},
"if": {
"prefix": "if",
"body": [
"if ${1:condition}:",
"\t${2:pass}"
]
},
"while": {
"prefix": "while",
"if ${1:condition}:",
"\t${2:pass}"
]
},
"while": {
"prefix": "while",
"body": [
"while ${1:condition}:",
"\t${2:pass}"
]
},
"function define": {
"prefix": "func",
"while ${1:condition}:",
"\t${2:pass}"
]
},
"function define": {
"prefix": "func",
"body": [
"func ${1:method}(${2:args}):",
"\t${3:pass}"
]
},
"match": {
"prefix": "match",
"body": [
"match ${1:expression}:\n\t${2:pattern}:\n\t\t${3}\n\t_:\n\t\t${0:default}"
]
},
"signal declaration": {
"prefix": "signal",
"func ${1:method}(${2:args}):",
"\t${3:pass}"
]
},
"match": {
"prefix": "match",
"body": [
"signal ${1:signalname}(${2:args})"
]
},
"export variables": {
"prefix": "export",
"match ${1:expression}:\n\t${2:pattern}:\n\t\t${3}\n\t_:\n\t\t${0:default}"
]
},
"signal declaration": {
"prefix": "signal",
"body": [
"export(${1:type}${2:,other_configs}) var ${3:name}${4: = default}${5: setget }"
]
},
"define variables": {
"prefix": "var",
"signal ${1:signalname}(${2:args})"
]
},
"export variables": {
"prefix": "export",
"body": [
"var ${1:name}${2: = default}${3: setget }"
]
},
"define onready variables": {
"prefix": "onready",
"@export(${1:type}${2:,other_configs}) var ${3:name}${4: = default}${5: setget }"
]
},
"define variables": {
"prefix": "var",
"body": [
"onready var ${1:name} = get_node($2)"
]
},
"Is instance of a class or script": {
"prefix": "is",
"body": [
"${1:instance} is ${2:class}"
]
},
"element in array": {
"prefix": "in",
"var ${1:name}${2: = default}${3: setget }"
]
},
"define onready variables": {
"prefix": "onready",
"body": [
"${1:element} in ${$2:array}"
]
},
"GDScript template": {
"prefix": "gdscript",
"onready var ${1:name} = get_node($2)"
]
},
"Is instance of a class or script": {
"prefix": "is",
"body": [
"extends ${1:BaseClass}",
"",
"# class member variables go here, for example:",
"# var a = 2",
"# var b = \"textvar\"",
"",
"func _ready():",
"\t# Called every time the node is added to the scene.",
"\t# Initialization here",
"\tpass",
""
]
},
"pass statement": {
"prefix": "pass",
"body": [
"pass"
]
}
}
"${1:instance} is ${2:class}"
]
},
"element in array": {
"prefix": "in",
"body": [
"${1:element} in ${$2:array}"
]
},
"GDScript template": {
"prefix": "gdscript",
"body": [
"extends ${1:BaseClass}",
"",
"# class member variables go here, for example:",
"# var a = 2",
"# var b = \"textvar\"",
"",
"func _ready():",
"\t# Called every time the node is added to the scene.",
"\t# Initialization here",
"\tpass",
""
]
},
"pass statement": {
"prefix": "pass",
"body": [
"pass"
]
},
"GDScript Void": {
"prefix": [
"void"
],
"body": [
"func ${1:function_name}($2) -> void:",
"\t${3:pass}"
],
"description": "Void function"
},
"GDScript Load Resource": {
"prefix": [
"loadres",
"ld"
],
"body": [
"load(\"res://${1:resource_path}\")$0"
],
"description": "Quickly load a resource with the 'res://' prefix"
},
"GDScript Preload Resource": {
"prefix": [
"preloadres",
"pl"
],
"body": [
"preload(\"res://${1:resource_path}\")$0"
],
"description": "Quickly preload a resource with the 'res://' prefix"
},
"GDScript Variable with Getter and Setter": {
"prefix": [
"gs",
"vargetset"
],
"body": [
"var ${1:variable_name}:",
"\tget:",
"\t\treturn ${1:variable_name}",
"\tset(value):",
"\t\t${1:variable_name} = value"
],
"description": "Creates a variable with getter and setter functions in GDScript"
},
"GDScript Variable with Getter and Setter (typed)": {
"prefix": [
"gst",
"vargetsettyped"
],
"body": [
"var ${1:variable_name}: ${2:String}:",
"\tget:",
"\t\treturn ${1:variable_name}",
"\tset(value):",
"\t\t${1:variable_name} = value"
],
"description": "Creates a typed variable with getter and setter functions in GDScript"
},
"GDScript export var": {
"prefix": [
"exportvar",
"xp"
],
"body": [
"export var ${1:variable_name}: ${2:String} = ${3:default_value}"
],
"description": "Creates an exported (typed) variable in GDScript"
},
"GDScript tween": {
"prefix": [
"tween",
"tw"
],
"body": [
"var tween := create_tween()"
],
"description": "Creates a tween object"
},
"GDScript wait": {
"prefix": [
"wait",
"timer"
],
"body": [
"await get_tree().create_timer($1).timeout"
],
"description": "Waits for a given amount of seconds"
}
}

View File

@@ -1,12 +1,8 @@
body {
margin-right: 200px;
}
a {
text-decoration: none;
}
#map {
#minimap {
position: fixed;
top: 0;
right: 0;

1579
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.2.0",
"version": "2.5.1",
"description": "Tools for game development with Godot Engine and GDScript",
"repository": {
"type": "git",
@@ -15,7 +15,7 @@
"author": "The Godot Engine community",
"publisher": "geequlim",
"engines": {
"vscode": "^1.80.0"
"vscode": "^1.96.0"
},
"categories": [
"Programming Languages",
@@ -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",
@@ -251,15 +257,27 @@
"type": "object",
"title": "Godot Tools",
"properties": {
"godotTools.documentation.pageScale": {
"type": "integer",
"default": 100,
"minimum": 50,
"maximum": 200,
"description": "Scale factor (%) to apply to the Godot documentation viewer."
},
"godotTools.documentation.displayMinimap": {
"type": "boolean",
"default": true,
"description": "Whether to display the minimap for the Godot documentation viewer."
},
"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",
@@ -289,20 +307,18 @@
"default": false,
"description": "Whether extra space should be removed from function parameter lists"
},
"godotTools.lsp.serverProtocol": {
"type": [
"string"
],
"godotTools.formatter.spacesBeforeEndOfLineComment": {
"type": "string",
"enum": [
"ws",
"tcp"
"1",
"2"
],
"default": "tcp",
"enumDescriptions": [
"Use the WebSocket protocol to connect to Godot 3.2 and Godot 3.2.1",
"Use the TCP protocol to connect to Godot 3.2.2 and newer versions"
"1 space before EOL comments # Like this.",
"2 spaces before EOL comments  # Like this."
],
"description": "The server protocol of the GDScript language server.\nYou must restart VSCode after changing this value."
"default": "1",
"description": "Number of spaces before an end-of-line comment"
},
"godotTools.lsp.serverHost": {
"type": "string",
@@ -639,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"
}
],
@@ -718,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"
}
],
@@ -873,38 +892,46 @@
}
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/chai": "^4.3.11",
"@types/chai-as-promised": "^8.0.1",
"@types/chai-subset": "^1.3.5",
"@types/marked": "^4.0.8",
"@types/mocha": "^10.0.6",
"@types/node": "^18.15.0",
"@types/node": "^18.19.75",
"@types/prismjs": "^1.16.8",
"@types/vscode": "^1.80.0",
"@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",
"esbuild": "^0.17.15",
"chai": "^4.5.0",
"chai-as-promised": "^8.0.1",
"chai-subset": "^1.6.0",
"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",
"typescript": "^5.2.2"
},
"dependencies": {
"@vscode/debugadapter": "^1.64.0",
"@vscode/debugprotocol": "^1.64.0",
"@vscode/debugadapter": "^1.68.0",
"@vscode/debugprotocol": "^1.68.0",
"await-notify": "^1.0.1",
"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

@@ -1,6 +1,7 @@
import { SceneTreeProvider } from "./scene_tree_provider";
import path = require("path");
import * as path from "node:path";
import { createLogger } from "../utils";
import { SceneTreeProvider } from "./scene_tree_provider";
const log = createLogger("debugger.runtime");
@@ -24,9 +25,9 @@ export class GodotStackVars {
public locals: GodotVariable[] = [],
public members: GodotVariable[] = [],
public globals: GodotVariable[] = [],
) { }
) {}
public reset(count: number = 0) {
public reset(count = 0) {
this.locals = [];
this.members = [];
this.globals = [];
@@ -45,7 +46,7 @@ export interface GodotVariable {
scope_path?: string;
sub_values?: GodotVariable[];
value: any;
type?: bigint;
type?: number;
id?: bigint;
}
@@ -62,7 +63,7 @@ export class RawObject extends Map<any, any> {
}
export class ObjectId implements GDObject {
constructor(public id: bigint) { }
constructor(public id: bigint) {}
public stringify_value(): string {
return `<${this.id}>`;
@@ -85,7 +86,7 @@ export class GodotDebugData {
public last_frames: GodotStackFrame[] = [];
public projectPath: string;
public scene_tree?: SceneTreeProvider;
public stack_count: number = 0;
public stack_count = 0;
public stack_files: string[] = [];
public session;
@@ -126,19 +127,16 @@ export class GodotDebugData {
bps.splice(index, 1);
this.breakpoints.set(pathTo, bps);
const file = `res://${path.relative(this.projectPath, bp.file)}`;
this.session?.controller.remove_breakpoint(
file.replace(/\\/g, "/"),
bp.line,
);
this.session?.controller.remove_breakpoint(file.replace(/\\/g, "/"), bp.line);
}
}
}
public get_all_breakpoints(): GodotBreakpoint[] {
const output: GodotBreakpoint[] = [];
Array.from(this.breakpoints.values()).forEach((bp_array) => {
for (const bp_array of Array.from(this.breakpoints.values())) {
output.push(...bp_array);
});
}
return output;
}
@@ -150,14 +148,14 @@ export class GodotDebugData {
const breakpoints = this.get_all_breakpoints();
let output = "";
if (breakpoints.length > 0) {
output += " --breakpoints \"";
output += ' --breakpoints "';
breakpoints.forEach((bp, i) => {
output += `${this.get_breakpoint_path(bp.file)}:${bp.line}`;
if (i < breakpoints.length - 1) {
output += ",";
}
});
output += "\"";
output += '"';
}
return output;
}

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 = parseFloat(value);
if (isNaN(new_parsed_value)) {
return;
}
} else {
new_parsed_value = parseInt(value);
if (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,41 +1,44 @@
import * as fs from "fs";
import * as fs from "node:fs";
import {
LoggingDebugSession,
InitializedEvent,
Thread,
Source,
Breakpoint,
StoppedEvent,
InitializedEvent,
LoggingDebugSession,
Source,
TerminatedEvent,
Thread,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { debug } from "vscode";
import { Subject } from "await-notify";
import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime";
import { LaunchRequestArguments, AttachRequestArguments } from "../debugger";
import { SceneTreeProvider } from "../scene_tree_provider";
import { ObjectId } from "./variables/variants";
import { parse_variable, is_variable_built_in_type } from "./helpers";
import { ServerController } from "./server_controller";
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";
import { ObjectId } from "./variables/variants";
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;
public inspector: InspectorProvider;
private got_scope: Subject = new Subject();
private ongoing_inspections: bigint[] = [];
private previous_inspections: bigint[] = [];
private configuration_done: Subject = new Subject();
private mode: "launch" | "attach" | "" = "";
public inspect_callbacks: Map<
bigint,
(class_name: string, variable: GodotVariable) => void
> = new Map();
public inspect_callbacks: Map<bigint, (class_name: string, variable: GodotVariable) => void> = new Map();
public constructor() {
super();
@@ -50,7 +53,7 @@ export class GodotDebugSession extends LoggingDebugSession {
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
args: DebugProtocol.InitializeRequestArguments
args: DebugProtocol.InitializeRequestArguments,
) {
response.body = response.body || {};
@@ -79,30 +82,22 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendEvent(new InitializedEvent());
}
protected async launchRequest(
response: DebugProtocol.LaunchResponse,
args: LaunchRequestArguments
) {
protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) {
await this.configuration_done.wait(1000);
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
}
protected async attachRequest(
response: DebugProtocol.AttachResponse,
args: AttachRequestArguments
) {
protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) {
await this.configuration_done.wait(1000);
this.mode = "attach";
this.exception = false;
await this.controller.attach(args);
this.sendResponse(response);
@@ -110,41 +105,32 @@ export class GodotDebugSession extends LoggingDebugSession {
public configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments
args: DebugProtocol.ConfigurationDoneArguments,
) {
this.configuration_done.notify();
this.sendResponse(response);
}
protected continueRequest(
response: DebugProtocol.ContinueResponse,
args: DebugProtocol.ContinueArguments
) {
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
protected async evaluateRequest(
response: DebugProtocol.EvaluateResponse,
args: DebugProtocol.EvaluateArguments
) {
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
if (this.all_scopes) {
var variable = this.get_variable(args.expression, null, null, null);
if (variable.error == null) {
var parsed_variable = parse_variable(variable.variable);
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
variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0,
};
} else {
} catch (error) {
response.success = false;
response.message = variable.error;
response.message = error.toString();
}
}
@@ -158,30 +144,17 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
protected nextRequest(
response: DebugProtocol.NextResponse,
args: DebugProtocol.NextArguments
) {
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
this.controller.next();
this.sendResponse(response);
}
protected pauseRequest(
response: DebugProtocol.PauseResponse,
args: DebugProtocol.PauseArguments
) {
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
this.controller.break();
this.sendResponse(response);
}
protected async scopesRequest(
response: DebugProtocol.ScopesResponse,
args: DebugProtocol.ScopesArguments
) {
protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
this.controller.request_stack_frame_vars(args.frameId);
await this.got_scope.wait(2000);
@@ -197,7 +170,7 @@ export class GodotDebugSession extends LoggingDebugSession {
protected setBreakPointsRequest(
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments
args: DebugProtocol.SetBreakpointsArguments,
) {
const path = (args.source.path as string).replace(/\\/g, "/");
const client_lines = args.lines || [];
@@ -206,19 +179,19 @@ export class GodotDebugSession extends LoggingDebugSession {
let bps = this.debug_data.get_breakpoints(path);
const bp_lines = bps.map((bp) => bp.line);
bps.forEach((bp) => {
for (const bp of bps) {
if (client_lines.indexOf(bp.line) === -1) {
this.debug_data.remove_breakpoint(path, bp.line);
}
});
client_lines.forEach((l) => {
}
for (const l of client_lines) {
if (bp_lines.indexOf(l) === -1) {
const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
const bp = args.breakpoints.find((bp_at_line) => bp_at_line.line === l);
if (!bp.condition) {
this.debug_data.set_breakpoint(path, l);
}
}
});
}
bps = this.debug_data.get_breakpoints(path);
// Sort to ensure breakpoints aren't out-of-order, which would confuse VS Code.
@@ -226,12 +199,7 @@ export class GodotDebugSession extends LoggingDebugSession {
response.body = {
breakpoints: bps.map((bp) => {
return new Breakpoint(
true,
bp.line,
1,
new Source(bp.file.split("/").reverse()[0], bp.file)
);
return new Breakpoint(true, bp.line, 1, new Source(bp.file.split("/").reverse()[0], bp.file));
}),
};
@@ -239,10 +207,7 @@ export class GodotDebugSession extends LoggingDebugSession {
}
}
protected stackTraceRequest(
response: DebugProtocol.StackTraceResponse,
args: DebugProtocol.StackTraceArguments
) {
protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
if (this.debug_data.last_frame) {
response.body = {
totalFrames: this.debug_data.last_frames.length,
@@ -252,10 +217,7 @@ export class GodotDebugSession extends LoggingDebugSession {
name: sf.function,
line: sf.line,
column: 1,
source: new Source(
sf.file,
`${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`
),
source: new Source(sf.file, `${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`),
};
}),
};
@@ -263,30 +225,17 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
protected stepInRequest(
response: DebugProtocol.StepInResponse,
args: DebugProtocol.StepInArguments
) {
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
this.controller.step();
this.sendResponse(response);
}
protected stepOutRequest(
response: DebugProtocol.StepOutResponse,
args: DebugProtocol.StepOutArguments
) {
if (!this.exception) {
this.controller.step_out();
this.sendResponse(response);
}
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
this.controller.step_out();
this.sendResponse(response);
}
protected terminateRequest(
response: DebugProtocol.TerminateResponse,
args: DebugProtocol.TerminateArguments
) {
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
if (this.mode === "launch") {
this.controller.stop();
this.sendEvent(new TerminatedEvent());
@@ -301,11 +250,11 @@ export class GodotDebugSession extends LoggingDebugSession {
protected async variablesRequest(
response: DebugProtocol.VariablesResponse,
args: DebugProtocol.VariablesArguments
args: DebugProtocol.VariablesArguments,
) {
if (!this.all_scopes) {
response.body = {
variables: []
variables: [],
};
this.sendResponse(response);
return;
@@ -319,8 +268,7 @@ export class GodotDebugSession extends LoggingDebugSession {
} 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
(sva) => sva && sva.scope_path === va.scope_path && sva.name === va.name,
);
if (sva) {
return parse_variable(
@@ -329,8 +277,8 @@ export class GodotDebugSession extends LoggingDebugSession {
(va_idx) =>
va_idx &&
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
va_idx.name === va.name
)
va_idx.name === va.name,
),
);
}
});
@@ -343,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,
@@ -354,7 +298,7 @@ export class GodotDebugSession extends LoggingDebugSession {
name: "local",
value: undefined,
sub_values: stackVars.locals,
scope_path: "@"
scope_path: "@",
},
{
name: "member",
@@ -370,20 +314,20 @@ export class GodotDebugSession extends LoggingDebugSession {
},
];
stackVars.locals.forEach((va) => {
for (const va of stackVars.locals) {
va.scope_path = "@.local";
this.append_variable(va);
});
}
stackVars.members.forEach((va) => {
for (const va of stackVars.members) {
va.scope_path = "@.member";
this.append_variable(va);
});
}
stackVars.globals.forEach((va) => {
for (const va of stackVars.globals) {
va.scope_path = "@.global";
this.append_variable(va);
});
}
this.add_to_inspections();
@@ -394,21 +338,19 @@ export class GodotDebugSession extends LoggingDebugSession {
}
public set_inspection(id: bigint, replacement: GodotVariable) {
const variables = this.all_scopes.filter(
(va) => va && va.value instanceof ObjectId && va.value.id === id
);
const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id);
variables.forEach((va) => {
for (const va of variables) {
const index = this.all_scopes.findIndex((va_id) => va_id === va);
const old = this.all_scopes.splice(index, 1);
replacement.name = old[0].name;
replacement.scope_path = old[0].scope_path;
this.append_variable(replacement, index);
});
}
this.ongoing_inspections.splice(
this.ongoing_inspections.findIndex((va_id) => va_id === id),
1
1,
);
this.previous_inspections.push(id);
@@ -422,7 +364,7 @@ export class GodotDebugSession extends LoggingDebugSession {
}
private add_to_inspections() {
this.all_scopes.forEach((va) => {
for (const va of this.all_scopes) {
if (va && va.value instanceof ObjectId) {
if (
!this.ongoing_inspections.includes(va.value.id) &&
@@ -432,38 +374,57 @@ export class GodotDebugSession extends LoggingDebugSession {
this.ongoing_inspections.push(va.value.id);
}
}
});
}
}
protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } {
var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: null };
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;
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;
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;
}
var items = expression.split(".");
var propertyName = items[index + 1];
var path = items.slice(0, index + 1).join(".")
.split("self.").join("")
.split("self").join("")
.split("[").join(".")
.split("]").join("");
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") {
if (items.length === 1 && items[0] === "self") {
propertyName = "self";
}
// Detect index/key
var key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0];
let key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0];
if (key) {
key = key.replace(/['"]+/g, "");
propertyName = propertyName.split(/(?<=\[).*(?=\])/).join("").split("\[\]").join("");
propertyName = propertyName
.split(/(?<=\[).*(?=\])/)
.join("")
.split("[]")
.join("");
if (path) path += ".";
path += propertyName;
propertyName = key;
@@ -474,49 +435,60 @@ export class GodotDebugSession extends LoggingDebugSession {
}
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("");
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("");
}
var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) {
return {
const sanitized_all_scopes = this.all_scopes
.filter((x) => x)
.map((x) => ({
sanitized: {
name: sanitizeName(x.name),
scope_path: sanitizeScopePath(x.scope_path)
scope_path: sanitizeScopePath(x.scope_path),
},
real: x
};
});
real: x,
}));
result.variable = sanitized_all_scopes
.find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path)
?.real;
result.variable = sanitized_all_scopes.find(
(x) => x.sanitized.name === propertyName && x.sanitized.scope_path === path,
)?.real;
if (!result.variable) {
result.error = `Could not find: ${propertyName}`;
return result;
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;
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) {
var collection = path.split(".")[path.split(".").length - 1];
var 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;
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 {
result.object_id = Array.from(root.value.entries())
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName)[1].id;
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;
}
}
@@ -524,7 +496,9 @@ export class GodotDebugSession extends LoggingDebugSession {
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);
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);

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

@@ -1,27 +1,41 @@
import * as fs from "fs";
import net = require("net");
import { debug, window } from "vscode";
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { VariantEncoder } from "./variables/variant_encoder";
import { VariantDecoder } from "./variables/variant_decoder";
import { RawObject } from "./variables/variants";
import { GodotStackFrame, GodotStackVars } from "../debug_runtime";
import { GodotDebugSession } from "./debug_session";
import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers";
import { get_configuration, get_free_port, createLogger, verify_godot_version, get_project_version } from "../../utils";
import { DebugProtocol } from "@vscode/debugprotocol";
import * as fs from "node:fs";
import * as net from "node:net";
import { debug, window } from "vscode";
import {
ansi,
convert_resource_path_to_uri,
createLogger,
get_configuration,
get_free_port,
get_project_version,
verify_godot_version,
VERIFY_RESULT,
} from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
import { subProcess, killSubProcesses } from "../../utils/subspawn";
import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger";
import { killSubProcesses, subProcess } from "../../utils/subspawn";
import { GodotStackFrame, GodotStackVars } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments, pinnedScene } from "../debugger";
import { GodotDebugSession } from "./debug_session";
import { build_sub_values, parse_next_scene_node, split_buffers } from "./helpers";
import { VariantDecoder } from "./variables/variant_decoder";
import { VariantEncoder } from "./variables/variant_encoder";
import { RawObject } from "./variables/variants";
import 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 {
@@ -34,12 +48,10 @@ 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
) { }
public constructor(public session: GodotDebugSession) {}
public break() {
this.send_command("break");
@@ -87,12 +99,8 @@ export class ServerController {
this.send_command("get_stack_frame_vars", [frame_id]);
}
public set_object_property(objectId: bigint, label: string, newParsedValue: any) {
this.send_command("set_object_property", [
objectId,
label,
newParsedValue,
]);
public set_object_property(objectId: bigint, label: string, newParsedValue) {
this.send_command("set_object_property", [objectId, label, newParsedValue]);
}
public set_exception(exception: string) {
@@ -103,7 +111,7 @@ export class ServerController {
log.info("Starting game process");
let godotPath: string;
let result;
let result: VERIFY_RESULT;
if (args.editor_path) {
log.info("Using 'editor_path' variable from launch.json");
@@ -168,12 +176,12 @@ export class ServerController {
const address = args.address.replace("tcp://", "");
command += ` --remote-debug "${address}:${args.port}"`;
if (args.profiling) { command += " --profiling"; }
if (args.debug_collisions) { command += " --debug-collisions"; }
if (args.debug_paths) { command += " --debug-paths"; }
if (args.frame_delay) { command += ` --frame-delay ${args.frame_delay}`; }
if (args.time_scale) { command += ` --time-scale ${args.time_scale}`; }
if (args.fixed_fps) { command += ` --fixed-fps ${args.fixed_fps}`; }
if (args.profiling) command += " --profiling";
if (args.debug_collisions) command += " --debug-collisions";
if (args.debug_paths) command += " --debug-paths";
if (args.frame_delay) command += ` --frame-delay ${args.frame_delay}`;
if (args.time_scale) command += ` --time-scale ${args.time_scale}`;
if (args.fixed_fps) command += ` --fixed-fps ${args.fixed_fps}`;
if (args.scene && args.scene !== "main") {
log.info(`Custom scene argument provided: ${args.scene}`);
@@ -219,15 +227,15 @@ export class ServerController {
command += this.session.debug_data.get_breakpoint_string();
if (args.additional_options) {
command += " " + args.additional_options;
command += ` ${args.additional_options}`;
}
log.info(`Launching game process using command: '${command}'`);
const debugProcess = subProcess("debug", command, { shell: true, detached: true });
debugProcess.stdout.on("data", (data) => { });
debugProcess.stderr.on("data", (data) => { });
debugProcess.on("close", (code) => { });
debugProcess.stdout.on("data", (data) => {});
debugProcess.stderr.on("data", (data) => {});
debugProcess.on("close", (code) => {});
}
private stash: Buffer;
@@ -351,7 +359,7 @@ export class ServerController {
}
}
private handle_command(command: Command) {
private async handle_command(command: Command) {
switch (command.command) {
case "debug_enter": {
const reason: string = command.parameters[1];
@@ -379,7 +387,7 @@ export class ServerController {
case "message:inspect_object": {
let id = BigInt(command.parameters[0]);
const className: string = command.parameters[1];
const properties: any[] = command.parameters[2];
const properties: string[] = command.parameters[2];
// message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer,
// thus we need to convert it to its equivalent unsigned value here.
@@ -388,16 +396,13 @@ export class ServerController {
}
const rawObject = new RawObject(className);
properties.forEach((prop) => {
for (const prop of properties) {
rawObject.set(prop[0], prop[5]);
});
}
const inspectedVariable = { name: "", value: rawObject };
build_sub_values(inspectedVariable);
if (this.session.inspect_callbacks.has(BigInt(id))) {
this.session.inspect_callbacks.get(BigInt(id))(
inspectedVariable.name,
inspectedVariable
);
this.session.inspect_callbacks.get(BigInt(id))(inspectedVariable.name, inspectedVariable);
this.session.inspect_callbacks.delete(BigInt(id));
}
this.session.set_inspection(id, inspectedVariable);
@@ -425,15 +430,101 @@ export class ServerController {
this.didFirstOutput = true;
// this.request_scene_tree();
}
command.parameters.forEach((line) => {
debug.activeDebugConsole.appendLine(line[0]);
});
for (const output of command.parameters) {
for (const line of output[0].split("\n")) {
debug.activeDebugConsole.appendLine(bbcodeParser.parse(line));
}
}
break;
}
case "error": {
if (!this.didFirstOutput) {
this.didFirstOutput = true;
}
this.handle_error(command);
break;
}
}
}
async handle_error(command: Command) {
const params = command.parameters[0];
const e = {
hr: params[0],
min: params[1],
sec: params[2],
msec: params[3],
func: params[4] as string,
file: params[5] as string,
line: params[6],
cond: params[7] as string,
msg: params[8] as string,
warning: params[9] as boolean,
stack: [],
};
const stackCount = command.parameters[1];
for (let i = 0; i < stackCount; i += 3) {
const file = command.parameters[i + 2];
const func = command.parameters[i + 3];
const line = command.parameters[i + 4];
const msg = `${file}:${line} @ ${func}()`;
const extras = {
source: { name: (await convert_resource_path_to_uri(file)).toString() },
line: line,
};
e.stack.push({ msg: msg, extras: extras });
}
const time = `${e.hr}:${e.min}:${e.sec}.${e.msec}`;
const location = `${e.file}:${e.line} @ ${e.func}()`;
const color = e.warning ? "yellow" : "red";
const lang = e.file.startsWith("res://") ? "GDScript" : "C++";
const extras = {
source: { name: (await convert_resource_path_to_uri(e.file)).toString() },
line: e.line,
group: "startCollapsed",
};
if (e.msg) {
this.stderr(`${ansi[color]}${time} | ${e.func}: ${e.msg}`, extras);
this.stderr(`${ansi.dim.white}<${lang} Error> ${ansi.white}${e.cond}`);
} else {
this.stderr(`${ansi[color]}${time} | ${e.func}: ${e.cond}`, extras);
}
this.stderr(`${ansi.dim.white}<${lang} Source> ${ansi.white}${location}`);
if (stackCount !== 0) {
this.stderr(`${ansi.dim.white}<Stack Trace>`, { group: "start" });
for (const frame of e.stack) {
this.stderr(`${ansi.white}${frame.msg}`, frame.extras);
}
this.stderr("", { group: "end" });
}
this.stderr("", { group: "end" });
}
stdout(output = "", extra = {}) {
this.session.sendEvent({
event: "output",
body: {
category: "stdout",
output: output + ansi.reset,
...extra,
},
} as DebugProtocol.OutputEvent);
}
stderr(output = "", extra = {}) {
this.session.sendEvent({
event: "output",
body: {
category: "stderr",
output: output + ansi.reset,
...extra,
},
} as DebugProtocol.OutputEvent);
}
public abort() {
log.info("Aborting debug controller");
this.session.sendEvent(new TerminatedEvent());
@@ -468,19 +559,14 @@ export class ServerController {
const line = stackFrames[0].line;
if (this.steppingOut) {
const breakpoint = this.session.debug_data
.get_breakpoints(file)
.find((bp) => bp.line === line);
const breakpoint = this.session.debug_data.get_breakpoints(file).find((bp) => bp.line === line);
if (!breakpoint) {
if (this.session.debug_data.stack_count > 1) {
continueStepping = this.session.debug_data.stack_count === stackCount;
} else {
const fileSame =
stackFrames[0].file === this.session.debug_data.last_frame.file;
const funcSame =
stackFrames[0].function === this.session.debug_data.last_frame.function;
const lineGreater =
stackFrames[0].line >= this.session.debug_data.last_frame.line;
const fileSame = stackFrames[0].file === this.session.debug_data.last_frame.file;
const funcSame = stackFrames[0].function === this.session.debug_data.last_frame.function;
const lineGreater = stackFrames[0].line >= this.session.debug_data.last_frame.line;
continueStepping = fileSame && funcSame && lineGreater;
}
@@ -505,10 +591,7 @@ 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)
);
this.session.sendEvent(new StoppedEvent("exception", 0, this.exception));
}
}
@@ -535,8 +618,8 @@ export class ServerController {
const stackVars = new GodotStackVars();
let localsRemaining = parameters[0];
let membersRemaining = parameters[1 + (localsRemaining * 2)];
let globalsRemaining = parameters[2 + ((localsRemaining + membersRemaining) * 2)];
let membersRemaining = parameters[1 + localsRemaining * 2];
let globalsRemaining = parameters[2 + (localsRemaining + membersRemaining) * 2];
let i = 1;
while (localsRemaining--) {
@@ -551,7 +634,8 @@ export class ServerController {
stackVars.globals.push({ name: parameters[i++], value: parameters[i++] });
}
stackVars.forEach(item => build_sub_values(item));
// 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,47 +1,41 @@
import * as fs from "fs";
import {
LoggingDebugSession,
InitializedEvent,
Thread,
Source,
Breakpoint,
StoppedEvent,
TerminatedEvent,
Breakpoint,
InitializedEvent,
LoggingDebugSession,
Source,
TerminatedEvent,
Thread,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { debug } from "vscode";
import { Subject } from "await-notify";
import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime";
import { LaunchRequestArguments, AttachRequestArguments } from "../debugger";
import { SceneTreeProvider } from "../scene_tree_provider";
import { ObjectId } from "./variables/variants";
import { parse_variable, is_variable_built_in_type } from "./helpers";
import { ServerController } from "./server_controller";
import * as fs from "node:fs";
import { createLogger } from "../../utils";
import { GodotDebugData } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
import { InspectorProvider } from "../inspector_provider";
import { SceneTreeProvider } from "../scene_tree_provider";
import { ServerController } from "./server_controller";
import { VariablesManager } from "./variables/variables_manager";
const log = createLogger("debugger.session", { output: "Godot Debugger" });
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() {
@@ -50,8 +44,9 @@ export class GodotDebugSession extends LoggingDebugSession {
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
args: DebugProtocol.InitializeRequestArguments
args: DebugProtocol.InitializeRequestArguments,
) {
log.info("initializeRequest", args);
response.body = response.body || {};
response.body.supportsConfigurationDoneRequest = true;
@@ -79,30 +74,25 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendEvent(new InitializedEvent());
}
protected async launchRequest(
response: DebugProtocol.LaunchResponse,
args: LaunchRequestArguments
) {
protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) {
log.info("launchRequest", args);
await this.configuration_done.wait(1000);
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
}
protected async attachRequest(
response: DebugProtocol.AttachResponse,
args: AttachRequestArguments
) {
protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) {
log.info("attachRequest", args);
await this.configuration_done.wait(1000);
this.mode = "attach";
this.exception = false;
this.debug_data.projectPath = args.project;
await this.controller.attach(args);
this.sendResponse(response);
@@ -110,95 +100,37 @@ export class GodotDebugSession extends LoggingDebugSession {
public configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments
args: DebugProtocol.ConfigurationDoneArguments,
) {
log.info("configurationDoneRequest", args);
this.configuration_done.notify();
this.sendResponse(response);
}
protected continueRequest(
response: DebugProtocol.ContinueResponse,
args: DebugProtocol.ContinueArguments
) {
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
}
protected async evaluateRequest(
response: DebugProtocol.EvaluateResponse,
args: DebugProtocol.EvaluateArguments
) {
await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
if (this.all_scopes) {
var variable = this.get_variable(args.expression, null, null, null);
if (variable.error == null) {
var parsed_variable = parse_variable(variable.variable);
response.body = {
result: parsed_variable.value,
variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0
};
} else {
response.success = false;
response.message = variable.error;
}
}
if (!response.body) {
response.body = {
result: "null",
variablesReference: 0,
};
}
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
log.info("continueRequest", args);
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
protected nextRequest(
response: DebugProtocol.NextResponse,
args: DebugProtocol.NextArguments
) {
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
log.info("nextRequest", args);
this.controller.next();
this.sendResponse(response);
}
protected pauseRequest(
response: DebugProtocol.PauseResponse,
args: DebugProtocol.PauseArguments
) {
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
}
protected async scopesRequest(
response: DebugProtocol.ScopesResponse,
args: DebugProtocol.ScopesArguments
) {
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 },
],
};
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
log.info("pauseRequest", args);
this.controller.break();
this.sendResponse(response);
}
protected setBreakPointsRequest(
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments
args: DebugProtocol.SetBreakpointsArguments,
) {
log.info("setBreakPointsRequest", args);
const path = (args.source.path as string).replace(/\\/g, "/");
const client_lines = args.lines || [];
@@ -206,19 +138,19 @@ export class GodotDebugSession extends LoggingDebugSession {
let bps = this.debug_data.get_breakpoints(path);
const bp_lines = bps.map((bp) => bp.line);
bps.forEach((bp) => {
for (const bp of bps) {
if (client_lines.indexOf(bp.line) === -1) {
this.debug_data.remove_breakpoint(path, bp.line);
}
});
client_lines.forEach((l) => {
}
for (const l of client_lines) {
if (bp_lines.indexOf(l) === -1) {
const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
const bp = args.breakpoints.find((bp_at_line) => bp_at_line.line === l);
if (!bp.condition) {
this.debug_data.set_breakpoint(path, l);
}
}
});
}
bps = this.debug_data.get_breakpoints(path);
// Sort to ensure breakpoints aren't out-of-order, which would confuse VS Code.
@@ -226,12 +158,7 @@ export class GodotDebugSession extends LoggingDebugSession {
response.body = {
breakpoints: bps.map((bp) => {
return new Breakpoint(
true,
bp.line,
1,
new Source(bp.file.split("/").reverse()[0], bp.file)
);
return new Breakpoint(true, bp.line, 1, new Source(bp.file.split("/").reverse()[0], bp.file));
}),
};
@@ -239,10 +166,36 @@ export class GodotDebugSession extends LoggingDebugSession {
}
}
protected stackTraceRequest(
response: DebugProtocol.StackTraceResponse,
args: DebugProtocol.StackTraceArguments
) {
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) {
response.body = {
totalFrames: this.debug_data.last_frames.length,
@@ -252,300 +205,84 @@ export class GodotDebugSession extends LoggingDebugSession {
name: sf.function,
line: sf.line,
column: 1,
source: new Source(
sf.file,
`${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`
),
source: new Source(sf.file, `${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`),
};
}),
};
}
log.info("stackTraceRequest response", response);
this.sendResponse(response);
}
protected stepInRequest(
response: DebugProtocol.StepInResponse,
args: DebugProtocol.StepInArguments
) {
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
) {
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
) {
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) {
response.body = { threads: [new Thread(0, "thread_1")] };
log.info("scopesRequest response", response);
this.sendResponse(response);
}
protected async variablesRequest(
response: DebugProtocol.VariablesResponse,
args: DebugProtocol.VariablesArguments
args: DebugProtocol.VariablesArguments,
) {
if (!this.all_scopes) {
log.info("variablesRequest", args);
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: "@",
},
];
stackVars.locals.forEach((va) => {
va.scope_path = "@.local";
this.append_variable(va);
});
stackVars.members.forEach((va) => {
va.scope_path = "@.member";
this.append_variable(va);
});
stackVars.globals.forEach((va) => {
va.scope_path = "@.global";
this.append_variable(va);
});
this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
}
public set_inspection(id: bigint, replacement: GodotVariable) {
const variables = this.all_scopes.filter(
(va) => va && va.value instanceof ObjectId && va.value.id === id
);
variables.forEach((va) => {
const index = this.all_scopes.findIndex((va_id) => va_id === va);
const old = this.all_scopes.splice(index, 1);
replacement.name = old[0].name;
replacement.scope_path = old[0].scope_path;
this.append_variable(replacement, index);
});
this.ongoing_inspections.splice(
this.ongoing_inspections.findIndex((va_id) => va_id === id),
1
);
this.previous_inspections.push(id);
// this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
}
private add_to_inspections() {
this.all_scopes.forEach((va) => {
if (va && va.value instanceof ObjectId) {
if (
!this.ongoing_inspections.includes(va.value.id) &&
!this.previous_inspections.includes(va.value.id)
) {
this.controller.request_inspect_object(va.value.id);
this.ongoing_inspections.push(va.value.id);
}
}
});
}
protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } {
var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: 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;
}
var items = expression.split(".");
var propertyName = items[index + 1];
var 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
var 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("");
}
var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) {
return {
sanitized: {
name: sanitizeName(x.name),
scope_path: sanitizeScopePath(x.scope_path)
},
real: x
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,
};
});
result.variable = sanitized_all_scopes
.find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path)
?.real;
if (!result.variable) {
result.error = `Could not find: ${propertyName}`;
return result;
}
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) {
var collection = path.split(".")[path.split(".").length - 1];
var 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 entries = Array.from(root.value.entries());
const item = 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,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,88 +32,34 @@ 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 build_sub_values(va: GodotVariable) {
const value = va.value;
export function get_sub_values(value: any): GodotVariable[] {
let subValues: GodotVariable[] = undefined;
if (value && Array.isArray(value)) {
subValues = value.map((va, i) => {
return { name: `${i}`, value: va } as GodotVariable;
});
} else if (value instanceof Map) {
subValues = Array.from(value.keys()).map((va) => {
if (typeof va["stringify_value"] === "function") {
return {
name: `${va.type_name()}${va.stringify_value()}`,
value: value.get(va),
} as GodotVariable;
} else {
return {
name: `${va}`,
value: value.get(va),
} as GodotVariable;
}
});
} else if (value && typeof value["sub_values"] === "function") {
subValues = value.sub_values().map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
});
}
va.sub_values = subValues;
subValues?.forEach(build_sub_values);
}
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 (value) {
if (Array.isArray(value)) {
rendered_value = `Array[${value.length}]`;
array_size = value.length;
array_type = "indexed";
reference = i ? i : 0;
subValues = value.map((va, i) => {
return { name: `${i}`, value: va } as GodotVariable;
});
} 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 {
rendered_value = `${value.type_name()}${value.stringify_value()}`;
reference = i ? i : 0;
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;
});
}
}
return {
name: va.name,
value: rendered_value,
variablesReference: reference,
array_size: array_size > 0 ? array_size : undefined,
filter: array_type,
};
for (let i = 0; i < subValues?.length; i++) {
subValues[i].sub_values = get_sub_values(subValues[i].value);
}
return subValues;
}

View File

@@ -1,27 +1,67 @@
import * as fs from "fs";
import net = require("net");
import { debug, window } from "vscode";
import * as fs from "node:fs";
import * as net from "node:net";
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { VariantEncoder } from "./variables/variant_encoder";
import { VariantDecoder } from "./variables/variant_decoder";
import { RawObject } from "./variables/variants";
import { GodotStackFrame, GodotVariable, GodotStackVars } from "../debug_runtime";
import { GodotDebugSession } from "./debug_session";
import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers";
import { get_configuration, get_free_port, createLogger, verify_godot_version, get_project_version } from "../../utils";
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,
get_configuration,
get_free_port,
get_project_version,
verify_godot_version,
} from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
import { subProcess, killSubProcesses } from "../../utils/subspawn";
import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger";
import { killSubProcesses, subProcess } from "../../utils/subspawn";
import { GodotStackFrame, GodotVariable } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments, pinnedScene } from "../debugger";
import { GodotDebugSession } from "./debug_session";
import { get_sub_values, parse_next_scene_node, split_buffers } from "./helpers";
import { VariantDecoder } from "./variables/variant_decoder";
import { VariantEncoder } from "./variables/variant_encoder";
import { RawObject } from "./variables/variants";
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 {
@@ -34,13 +74,20 @@ export class ServerController {
private server?: net.Server;
private socket?: net.Socket;
private steppingOut = false;
private didFirstOutput: boolean = false;
private partialStackVars = new GodotStackVars();
private connectedVersion = "";
private didFirstOutput = false;
private partialStackVars: GodotPartialStackVars;
private projectVersionMajor: number;
private projectVersionMinor: number;
private projectVersionPoint: number;
public constructor(
public session: GodotDebugSession
) { }
public constructor(public session: GodotDebugSession) {}
public setProjectVersion(projectVersion: string) {
const versionParts = projectVersion.split(".").map(Number);
this.projectVersionMajor = versionParts[0] || 0;
this.projectVersionMinor = versionParts[1] || 0;
this.projectVersionPoint = versionParts[2] || 0;
}
public break() {
this.send_command("break");
@@ -84,16 +131,18 @@ 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: any) {
this.send_command("scene:set_object_property", [
objectId,
label,
newParsedValue,
]);
public set_object_property(objectId: bigint, label: string, newParsedValue) {
this.send_command("scene:set_object_property", [objectId, label, newParsedValue]);
}
public set_exception(exception: string) {
@@ -104,7 +153,7 @@ export class ServerController {
log.info("Starting game process");
let godotPath: string;
let result;
let result: VERIFY_RESULT;
if (args.editor_path) {
log.info("Using 'editor_path' variable from launch.json");
@@ -163,23 +212,23 @@ export class ServerController {
}
}
this.connectedVersion = result.version;
this.setProjectVersion(result.version);
let command = `"${godotPath}" --path "${args.project}"`;
const address = args.address.replace("tcp://", "");
command += ` --remote-debug "tcp://${address}:${args.port}"`;
if (args.profiling) { command += " --profiling"; }
if (args.single_threaded_scene) { command += " --single-threaded-scene"; }
if (args.debug_collisions) { command += " --debug-collisions"; }
if (args.debug_paths) { command += " --debug-paths"; }
if (args.debug_navigation) { command += " --debug-navigation"; }
if (args.debug_avoidance) { command += " --debug-avoidance"; }
if (args.debug_stringnames) { command += " --debug-stringnames"; }
if (args.frame_delay) { command += ` --frame-delay ${args.frame_delay}`; }
if (args.time_scale) { command += ` --time-scale ${args.time_scale}`; }
if (args.disable_vsync) { command += " --disable-vsync"; }
if (args.fixed_fps) { command += ` --fixed-fps ${args.fixed_fps}`; }
if (args.profiling) command += " --profiling";
if (args.single_threaded_scene) command += " --single-threaded-scene";
if (args.debug_collisions) command += " --debug-collisions";
if (args.debug_paths) command += " --debug-paths";
if (args.debug_navigation) command += " --debug-navigation";
if (args.debug_avoidance) command += " --debug-avoidance";
if (args.debug_stringnames) command += " --debug-stringnames";
if (args.frame_delay) command += ` --frame-delay ${args.frame_delay}`;
if (args.time_scale) command += ` --time-scale ${args.time_scale}`;
if (args.disable_vsync) command += " --disable-vsync";
if (args.fixed_fps) command += ` --fixed-fps ${args.fixed_fps}`;
if (args.scene && args.scene !== "main") {
log.info(`Custom scene argument provided: ${args.scene}`);
@@ -225,15 +274,15 @@ export class ServerController {
command += this.session.debug_data.get_breakpoint_string();
if (args.additional_options) {
command += " " + args.additional_options;
command += ` ${args.additional_options}`;
}
log.info(`Launching game process using command: '${command}'`);
const debugProcess = subProcess("debug", command, { shell: true, detached: true });
debugProcess.stdout.on("data", (data) => { });
debugProcess.stderr.on("data", (data) => { });
debugProcess.on("close", (code) => { });
debugProcess.stdout.on("data", (data) => {});
debugProcess.stderr.on("data", (data) => {});
debugProcess.on("close", (code) => {});
}
private stash: Buffer;
@@ -336,18 +385,18 @@ export class ServerController {
this.server.listen(args.port, args.address);
}
private parse_message(dataset: any[]) {
private parse_message(dataset: []) {
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++];
return command;
}
private handle_command(command: Command) {
private async handle_command(command: Command) {
switch (command.command) {
case "debug_enter": {
const reason: string = command.parameters[1];
@@ -357,6 +406,7 @@ export class ServerController {
this.set_exception("");
}
this.request_stack_dump();
this.request_scene_tree();
break;
}
case "debug_exit":
@@ -376,30 +426,34 @@ 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: any[] = command.parameters[2];
const properties: string[] = command.parameters[2];
// message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer,
// 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);
properties.forEach((prop) => {
for (const prop of properties) {
rawObject.set(prop[0], prop[5]);
});
const inspectedVariable = { name: "", value: rawObject };
build_sub_values(inspectedVariable);
if (this.session.inspect_callbacks.has(BigInt(id))) {
this.session.inspect_callbacks.get(BigInt(id))(
inspectedVariable.name,
inspectedVariable
);
this.session.inspect_callbacks.delete(BigInt(id));
}
this.session.set_inspection(id, inspectedVariable);
const sub_values = get_sub_values(rawObject);
// 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);
}
break;
}
case "stack_dump": {
@@ -419,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": {
@@ -437,15 +546,106 @@ export class ServerController {
this.didFirstOutput = true;
// this.request_scene_tree();
}
const lines = command.parameters[0];
for (const line of lines) {
debug.activeDebugConsole.appendLine(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;
}
case "error": {
if (!this.didFirstOutput) {
this.didFirstOutput = true;
}
this.handle_error(command);
break;
}
}
}
async handle_error(command: Command) {
const params = command.parameters;
const e = {
hr: params[0],
min: params[1],
sec: params[2],
msec: params[3],
file: params[4] as string,
func: params[5] as string,
line: params[6],
error: params[7] as string,
desc: params[8] as string,
warning: params[9] as boolean,
stack: [],
};
const stackCount = params[10] ?? 0;
for (let i = 0; i < stackCount; i += 3) {
const file = params[11 + i];
const func = params[12 + i];
const line = params[13 + i];
const msg = `${file.slice("res://".length)}:${line} @ ${func}()`;
const extras = {
source: { name: (await convert_resource_path_to_uri(file)).toString() },
line: line,
};
e.stack.push({ msg: msg, extras: extras });
}
const time = `${e.hr}:${e.min}:${e.sec}.${e.msec}`;
let file = e.file;
if (file.startsWith("res://")) {
file = file.slice("res://".length);
}
const location = `${file}:${e.line}`;
const color = e.warning ? "yellow" : "red";
const lang = e.file.startsWith("res://") ? "GDScript" : "C++";
const extras = {
source: { name: (await convert_resource_path_to_uri(e.file)).toString() },
line: e.line,
group: "startCollapsed",
};
if (e.desc) {
this.stderr(`${ansi[color]}${time} | ${e.desc}`, extras);
this.stderr(`${ansi.dim.white}<${lang} Error> ${ansi.white}${e.error}`);
} else {
this.stderr(`${ansi[color]}${time} | ${e.error}`, extras);
}
this.stderr(`${ansi.dim.white}<${lang} Source> ${ansi.white}${location}`);
if (stackCount !== 0) {
this.stderr(`${ansi.dim.white}<Stack Trace>`, { group: "start" });
for (const frame of e.stack) {
this.stderr(`${ansi.white}${frame.msg}`, frame.extras);
}
this.stderr("", { group: "end" });
}
this.stderr("", { group: "end" });
}
stdout(output = "", extra = {}) {
this.session.sendEvent({
event: "output",
body: {
category: "stdout",
output: output + ansi.reset,
...extra,
},
} as DebugProtocol.OutputEvent);
}
stderr(output = "", extra = {}) {
this.session.sendEvent({
event: "output",
body: {
category: "stderr",
output: output + ansi.reset,
...extra,
},
} as DebugProtocol.OutputEvent);
}
public abort() {
log.info("Aborting debug controller");
this.session.sendEvent(new TerminatedEvent());
@@ -480,19 +680,14 @@ export class ServerController {
const line = stackFrames[0].line;
if (this.steppingOut) {
const breakpoint = this.session.debug_data
.get_breakpoints(file)
.find((bp) => bp.line === line);
const breakpoint = this.session.debug_data.get_breakpoints(file).find((bp) => bp.line === line);
if (!breakpoint) {
if (this.session.debug_data.stack_count > 1) {
continueStepping = this.session.debug_data.stack_count === stackCount;
} else {
const fileSame =
stackFrames[0].file === this.session.debug_data.last_frame.file;
const funcSame =
stackFrames[0].function === this.session.debug_data.last_frame.function;
const lineGreater =
stackFrames[0].line >= this.session.debug_data.last_frame.line;
const fileSame = stackFrames[0].file === this.session.debug_data.last_frame.file;
const funcSame = stackFrames[0].function === this.session.debug_data.last_frame.function;
const lineGreater = stackFrames[0].line >= this.session.debug_data.last_frame.line;
continueStepping = fileSame && funcSame && lineGreater;
}
@@ -517,21 +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)
);
this.session.sendEvent(new StoppedEvent("exception", 0, this.exception));
}
}
private send_command(command: string, parameters?: any[]) {
const commandArray: any[] = [command];
// log.debug("send_command", this.connectedVersion);
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();
@@ -547,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 variable: GodotVariable = { name, value, type };
build_sub_values(variable);
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

@@ -23,7 +23,9 @@ import {
Projection,
ENCODE_FLAG_64,
ENCODE_FLAG_OBJECT_AS_ID,
ENCODE_FLAG_TYPED_ARRAY,
ENCODE_FLAG_TYPED_ARRAY_MASK,
ENCODE_FLAG_TYPED_DICT_MASK,
ContainerTypeFlags,
RID,
Callable,
Signal,
@@ -142,13 +144,9 @@ export class VariantDecoder {
case GDScriptTypes.SIGNAL:
return this.decode_Signal(model);
case GDScriptTypes.DICTIONARY:
return this.decode_Dictionary(model);
return this.decode_Dictionary(model, type);
case GDScriptTypes.ARRAY:
if (type & ENCODE_FLAG_TYPED_ARRAY) {
return this.decode_TypedArray(model);
} else {
return this.decode_Array(model);
}
return this.decode_Array(model, type);
case GDScriptTypes.PACKED_BYTE_ARRAY:
return this.decode_PackedByteArray(model);
case GDScriptTypes.PACKED_INT32_ARRAY:
@@ -210,6 +208,18 @@ export class VariantDecoder {
return output;
}
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 {
return this.decode_String(model);
}
}
private decode_AABBf(model: BufferModel) {
return new AABB(this.decode_Vector3f(model), this.decode_Vector3f(model));
}
@@ -218,26 +228,16 @@ export class VariantDecoder {
return new AABB(this.decode_Vector3d(model), this.decode_Vector3d(model));
}
private decode_Array(model: BufferModel) {
const output: Array<any> = [];
private decode_Array(model: BufferModel, type: GDScriptTypes) {
const output = [];
const count = this.decode_UInt32(model);
for (let i = 0; i < count; i++) {
const value = this.decode_variant(model);
output.push(value);
let arrayType = null;
if (type & ENCODE_FLAG_TYPED_ARRAY_MASK) {
arrayType = this.decode_ContainerTypeFlag(model, type, 16);
}
return output;
}
private decode_TypedArray(model: BufferModel) {
const output: Array<any> = [];
// TODO: the type information is currently discarded
// it needs to be decoded and then packed into the output somehow
const type = this.decode_UInt32(model);
const count = this.decode_UInt32(model);
for (let i = 0; i < count; i++) {
@@ -271,8 +271,20 @@ export class VariantDecoder {
return new Color(rgb.x, rgb.y, rgb.z, a);
}
private decode_Dictionary(model: BufferModel) {
const output = new Map<any, any>();
private decode_Dictionary(model: BufferModel, type: GDScriptTypes) {
const output = new Map();
let keyType = null;
let valueType = null;
if (type & ENCODE_FLAG_TYPED_DICT_MASK) {
keyType = this.decode_ContainerTypeFlag(model, type, 16);
valueType = this.decode_ContainerTypeFlag(model, type, 18);
// console.log("type:", (type >> 16) & 0b11, "keyType:", keyType);
// console.log("type:", type >> 18, "valueType:", valueType);
}
// TODO: the type information is currently discarded
// it needs to be decoded and then packed into the output somehow
const count = this.decode_UInt32(model);
for (let i = 0; i < count; i++) {

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,60 +1,68 @@
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;
export const ENCODE_FLAG_OBJECT_AS_ID = 1 << 16;
export const ENCODE_FLAG_TYPED_ARRAY = 1 << 16;
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,
}
export interface BufferModel {
buffer: Buffer;
@@ -74,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 {
@@ -107,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 {
@@ -140,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)})`;
@@ -209,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 {
@@ -276,7 +284,7 @@ export class ObjectId implements GDObject {
}
public type_name(): string {
return "Object";
return "ObjectId";
}
}
@@ -464,7 +472,7 @@ export class Signal implements GDObject {
constructor(public name: string, public oid: ObjectId) {}
public stringify_value(): string {
return `${this.name}() ${this.oid.stringify_value()}`;
return `(${this.name}, ${this.oid.stringify_value()})`;
}
public sub_values(): GodotVariable[] {

View File

@@ -1,55 +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>;
constructor() {}
constructor() {
this.view = window.createTreeView("godotTools.nodeInspector", {
treeDataProvider: this,
});
}
public clean_up() {
if (this.tree) {
this.tree = undefined;
this._on_did_change_tree_data.fire(undefined);
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);
public fill_tree(element_name: string, class_name: string, object_id: number, variable: GodotVariable) {
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,15 +48,11 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
return element;
}
public get_changed_value(
parents: RemoteProperty[],
property: RemoteProperty,
new_parsed_value: any
) {
public get_changed_value(parents: RemoteProperty[], property: RemoteProperty, new_parsed_value: any) {
const idx = parents.length - 1;
const value = parents[idx].value;
if (Array.isArray(value)) {
const idx = parseInt(property.label);
const idx = Number.parseInt(property.label);
if (idx < value.length) {
value[idx] = new_parsed_value;
}
@@ -78,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 = "";
@@ -104,13 +84,9 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
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" ||
typeof value === "boolean" ||
typeof value === "string"
) {
} else if (typeof value === "bigint" || typeof value === "boolean" || typeof value === "string") {
rendered_value = `${value}`;
} else if (typeof value === "undefined") {
rendered_value = "null";
@@ -131,25 +107,21 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
let child_props: RemoteProperty[] = [];
if (value) {
const sub_variables =
typeof value["sub_values"] === "function" &&
value instanceof ObjectId === false
? value.sub_values()
: Array.isArray(value)
? value.map((va, i) => {
return { name: `${i}`, value: va };
})
: value instanceof Map
? Array.from(value.keys()).map((va) => {
const name =
typeof va["rendered_value"] === "function"
? va.rendered_value()
: `${va}`;
const map_value = value.get(va);
let sub_variables = [];
if (typeof value.sub_values === "function" && value instanceof ObjectId === false) {
sub_variables = value.sub_values();
} else if (Array.isArray(value)) {
sub_variables = value.map((va, i) => {
return { name: `${i}`, value: va };
});
} else if (value instanceof Map) {
sub_variables = Array.from(value.keys()).map((va) => {
const name = typeof va.rendered_value === "function" ? va.rendered_value() : `${va}`;
const map_value = value.get(va);
return { name: name, value: map_value };
});
}
return { name: name, value: map_value };
})
: [];
child_props = sub_variables?.map((va) => {
return this.parse_variable(va, object_id);
});
@@ -160,14 +132,12 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
value,
object_id,
child_props,
child_props.length === 0
? TreeItemCollapsibleState.None
: TreeItemCollapsibleState.Collapsed
child_props.length === 0 ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
);
out_prop.description = rendered_value;
out_prop.properties.forEach((prop) => {
for (const prop of out_prop.properties) {
prop.parent = out_prop;
});
}
out_prop.description = rendered_value;
if (value instanceof ObjectId) {
@@ -180,11 +150,10 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
typeof value === "string"
) {
out_prop.contextValue = "editable_value";
} else if (
Array.isArray(value) ||
(value instanceof Map && value instanceof RawObject === false)
) {
out_prop.properties.forEach((prop) => (prop.changes_parent = true));
} else if (Array.isArray(value) || (value instanceof Map && value instanceof RawObject === false)) {
for (const prop of out_prop.properties) {
prop.parent = out_prop;
}
}
return out_prop;
@@ -200,7 +169,7 @@ export class RemoteProperty extends TreeItem {
public value: any,
public object_id: number,
public properties: RemoteProperty[],
public collapsibleState?: TreeItemCollapsibleState
public collapsibleState?: TreeItemCollapsibleState,
) {
super(label, collapsibleState);
}

View File

@@ -1,41 +1,46 @@
import {
TreeDataProvider,
EventEmitter,
Event,
ProviderResult,
TreeItem,
TreeItemCollapsibleState,
} 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;
}
}
@@ -44,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;
@@ -78,11 +83,11 @@ export class SceneNode extends TreeItem {
) {
super(label);
const iconName = class_name + ".svg";
const iconName = `${class_name}.svg`;
this.iconPath = {
light: path.join(iconDir, "light", iconName),
dark: path.join(iconDir, "dark", iconName),
light: Uri.file(path.join(iconDir, "light", iconName)),
dark: Uri.file(path.join(iconDir, "dark", iconName)),
};
}
}

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,25 +123,18 @@ 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);
}
async function list_classes() {
await globals.lsp.client.list_classes();
await globals.docsProvider.list_native_classes();
}
async function switch_scene_script() {
@@ -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

@@ -1,61 +1,200 @@
import * as vscode from "vscode";
import * as path from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import * as vscode from "vscode";
import { format_document, type FormatterOptions } from "./textmate";
import * as chai from "chai";
const expect = chai.expect;
import { expect } from "chai";
const dots = ["..", "..", ".."];
const basePath = path.join(__filename, ...dots);
const snapshotsFolderPath = path.join(basePath, "src/formatter/snapshots");
function get_options(testFolderPath: string) {
const options: FormatterOptions = {
maxEmptyLines: 2,
denseFunctionParameters: false,
};
const optionsPath = path.join(testFolderPath, "config.json");
function normalizeLineEndings(str: string) {
return str.replace(/\r?\n/g, "\n");
}
const defaultOptions: FormatterOptions = {
maxEmptyLines: 2,
denseFunctionParameters: false,
spacesBeforeEndOfLineComment: 1,
};
function get_options(folder: fs.Dirent) {
const optionsPath = path.join(folder.path, folder.name, "config.json");
if (fs.existsSync(optionsPath)) {
const file = fs.readFileSync(optionsPath).toString();
const config = JSON.parse(file);
return { ...options, ...config } as FormatterOptions;
return { ...defaultOptions, ...config } as FormatterOptions;
}
return options;
return defaultOptions;
}
function set_content(content: string) {
return vscode.workspace
.openTextDocument()
.then((doc) => vscode.window.showTextDocument(doc))
.then((editor) => {
const editBuilder = (textEdit) => {
textEdit.insert(new vscode.Position(0, 0), String(content));
};
return editor
.edit(editBuilder, {
undoStopBefore: true,
undoStopAfter: false,
})
.then(() => editor);
});
}
function build_config(lines: string[]) {
try {
return JSON.parse(lines.join("\n"));
} catch (e) {
return {};
}
}
class TestLines {
config: string[] = [];
in: string[] = [];
out: string[] = [];
parse(_config) {
const config = { ...defaultOptions, ..._config, ...build_config(this.config) };
const test: Test = {
in: this.in.join("\n"),
out: this.out.join("\n"),
config: config,
};
if (test.out === "") {
test.out = this.in.join("\n");
}
if (!config.strictTrailingNewlines) {
test.in = test.in.trimEnd();
test.out = test.out.trimEnd();
}
return test;
}
}
interface Test {
config?: FormatterOptions;
in: string;
out: string;
}
const CONFIG_ALL = "# --- CONFIG ALL ---";
const CONFIG = "# --- CONFIG ---";
const IN = "# --- IN ---";
const OUT = "# --- OUT ---";
const END = "# --- END ---";
const MODES = [CONFIG_ALL, CONFIG, IN, OUT, END];
function parse_test_file(content: string): Test[] {
let defaultConfig = null;
let defaultConfigString: string[] = [];
const tests: Test[] = [];
let mode = null;
let test = new TestLines();
for (const _line of content.split("\n")) {
const line = _line.trim();
if (MODES.includes(line)) {
if (line === CONFIG || line === IN) {
if (test.in.length !== 0) {
tests.push(test.parse(defaultConfig));
test = new TestLines();
}
}
if (defaultConfigString.length !== 0) {
defaultConfig = build_config(defaultConfigString);
defaultConfigString = [];
}
mode = line;
continue;
}
if (mode === CONFIG_ALL) defaultConfigString.push(line);
if (mode === CONFIG) test.config.push(line);
if (mode === IN) test.in.push(line);
if (mode === OUT) test.out.push(line);
}
if (test.in.length !== 0) {
tests.push(test.parse(defaultConfig));
}
return tests;
}
suite("GDScript Formatter Tests", () => {
// Search for all folders in the snapshots folder and run a test for each
// comparing the output of the formatter with the expected output.
// To add a new test, create a new folder in the snapshots folder
// and add two files, `in.gd` and `out.gd` for the input and expected output.
const snapshotsFolderPath = path.join(basePath, "src/formatter/snapshots");
const testFolders = fs.readdirSync(snapshotsFolderPath);
const testFiles = fs.readdirSync(snapshotsFolderPath, { withFileTypes: true, recursive: true });
// biome-ignore lint/complexity/noForEach: <explanation>
testFolders.forEach((testFolder) => {
const testFolderPath = path.join(snapshotsFolderPath, testFolder);
if (fs.statSync(testFolderPath).isDirectory()) {
test(`Snapshot Test: ${testFolder}`, async () => {
const uriIn = vscode.Uri.file(path.join(testFolderPath, "in.gd"));
const uriOut = vscode.Uri.file(path.join(testFolderPath, "out.gd"));
teardown(async () => {
await vscode.commands.executeCommand("workbench.action.closeAllEditors");
});
const documentIn = await vscode.workspace.openTextDocument(uriIn);
const documentOut = await vscode.workspace.openTextDocument(uriOut);
for (const file of testFiles.filter((f) => f.isFile())) {
if (["in.gd", "out.gd"].includes(file.name) || !file.name.endsWith(".gd")) {
continue;
}
test(`Snapshot Test: ${file.name}`, async () => {
const uri = vscode.Uri.file(path.join(snapshotsFolderPath, file.name));
const inDoc = await vscode.workspace.openTextDocument(uri);
const text = inDoc.getText();
const options = get_options(testFolderPath);
const edits = format_document(documentIn, options);
for (const test of parse_test_file(text)) {
const editor = await set_content(test.in);
const document = editor.document;
const edits = format_document(document, test.config);
// Apply the formatting edits
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.set(uriIn, edits);
workspaceEdit.set(document.uri, edits);
await vscode.workspace.applyEdit(workspaceEdit);
// Compare the result with the expected output
expect(documentIn.getText().replace("\r\n", "\n")).to.equal(
documentOut.getText().replace("\r\n", "\n"),
);
});
const actual = normalizeLineEndings(document.getText());
const expected = normalizeLineEndings(test.out);
expect(actual).to.equal(expected);
}
});
}
for (const folder of testFiles.filter((f) => f.isDirectory())) {
const pathIn = path.join(folder.path, folder.name, "in.gd");
const pathOut = path.join(folder.path, folder.name, "out.gd");
if (!(fs.existsSync(pathIn) && fs.existsSync(pathOut))) {
continue;
}
});
test(`Snapshot Pair Test: ${folder.name}`, async () => {
const uriIn = vscode.Uri.file(path.join(folder.path, folder.name, "in.gd"));
const uriOut = vscode.Uri.file(path.join(folder.path, folder.name, "out.gd"));
const documentIn = await vscode.workspace.openTextDocument(uriIn);
const documentOut = await vscode.workspace.openTextDocument(uriOut);
const options = get_options(folder);
const edits = format_document(documentIn, options);
// Apply the formatting edits
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.set(uriIn, edits);
await vscode.workspace.applyEdit(workspaceEdit);
// Compare the result with the expected output
const actual = normalizeLineEndings(documentIn.getText());
const expected = normalizeLineEndings(documentOut.getText());
expect(actual).to.equal(expected);
});
}
});

View File

@@ -0,0 +1,101 @@
## An `IN` block is fed into the formatter and the output is compared to the `OUT` block
```
# --- IN ---
var a = 10
# --- OUT ---
var a = 10
```
## Trailing newlines in `IN` and `OUT` blocks is automatically removed
```
# --- IN ---
var a = 10
# --- OUT ---
var a = 10
# --- IN ---
var b = 'ten'
# --- OUT ---
var b = 'ten'
```
## An `IN` block by itself will be reused at the `OUT` target
Many test cases can simply be expressed as "do not change this":
```
# --- IN ---
var a = """ {
level_file: '%s',
md5_hash: %s,
}
"""
```
## Formatter and test harness options can be controlled with `CONFIG` blocks
This test will fail because `strictTrailingNewlines: true` disables trailing newline removal.
```
# --- CONFIG ---
{"strictTrailingNewlines": true}
# --- IN ---
var a = 10
# --- OUT ---
var a = 10
```
## `CONFIG ALL` set the default options moving forward, and `END` blocks allow additional layout flexibility
```
# --- CONFIG ALL ---
{"strictTrailingNewlines": true}
# --- IN ---
var a = 10
# --- OUT ---
var a = 10
# --- END ---
# anything I want goes here
# --- IN ---
var b = 'ten'
# --- OUT ---
var b = 'ten'
```
## `CONFIG` blocks override `CONFIG ALL`, and the configs are merged for a given test
This test will pass, because the second test has a `CONFIG` that overrides the `CONFIG ALL` at the top.
```
# --- CONFIG ALL ---
{"strictTrailingNewlines": true}
# --- IN ---
var a = 10
# --- OUT ---
var a = 10
# --- END ---
# anything I want goes here
# --- CONFIG ---
{"strictTrailingNewlines": false}
# --- IN ---
var b = 'ten'
# --- OUT ---
var b = 'ten'
# --- IN ---
var c = true
# --- OUT ---
var c = true
```

View File

@@ -1,15 +0,0 @@
func f():
# arithmetic
x += 1
x -= 1
x *= 1
x /= 1
x %= 1
# bitwise
x |= 1
x &= 1
x ~= 1
x /= 1
x >>= 1
x <<= 1

View File

@@ -1,15 +0,0 @@
func f():
# arithmetic
x += 1
x -= 1
x *= 1
x /= 1
x %= 1
# bitwise
x |= 1
x &= 1
x ~= 1
x /= 1
x >>= 1
x <<= 1

View File

@@ -1,5 +0,0 @@
func f():
collision_mask = 1 << 1 | 1 << 3
collision_mask = 1 << 1 & 1 << 3
collision_mask = ~1
collision_mask = 1 ^ ~ 1

View File

@@ -1,5 +0,0 @@
func f():
collision_mask = 1 << 1 | 1 << 3
collision_mask = 1 << 1 & 1 << 3
collision_mask = ~1
collision_mask = 1 ^ ~1

View File

@@ -0,0 +1,5 @@
# --- IN ---
func handleDeath() -> void:
var signalConnections: Array[Dictionary] = self.get_incoming_connections()
for connection in signalConnections:
connection.signal.disconnect(connection.callable)

View File

@@ -0,0 +1,25 @@
# --- IN ---
func test():
# The following floating-point notations are all valid:
print(is_equal_approx(123., 123))
print(is_equal_approx(.123, 0.123))
print(is_equal_approx(.123e4, 1230))
print(is_equal_approx(123.e4, 1.23e6))
print(is_equal_approx(.123e-1, 0.0123))
print(is_equal_approx(123.e-1, 12.3))
# Same as above, but with negative numbers.
print(is_equal_approx(-123., -123))
print(is_equal_approx(-.123, -0.123))
print(is_equal_approx(-.123e4, -1230))
print(is_equal_approx(-123.e4, -1.23e6))
print(is_equal_approx(-.123e-1, -0.0123))
print(is_equal_approx(-123.e-1, -12.3))
# Same as above, but with explicit positive numbers (which is redundant).
print(is_equal_approx(+123., +123))
print(is_equal_approx(+.123, +0.123))
print(is_equal_approx(+.123e4, +1230))
print(is_equal_approx(+123.e4, +1.23e6))
print(is_equal_approx(+.123e-1, +0.0123))
print(is_equal_approx(+123.e-1, +12.3))

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

@@ -0,0 +1,4 @@
# --- IN ---
poll_animation.on_complete(func() -> void:
highlight.visible = false
)

View File

@@ -0,0 +1,17 @@
# --- IN ---
func dump() -> String:
return """
{
level_file: '%s',
md5_hash: %s,
text: '%s',
level_size: %s,
world_pos: %s,
preview_size: %s,
preview_pos: %s,
preview_texture: %s,
explorer_layer: %s,
connections: %s,
test {test},
}
"""

View File

@@ -1,3 +0,0 @@
{
"maxEmptyLines": 1
}

View File

@@ -1,27 +0,0 @@
class Test:
func _ready():
pass
func test():
pass
# comments
func with_comments():
pass

View File

@@ -1,14 +0,0 @@
class Test:
func _ready():
pass
func test():
pass
# comments
func with_comments():
pass

View File

@@ -1,28 +0,0 @@
class Test:
func _ready():
pass
func test():
pass
# comments
func with_comments():
pass

View File

@@ -1,20 +0,0 @@
class Test:
func _ready():
pass
func test():
pass
# comments
func with_comments():
pass

View File

@@ -0,0 +1,72 @@
# --- IN ---
func test():
pass
# --- OUT ---
func test():
pass
# --- IN ---
class Test:
func _ready():
pass
# --- OUT ---
class Test:
func _ready():
pass
# --- IN ---
func test(): # with comment
pass
# --- OUT ---
func test(): # with comment
pass
# --- IN ---
class Test: # with comment
func _ready(): # with comment
pass
# --- OUT ---
class Test: # with comment
func _ready(): # with comment
pass
# --- CONFIG ---
{"maxEmptyLines": 1}
# --- IN ---
func a():
pass
func b():
pass
# --- OUT ---
func a():
pass
func b():
pass
# --- CONFIG ---
{"maxEmptyLines": 2}
# --- IN ---
func a():
pass
func b():
pass
# --- OUT ---
func a():
pass
func b():
pass

View File

@@ -0,0 +1,23 @@
# --- IN ---
var test1 := deg_to_rad(
-90
)
# --- IN ---
var test2 := Vector2(
-0.0,
1.0
)
# --- IN ---
var test3 := Vector3(
0.0,
-0.0,
0.0
)
# --- IN ---
func get_audio_compensation() -> float:
return AudioServer.get_time_since_last_mix() \
- AudioServer.get_output_latency() \
+ (1 / Engine.get_frames_per_second()) * 2

View File

@@ -44,6 +44,9 @@ var a = $Child/%Unique/ChildOfUnique
var a = %Unique
var a = %Unique/Child
var a = %Unique/%UniqueChild
var a = %"Unique"
var a = %'Unique/Child'
var a = %'Unique/%UniqueChild'
var a = $"%Unique"
var a = get_node("%Unique")

View File

@@ -44,6 +44,9 @@ var a = $Child/%Unique/ChildOfUnique
var a = %Unique
var a = %Unique/Child
var a = %Unique/%UniqueChild
var a = %"Unique"
var a = %'Unique/Child'
var a = %'Unique/%UniqueChild'
var a = $"%Unique"
var a = get_node("%Unique")

View File

@@ -0,0 +1,33 @@
# --- IN ---
func f():
# arithmetic
x += 1
x -= 1
x *= 1
x /= 1
x %= 1
x = 2 ** 2
x = 2 * -1
x **= 2
# bitwise
x |= 1
x &= 1
x ^= 1
x ~= 1
x = ~1
x /= 1
x >>= 1
x <<= 1
x = 1 << 1 | 1 >> 3
x = 1 << 1 & 1 >> 3
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,6 @@
# --- IN ---
var a = 1e-6
var b = 4e-09
var c = 58.1e-10
var d = 58.1e+10
var e = 9.732e-06

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

@@ -0,0 +1,14 @@
# --- CONFIG ---
{"strictTrailingNewlines": true}
# --- IN ---
var a = 10
# --- OUT ---
var a = 10
# --- END ---
# anything I want goes here
# --- IN ---
var b = 'ten'
# --- OUT ---
var b = 'ten'

View File

@@ -4,7 +4,7 @@ import * as fs from "node:fs";
import * as vsctm from "vscode-textmate";
import * as oniguruma from "vscode-oniguruma";
import { keywords, symbols } from "./symbols";
import { get_configuration, get_extension_uri, createLogger } from "../utils";
import { get_configuration, get_extension_uri, createLogger, is_debug_mode } from "../utils";
const log = createLogger("formatter.tm");
@@ -50,17 +50,20 @@ interface Token {
param?: boolean;
string?: boolean;
skip?: boolean;
identifier?: boolean;
}
export interface FormatterOptions {
maxEmptyLines: 1 | 2;
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;
@@ -73,6 +76,13 @@ function parse_token(token: Token) {
if (token.scopes.includes("meta.function.parameters.gdscript")) {
token.param = true;
}
if (token.scopes[0].includes("constant.numeric")) {
token.type = "literal";
return;
}
if (token.value.match(/[A-Za-z_]\w+/)) {
token.identifier = true;
}
if (token.scopes.includes("meta.literal.nodepath.gdscript")) {
token.skip = true;
token.type = "nodepath";
@@ -111,6 +121,10 @@ function parse_token(token: Token) {
token.type = "variable";
return;
}
if (token.scopes.includes("comment.line.number-sign.gdscript")) {
token.type = "comment";
return;
}
}
function between(tokens: Token[], current: number, options: FormatterOptions) {
@@ -123,17 +137,22 @@ 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 "";
if (prev === ".") {
if (nextToken?.type === "symbol") return " ";
return "";
}
if (next === ".") return "";
if (nextToken.param) {
if (options.denseFunctionParameters) {
if (prev === "-") {
if (prev === "-" || prev === "+") {
if (tokens[current - 2]?.value === "=") return "";
if (["keyword", "symbol"].includes(tokens[current - 2].type)) {
if (["keyword", "symbol"].includes(tokens[current - 2]?.type)) {
return "";
}
if ([",", "("].includes(tokens[current - 2]?.value)) {
@@ -168,13 +187,16 @@ function between(tokens: Token[], current: number, options: FormatterOptions) {
}
if (prev === "@") return "";
if (prev === "-") {
if (["keyword", "symbol"].includes(tokens[current - 2].type)) {
if (prev === "-" || prev === "+") {
if (next === "(") 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 "";
@@ -232,6 +254,7 @@ export function format_document(document: TextDocument, _options?: FormatterOpti
const options = _options ?? get_formatter_options();
let lastToken = null;
let lineTokens: vsctm.ITokenizeLineResult = null;
let onlyEmptyLinesSoFar = true;
let emptyLineCount = 0;
@@ -259,7 +282,11 @@ export function format_document(document: TextDocument, _options?: FormatterOpti
// delete consecutive empty lines
if (emptyLineCount) {
for (let i = emptyLineCount - options.maxEmptyLines; i > 0; i--) {
let maxEmptyLines = options.maxEmptyLines;
if (lastToken === ":") {
maxEmptyLines = 0;
}
for (let i = emptyLineCount - maxEmptyLines; i > 0; i--) {
edits.push(TextEdit.delete(document.lineAt(lineNum - i).rangeIncludingLineBreak));
}
emptyLineCount = 0;
@@ -284,7 +311,7 @@ export function format_document(document: TextDocument, _options?: FormatterOpti
const tokens: Token[] = [];
for (const t of lineTokens.tokens) {
const token: Token = {
scopes: t.scopes,
scopes: [t.scopes.join(" "), ...t.scopes],
original: line.text.slice(t.startIndex, t.endIndex),
value: line.text.slice(t.startIndex, t.endIndex).trim(),
};
@@ -296,12 +323,19 @@ export function format_document(document: TextDocument, _options?: FormatterOpti
tokens.push(token);
}
for (let i = 0; i < tokens.length; i++) {
log.debug(i, tokens[i].value, tokens[i]);
if (i > 0 && tokens[i - 1].string === true && tokens[i].string === true) {
if (is_debug_mode()) log.debug(i, tokens[i].value, tokens[i]);
if (i === 0 && tokens[i].string) {
// leading whitespace is already accounted for
nextLine += tokens[i].original.trimStart();
} else if (i > 0 && tokens[i - 1].string && tokens[i].string) {
nextLine += tokens[i].original;
} else {
nextLine += between(tokens, i, options) + tokens[i].value.trim();
}
if (tokens[i].type !== "comment") {
lastToken = tokens[i].value;
}
}
edits.push(TextEdit.replace(line.range, nextLine));

View File

@@ -1,34 +1,40 @@
import * as vscode from "vscode";
import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient";
import {
createLogger,
get_configuration,
get_free_port,
get_project_dir,
get_project_version,
set_context,
register_command,
set_configuration,
createLogger,
set_context,
verify_godot_version,
} from "../utils";
import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
import { subProcess, killSubProcesses } from "../utils/subspawn";
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 {
INITIALIZING,
INITIALIZING_LSP,
PENDING,
PENDING_LSP,
DISCONNECTED,
CONNECTED,
RETRYING,
export enum ManagerStatus {
INITIALIZING = 0,
INITIALIZING_LSP = 1,
PENDING = 2,
PENDING_LSP = 3,
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;
@@ -38,10 +44,7 @@ export class ClientConnectionManager {
private connectedVersion = "";
constructor(private context: vscode.ExtensionContext) {
this.context = context;
this.client = new GDScriptLanguageClient(context);
this.client.watch_status(this.on_client_status_changed.bind(this));
this.create_new_client();
setInterval(() => {
this.retry_callback();
@@ -60,7 +63,7 @@ export class ClientConnectionManager {
this.start_language_server();
this.reconnectionAttempts = 0;
this.target = TargetLSP.HEADLESS;
this.client.connect_to_server(this.target);
this.client.connect(this.target);
}),
register_command("stopLanguageServer", this.stop_language_server.bind(this)),
register_command("checkStatus", this.on_status_item_click.bind(this)),
@@ -70,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;
@@ -81,7 +92,7 @@ export class ClientConnectionManager {
}
this.reconnectionAttempts = 0;
this.client.connect_to_server(this.target);
this.client.connect(this.target);
}
private stop_language_server() {
@@ -126,16 +137,18 @@ export class ClientConnectionManager {
if (result.version[2] < minimumVersion) {
const message = `Cannot launch headless LSP: Headless LSP mode is only available on v${targetVersion} or newer, but the specified Godot executable is v${result.version}.`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore").then(item => {
if (item === "Select Godot executable") {
select_godot_executable(settingName);
} else if (item === "Open Settings") {
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
} else if (item === "Disable Headless LSP") {
set_configuration("lsp.headless", false);
prompt_for_reload();
}
});
vscode.window
.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore")
.then((item) => {
if (item === "Select Godot executable") {
select_godot_executable(settingName);
} else if (item === "Open Settings") {
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
} else if (item === "Disable Headless LSP") {
set_configuration("lsp.headless", false);
prompt_for_reload();
}
});
return;
}
@@ -197,7 +210,7 @@ export class ClientConnectionManager {
if (this.target === TargetLSP.HEADLESS) {
options = ["Restart LSP", ...options];
}
vscode.window.showInformationMessage(message, ...options).then(item => {
vscode.window.showInformationMessage(message, ...options).then((item) => {
if (item === "Restart LSP") {
this.connect_to_language_server();
}
@@ -210,6 +223,9 @@ export class ClientConnectionManager {
case ManagerStatus.RETRYING:
this.show_retrying_prompt();
break;
case ManagerStatus.WRONG_WORKSPACE:
this.retry_connect_client();
break;
}
}
@@ -238,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:
@@ -252,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;
@@ -267,12 +287,14 @@ export class ClientConnectionManager {
this.reconnectionAttempts = 0;
set_context("connectedToLSP", true);
this.status = ManagerStatus.CONNECTED;
if (!this.client.started) {
this.context.subscriptions.push(this.client.start());
if (this.client.needsStart()) {
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;
@@ -284,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();
}
@@ -303,7 +330,7 @@ export class ClientConnectionManager {
const maxAttempts = get_configuration("lsp.autoReconnect.attempts");
if (autoRetry && this.reconnectionAttempts <= maxAttempts - 1) {
this.reconnectionAttempts++;
this.client.connect_to_server(this.target);
this.client.connect(this.target);
this.retry = true;
return;
}
@@ -324,7 +351,7 @@ export class ClientConnectionManager {
options = ["Open workspace with Godot Editor", ...options];
}
vscode.window.showErrorMessage(message, ...options).then(item => {
vscode.window.showErrorMessage(message, ...options).then((item) => {
if (item === "Retry") {
this.connect_to_language_server();
}

View File

@@ -1,92 +1,134 @@
import EventEmitter from "node:events";
import * as path from "node:path";
import * as vscode from "vscode";
import { LanguageClient, NotificationMessage, RequestMessage, ResponseMessage } from "vscode-languageclient/node";
import { EventEmitter } from "events";
import { get_configuration, createLogger } from "../utils";
import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
import {
LanguageClient,
MessageSignature,
type LanguageClientOptions,
type NotificationMessage,
type RequestMessage,
type ResponseMessage,
type ServerOptions,
} from "vscode-languageclient/node";
import { globals } from "../extension";
import { createLogger, get_configuration, get_project_dir } from "../utils";
import { MessageIO } from "./MessageIO";
const log = createLogger("lsp.client", { output: "Godot LSP" });
export enum ClientStatus {
PENDING,
DISCONNECTED,
CONNECTED,
PENDING = 0,
DISCONNECTED = 1,
CONNECTED = 2,
REJECTED = 3,
}
export enum TargetLSP {
HEADLESS,
EDITOR,
HEADLESS = 0,
EDITOR = 1,
}
const CUSTOM_MESSAGE = "gdscript_client/";
export type Target = {
host: string;
port: number;
type: TargetLSP;
};
type HoverResult = {
contents: {
kind: string;
value: string;
};
range: {
end: {
character: number;
line: number;
};
start: {
character: number;
line: number;
};
};
};
type HoverResponseMesssage = {
id: number;
jsonrpc: string;
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 readonly io: MessageIO = (get_configuration("lsp.serverProtocol") === "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
private _status_changed_callbacks: ((v: ClientStatus) => void)[] = [];
private _initialize_request: Message = null;
private messageHandler: MessageHandler = null;
public io: MessageIO = new MessageIO();
public target: TargetLSP = TargetLSP.EDITOR;
public port = -1;
public lastPortTried = -1;
public sentMessages = new Map();
public lastSymbolHovered = "";
private rejected = false;
private _started = false;
public get started(): boolean { return this._started; }
events = new EventEmitter();
private _status: ClientStatus;
public get status(): ClientStatus { return this._status; }
public set status(v: ClientStatus) {
if (this._status !== v) {
this._status = v;
for (const callback of this._status_changed_callbacks) {
callback(v);
}
}
this._status = v;
this.events.emit("status", this._status);
}
public watch_status(callback: (v: ClientStatus) => void) {
if (this._status_changed_callbacks.indexOf(callback) === -1) {
this._status_changed_callbacks.push(callback);
}
}
constructor() {
const serverOptions: ServerOptions = () => {
return new Promise((resolve, reject) => {
resolve({ reader: this.io.reader, writer: this.io.writer });
});
};
constructor(private context: vscode.ExtensionContext) {
super(
"GDScriptLanguageClient",
() => {
return new Promise((resolve, reject) => {
resolve({ reader: new MessageIOReader(this.io), writer: new MessageIOWriter(this.io) });
});
},
{
// Register the server for plain text documents
documentSelector: [
{ scheme: "file", language: "gdscript" },
{ scheme: "untitled", language: "gdscript" },
],
synchronize: {
// Notify the server about file changes to '.gd files contain in the workspace
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
},
}
);
const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: "file", language: "gdscript" },
{ scheme: "untitled", language: "gdscript" },
],
};
super("GDScriptLanguageClient", serverOptions, clientOptions);
this.status = ClientStatus.PENDING;
this.io.on("disconnected", this.on_disconnected.bind(this));
this.io.on("connected", this.on_connected.bind(this));
this.io.on("message", this.on_message.bind(this));
this.io.on("send_message", this.on_send_message.bind(this));
this.messageHandler = new MessageHandler(this.io);
this.io.on("disconnected", this.on_disconnected.bind(this));
this.io.requestFilter = this.request_filter.bind(this);
this.io.responseFilter = this.response_filter.bind(this);
this.io.notificationFilter = this.notification_filter.bind(this);
}
public async list_classes() {
await globals.docsProvider.list_native_classes();
}
connect_to_server(target: TargetLSP = TargetLSP.EDITOR) {
connect(target: TargetLSP = TargetLSP.EDITOR) {
this.rejected = false;
this.target = target;
this.status = ClientStatus.PENDING;
@@ -106,84 +148,179 @@ export default class GDScriptLanguageClient extends LanguageClient {
const host = get_configuration("lsp.serverHost");
log.info(`attempting to connect to LSP at ${host}:${port}`);
this.io.connect_to_language_server(host, port);
this.io.connect(host, port);
}
start() {
this._started = true;
return super.start();
}
private on_send_message(message: RequestMessage) {
this.sentMessages.set(message.id, message);
if (message.method === "initialize") {
this._initialize_request = message;
async send_request<R>(method: string, params): Promise<R> {
try {
return this.sendRequest(method, params);
} catch {
log.warn("sending request failed!");
}
}
private on_message(message: ResponseMessage | NotificationMessage) {
const msgString = JSON.stringify(message);
// This is a dirty hack to fix the language server sending us
// invalid file URIs
// This should be forward-compatible, meaning that it will work
// with the current broken version, AND the fixed future version.
const match = msgString.match(/"target":"file:\/\/[^\/][^"]*"/);
if (match) {
const count = (message["result"] as Array<object>).length;
for (let i = 0; i < count; i++) {
const x: string = message["result"][i]["target"];
message["result"][i]["target"] = x.replace("file://", "file:///");
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
);
}
if ("method" in message && message.method === "gdscript/capabilities") {
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 === "textDocument/didSave") {
// return false;
// }
// if (message.method === "textDocument/willSaveWaitUntil") {
// return false;
// }
if (message.method === "workspace/didChangeWatchedFiles") {
return false;
}
if (message.method === "workspace/symbol") {
// Fixed on server side since Godot 4.5
return false;
}
return message;
}
private response_filter(message: ResponseMessage) {
const sentMessage = this.sentMessages.get(message.id);
if (sentMessage?.method === "textDocument/hover") {
// fix markdown contents
let value: string = (message as HoverResponseMesssage).result.contents.value;
if (value) {
// this is a dirty hack to fix language server sending us prerendered
// markdown but not correctly stripping leading #'s, leading to
// docstrings being displayed as titles
value = value.replace(/\n[#]+/g, "\n");
// fix bbcode line breaks
value = value.replaceAll("`br`", "\n\n");
// fix bbcode code boxes
value = value.replace("`codeblocks`", "");
value = value.replace("`/codeblocks`", "");
value = value.replace("`gdscript`", "\nGDScript:\n```gdscript");
value = value.replace("`/gdscript`", "```");
value = value.replace("`csharp`", "\nC#:\n```csharp");
value = value.replace("`/csharp`", "```");
(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);
}
if ("id" in message) {
const sentMessage = this.sentMessages.get(message.id);
if (sentMessage && sentMessage.method === "textDocument/hover") {
// fix markdown contents
let value: string = message.result["contents"]?.value;
if (value) {
// this is a dirty hack to fix language server sending us prerendered
// markdown but not correctly stripping leading #'s, leading to
// docstrings being displayed as titles
value = value.replace(/\n[#]+/g, "\n");
// if (message.method === "textDocument/publishDiagnostics") {
// for (const diagnostic of message.params.diagnostics) {
// if (diagnostic.code === 6) {
// log.debug("UNUSED_SIGNAL", diagnostic);
// return;
// }
// if (diagnostic.code === 2) {
// log.debug("UNUSED_VARIABLE", diagnostic);
// return;
// }
// }
// }
// fix bbcode line breaks
value = value.replaceAll("`br`", "\n\n");
// fix bbcode code boxes
value = value.replace("`codeblocks`", "");
value = value.replace("`/codeblocks`", "");
value = value.replace("`gdscript`", "\nGDScript:\n```gdscript");
value = value.replace("`/gdscript`", "```");
value = value.replace("`csharp`", "\nC#:\n```csharp");
value = value.replace("`/csharp`", "```");
message.result["contents"].value = value;
}
}
}
this.messageHandler.on_message(message);
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 = await this.sendRequest("textDocument/hover", params);
return this.parse_hover_response(response);
const response = await this.send_request("textDocument/hover", params);
return this.parse_hover_result(response as HoverResult);
}
private parse_hover_response(message) {
const contents = message["contents"];
private parse_hover_result(message: HoverResult) {
const contents = message.contents;
let decl: string;
if (Array.isArray(contents)) {
@@ -212,16 +349,17 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
private on_connected() {
if (this._initialize_request) {
this.io.writer.write(this._initialize_request);
}
this.status = ClientStatus.CONNECTED;
const host = get_configuration("lsp.serverHost");
log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
}
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");
@@ -232,7 +370,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
log.info(`attempting to connect to LSP at ${host}:${port}`);
this.lastPortTried = port;
this.io.connect_to_language_server(host, port);
this.io.connect(host, port);
return;
}
}
@@ -240,39 +378,3 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.status = ClientStatus.DISCONNECTED;
}
}
class MessageHandler extends EventEmitter {
private io: MessageIO = null;
constructor(io: MessageIO) {
super();
this.io = io;
}
// changeWorkspace(params: { path: string }) {
// vscode.window.showErrorMessage("The GDScript language server can't work properly!\nThe open workspace is different from the editor's.", 'Reload', 'Ignore').then(item => {
// if (item == "Reload") {
// let folderUrl = vscode.Uri.file(params.path);
// vscode.commands.executeCommand('vscode.openFolder', folderUrl, false);
// }
// });
// }
on_message(message: any) {
// FIXME: Hot fix VSCode 1.42 hover position
if (message && message.result && message.result.range && message.result.contents) {
message.result.range = undefined;
}
// What does this do?
if (message && message.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) {
const method = (message.method as string).substring(CUSTOM_MESSAGE.length, message.method.length);
if (this[method]) {
const ret = this[method](message.params);
if (ret) {
this.io.writer.write(ret);
}
}
}
}
}

View File

@@ -3,35 +3,41 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { createLogger } from "../utils";
const log = createLogger("lsp.buf");
const DefaultSize: number = 8192;
const CR: number = Buffer.from('\r', 'ascii')[0];
const LF: number = Buffer.from('\n', 'ascii')[0];
const CRLF: string = '\r\n';
const CR: number = Buffer.from("\r", "ascii")[0];
const LF: number = Buffer.from("\n", "ascii")[0];
const CRLF: string = "\r\n";
type Headers = { [key: string]: string };
export default class MessageBuffer {
private encoding: BufferEncoding = "utf8";
private index = 0;
private buffer: Buffer = Buffer.allocUnsafe(DefaultSize);
private encoding: BufferEncoding;
private index: number;
private buffer: Buffer;
private nextMessageLength: number;
private messageToken: number;
private partialMessageTimer: NodeJS.Timeout | undefined;
private _partialMessageTimeout = 10000;
constructor(encoding: string = 'utf8') {
this.encoding = encoding as BufferEncoding;
this.index = 0;
this.buffer = Buffer.allocUnsafe(DefaultSize);
}
constructor(private reader) {}
public append(chunk: Buffer | String): void {
var toAppend: Buffer = <Buffer>chunk;
if (typeof (chunk) === 'string') {
var str = <string>chunk;
var bufferLen = Buffer.byteLength(str, this.encoding);
public append(chunk: Buffer | string): void {
let toAppend: Buffer = <Buffer>chunk;
if (typeof chunk === "string") {
const str = <string>chunk;
const bufferLen = Buffer.byteLength(str, this.encoding);
toAppend = Buffer.allocUnsafe(bufferLen);
toAppend.write(str, 0, bufferLen, this.encoding);
}
if (this.buffer.length - this.index >= toAppend.length) {
toAppend.copy(this.buffer, this.index, 0, toAppend.length);
} else {
var newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
const newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
if (this.index === 0) {
this.buffer = Buffer.allocUnsafe(newSize);
toAppend.copy(this.buffer, 0, 0, toAppend.length);
@@ -42,29 +48,34 @@ export default class MessageBuffer {
this.index += toAppend.length;
}
public tryReadHeaders(): { [key: string]: string; } | undefined {
let result: { [key: string]: string; } | undefined = undefined;
public tryReadHeaders(): Headers | undefined {
let current = 0;
while (current + 3 < this.index && (this.buffer[current] !== CR || this.buffer[current + 1] !== LF || this.buffer[current + 2] !== CR || this.buffer[current + 3] !== LF)) {
while (
current + 3 < this.index &&
(this.buffer[current] !== CR ||
this.buffer[current + 1] !== LF ||
this.buffer[current + 2] !== CR ||
this.buffer[current + 3] !== LF)
) {
current++;
}
// No header / body separator found (e.g CRLFCRLF)
if (current + 3 >= this.index) {
return result;
return undefined;
}
result = Object.create(null);
let headers = this.buffer.toString('ascii', 0, current).split(CRLF);
headers.forEach((header) => {
let index: number = header.indexOf(':');
const result = Object.create(null);
const headers = this.buffer.toString("ascii", 0, current).split(CRLF);
for (const header of headers) {
const index: number = header.indexOf(":");
if (index === -1) {
throw new Error('Message header must separate key and value using :');
throw new Error("Message header must separate key and value using :");
}
let key = header.substr(0, index);
let value = header.substr(index + 1).trim();
result![key] = value;
});
const key = header.substr(0, index);
const value = header.substr(index + 1).trim();
result[key] = value;
}
let nextStart = current + 4;
const nextStart = current + 4;
this.buffer = this.buffer.slice(nextStart);
this.index = this.index - nextStart;
return result;
@@ -74,14 +85,73 @@ export default class MessageBuffer {
if (this.index < length) {
return null;
}
let result = this.buffer.toString(this.encoding, 0, length);
let nextStart = length;
const result = this.buffer.toString(this.encoding, 0, length);
const nextStart = length;
this.buffer.copy(this.buffer, 0, nextStart);
this.index = this.index - nextStart;
return result;
}
public get numberOfBytes(): number {
return this.index;
public ready() {
if (this.nextMessageLength === -1) {
const headers = this.tryReadHeaders();
if (!headers) {
return;
}
const contentLength = headers["Content-Length"];
if (!contentLength) {
log.warn("Header must provide a Content-Length property.");
return;
}
const length = Number.parseInt(contentLength);
if (Number.isNaN(length)) {
log.warn("Content-Length value must be a number.");
return;
}
this.nextMessageLength = length;
}
const msg = this.tryReadContent(this.nextMessageLength);
if (!msg) {
log.warn("haven't recieved full message");
this.setPartialMessageTimer();
return;
}
this.clearPartialMessageTimer();
this.nextMessageLength = -1;
this.messageToken++;
return msg;
}
public reset() {
this.nextMessageLength = -1;
this.messageToken = 0;
this.partialMessageTimer = undefined;
}
private clearPartialMessageTimer(): void {
if (this.partialMessageTimer) {
clearTimeout(this.partialMessageTimer);
this.partialMessageTimer = undefined;
}
}
private setPartialMessageTimer(): void {
this.clearPartialMessageTimer();
if (this._partialMessageTimeout <= 0) {
return;
}
this.partialMessageTimer = setTimeout(
(token, timeout) => {
this.partialMessageTimer = undefined;
if (token === this.messageToken) {
this.reader.firePartialMessage({ messageToken: token, waitingTime: timeout });
this.setPartialMessageTimer();
}
},
this._partialMessageTimeout,
this.messageToken,
this._partialMessageTimeout,
);
}
}

View File

@@ -1,16 +1,15 @@
import {
AbstractMessageReader,
MessageReader,
DataCallback,
Disposable,
RequestMessage,
ResponseMessage,
NotificationMessage,
type MessageReader,
type DataCallback,
type Disposable,
type RequestMessage,
type ResponseMessage,
type NotificationMessage,
AbstractMessageWriter,
MessageWriter
type MessageWriter,
} from "vscode-jsonrpc";
import { EventEmitter } from "events";
import { WebSocket, Data } from "ws";
import { EventEmitter } from "node:events";
import { Socket } from "net";
import MessageBuffer from "./MessageBuffer";
import { createLogger } from "../utils";
@@ -20,243 +19,132 @@ const log = createLogger("lsp.io", { output: "Godot LSP" });
export type Message = RequestMessage | ResponseMessage | NotificationMessage;
export class MessageIO extends EventEmitter {
reader: MessageIOReader = null;
writer: MessageIOWriter = null;
reader = new MessageIOReader(this);
writer = new MessageIOWriter(this);
public send_message(message: string) {
// virtual
}
requestFilter: (msg: RequestMessage) => RequestMessage | false = (msg) => msg;
responseFilter: (msg: ResponseMessage) => ResponseMessage | false = (msg) => msg;
notificationFilter: (msg: NotificationMessage) => NotificationMessage | false = (msg) => msg;
protected on_message(chunk: Data) {
const message = chunk.toString();
this.emit("data", message);
}
socket: Socket = null;
messageCache: string[] = [];
on_send_message(message: any) {
this.emit("send_message", message);
}
on_message_callback(message: any) {
this.emit("message", message);
}
async connect_to_language_server(host: string, port: number): Promise<void> {
// virtual
}
}
export class WebSocketMessageIO extends MessageIO {
private socket: WebSocket = null;
public send_message(message: string) {
if (this.socket) {
this.socket.send(message);
}
}
async connect_to_language_server(host: string, port: number): Promise<void> {
async connect(host: string, port: number): Promise<void> {
log.debug(`connecting to ${host}:${port}`);
return new Promise((resolve, reject) => {
this.socket = null;
const ws = new WebSocket(`ws://${host}:${port}`);
ws.on("open", () => { this.on_connected(ws); resolve(); });
ws.on("message", this.on_message.bind(this));
ws.on("error", this.on_disconnected.bind(this));
ws.on("close", this.on_disconnected.bind(this));
});
}
protected on_connected(socket: WebSocket) {
this.socket = socket;
this.emit("connected");
}
protected on_disconnected() {
this.socket = null;
this.emit("disconnected");
}
}
export class TCPMessageIO extends MessageIO {
private socket: Socket = null;
public send_message(message: string) {
if (this.socket) {
this.socket.write(message);
}
}
async connect_to_language_server(host: string, port: number): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = null;
const socket = new Socket();
socket.connect(port, host);
socket.on("connect", () => { this.on_connected(socket); resolve(); });
socket.on("data", this.on_message.bind(this));
socket.on("end", this.on_disconnected.bind(this));
socket.on("close", this.on_disconnected.bind(this));
socket.on("error", this.on_error.bind(this));
socket.on("connect", () => {
this.socket = socket;
while (this.messageCache.length > 0) {
const msg = this.messageCache.shift();
this.socket.write(msg);
}
this.emit("connected");
resolve();
});
socket.on("data", (chunk: Buffer) => {
this.emit("data", chunk);
});
// socket.on("end", this.on_disconnected.bind(this));
socket.on("error", () => {
this.socket = null;
this.emit("disconnected");
});
socket.on("close", () => {
this.socket = null;
this.emit("disconnected");
});
});
}
protected on_connected(socket: Socket) {
this.socket = socket;
this.emit("connected");
}
protected on_disconnected() {
this.socket = null;
this.emit("disconnected");
}
protected on_error(error) {
// TODO: handle errors?
write(message: string) {
if (this.socket) {
this.socket.write(message);
} else {
this.messageCache.push(message);
}
}
}
export class MessageIOReader extends AbstractMessageReader implements MessageReader {
private io: MessageIO;
private callback: DataCallback;
private buffer: MessageBuffer;
private nextMessageLength: number;
private messageToken: number;
private partialMessageTimer: NodeJS.Timeout | undefined;
private _partialMessageTimeout: number;
callback: DataCallback;
private buffer = new MessageBuffer(this);
public constructor(io: MessageIO, encoding: BufferEncoding = "utf8") {
constructor(public io: MessageIO) {
super();
this.io = io;
this.io.reader = this;
this.buffer = new MessageBuffer(encoding);
this._partialMessageTimeout = 10000;
}
public set partialMessageTimeout(timeout: number) {
this._partialMessageTimeout = timeout;
}
listen(callback: DataCallback): Disposable {
this.buffer.reset();
public get partialMessageTimeout(): number {
return this._partialMessageTimeout;
}
public listen(callback: DataCallback): Disposable {
this.nextMessageLength = -1;
this.messageToken = 0;
this.partialMessageTimer = undefined;
this.callback = callback;
this.io.on("data", this.onData.bind(this));
this.io.on("data", this.on_data.bind(this));
this.io.on("error", this.fireError.bind(this));
this.io.on("close", this.fireClose.bind(this));
return;
}
private onData(data: Buffer | string): void {
private on_data(data: Buffer | string): void {
this.buffer.append(data);
while (true) {
if (this.nextMessageLength === -1) {
const headers = this.buffer.tryReadHeaders();
if (!headers) {
return;
}
const contentLength = headers["Content-Length"];
if (!contentLength) {
throw new Error("Header must provide a Content-Length property.");
}
const length = parseInt(contentLength);
if (isNaN(length)) {
throw new Error("Content-Length value must be a number.");
}
this.nextMessageLength = length;
// Take the encoding form the header. For compatibility
// treat both utf-8 and utf8 as node utf8
}
var msg = this.buffer.tryReadContent(this.nextMessageLength);
if (msg === null) {
/** We haven't received the full message yet. */
this.setPartialMessageTimer();
const msg = this.buffer.ready();
if (!msg) {
return;
}
this.clearPartialMessageTimer();
this.nextMessageLength = -1;
this.messageToken++;
var json = JSON.parse(msg);
log.debug("rx:", json);
this.callback(json);
// callback
this.io.on_message_callback(json);
}
}
private clearPartialMessageTimer(): void {
if (this.partialMessageTimer) {
clearTimeout(this.partialMessageTimer);
this.partialMessageTimer = undefined;
}
}
private setPartialMessageTimer(): void {
this.clearPartialMessageTimer();
if (this._partialMessageTimeout <= 0) {
return;
}
this.partialMessageTimer = setTimeout((token, timeout) => {
this.partialMessageTimer = undefined;
if (token === this.messageToken) {
this.firePartialMessage({ messageToken: token, waitingTime: timeout });
this.setPartialMessageTimer();
const json = JSON.parse(msg);
// allow message to be modified
let modified: ResponseMessage | NotificationMessage | false;
if ("id" in json) {
modified = this.io.responseFilter(json);
} else if ("method" in json) {
modified = this.io.notificationFilter(json);
} else {
log.warn("rx [unhandled]:", json);
}
}, this._partialMessageTimeout, this.messageToken, this._partialMessageTimeout);
if (modified === false) {
log.debug("rx [discarded]:", json);
return;
}
log.debug("rx:", modified);
this.callback(json);
}
}
}
const ContentLength: string = "Content-Length: ";
const CRLF = "\r\n";
export class MessageIOWriter extends AbstractMessageWriter implements MessageWriter {
private io: MessageIO;
private encoding: BufferEncoding;
private errorCount: number;
public constructor(io: MessageIO, encoding: BufferEncoding = "utf8") {
constructor(public io: MessageIO) {
super();
this.io = io;
this.io.writer = this;
this.encoding = encoding as BufferEncoding;
this.errorCount = 0;
this.io.on("error", (error: any) => this.fireError(error));
this.io.on("close", () => this.fireClose());
}
public end(): void {
}
public write(msg: Message): Promise<void> {
if ((msg as RequestMessage).method === "didChangeWatchedFiles") {
async write(msg: RequestMessage) {
const modified = this.io.requestFilter(msg);
if (modified === false) {
log.debug("tx [discarded]:", msg);
return;
}
const json = JSON.stringify(msg);
const contentLength = Buffer.byteLength(json, this.encoding);
log.debug("tx:", modified);
const json = JSON.stringify(modified);
const headers: string[] = [
ContentLength, contentLength.toString(), CRLF,
CRLF
];
const contentLength = Buffer.byteLength(json, "utf-8").toString();
const message = `Content-Length: ${contentLength}\r\n\r\n${json}`;
try {
// callback
this.io.on_send_message(msg);
// Header must be written in ASCII encoding
this.io.send_message(headers.join(""));
// Now write the content. This can be written in any encoding
log.debug("tx:", msg);
this.io.send_message(json);
this.io.write(message);
this.errorCount = 0;
} catch (error) {
this.errorCount++;
this.fireError(error, msg, this.errorCount);
this.fireError(error, modified, this.errorCount);
}
return;
}
end(): void {}
}

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

@@ -2,14 +2,14 @@ import * as vscode from "vscode";
import {
Uri,
Range,
TextDocument,
CancellationToken,
type TextDocument,
type CancellationToken,
DocumentLink,
DocumentLinkProvider,
ExtensionContext,
type DocumentLinkProvider,
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

@@ -1,5 +1,5 @@
import * as vscode from "vscode";
import {
import type {
CancellationToken,
CustomDocument,
CustomDocumentOpenContext,
@@ -8,15 +8,15 @@ import {
Uri,
WebviewPanel,
} from "vscode";
import { NotificationMessage } from "vscode-jsonrpc";
import {
import type { NotificationMessage } from "vscode-jsonrpc";
import type {
NativeSymbolInspectParams,
GodotNativeSymbol,
GodotNativeClassInfo,
GodotCapabilities,
} from "../lsp/gdscript.capabilities";
} from "./documentation_types";
import { make_html_content } from "./documentation_builder";
import { createLogger, get_extension_uri, make_docs_uri } from "../utils";
import { createLogger, get_configuration, get_extension_uri, make_docs_uri } from "../utils";
import { globals } from "../extension";
const log = createLogger("providers.docs");
@@ -37,9 +37,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
},
supportsMultipleEditorsPerDocument: true,
};
context.subscriptions.push(
vscode.window.registerCustomEditorProvider("gddoc", this, options),
);
context.subscriptions.push(vscode.window.registerCustomEditorProvider("gddoc", this, options));
}
public register_capabilities(message: NotificationMessage) {
@@ -63,23 +61,28 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
}
public async list_native_classes() {
const classname = await vscode.window.showQuickPick(
[...this.classInfo.keys()].sort(),
{
placeHolder: "Type godot class name here",
canPickMany: false,
}
);
const classname = await vscode.window.showQuickPick([...this.classInfo.keys()].sort(), {
placeHolder: "Type godot class name here",
canPickMany: false,
});
if (classname) {
vscode.commands.executeCommand("vscode.open", make_docs_uri(classname));
}
}
public openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): CustomDocument {
return { uri: uri, dispose: () => { } };
public openCustomDocument(
uri: Uri,
openContext: CustomDocumentOpenContext,
token: CancellationToken,
): CustomDocument {
return { uri: uri, dispose: () => {} };
}
public async resolveCustomEditor(document: CustomDocument, panel: WebviewPanel, token: CancellationToken): Promise<void> {
public async resolveCustomEditor(
document: CustomDocument,
panel: WebviewPanel,
token: CancellationToken,
): Promise<void> {
const className = document.uri.path.split(".")[0];
const target = document.uri.fragment;
let symbol: GodotNativeSymbol = null;
@@ -89,7 +92,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
};
while (!this.ready) {
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise((resolve) => setTimeout(resolve, 100));
}
symbol = this.symbolDb.get(className);
@@ -100,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);
@@ -109,9 +112,21 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
if (!this.htmlDb.has(className)) {
this.htmlDb.set(className, make_html_content(panel.webview, symbol, target));
}
panel.webview.html = this.htmlDb.get(className);
const scaleFactor = get_configuration("documentation.pageScale");
panel.webview.html = this.htmlDb.get(className).replaceAll("scaleFactor", scaleFactor);
const displayMinimap = get_configuration("documentation.displayMinimap");
if (displayMinimap) {
panel.webview.html = this.htmlDb.get(className).replace("displayMinimap", "initial;");
panel.webview.html = this.htmlDb.get(className).replace("bodyMargin", "200px;");
} else {
panel.webview.html = this.htmlDb.get(className).replace("bodyMargin", "0px;");
panel.webview.html = this.htmlDb.get(className).replace("displayMinimap", "none;");
}
panel.iconPath = get_extension_uri("resources/godot_icon.svg");
panel.webview.onDidReceiveMessage(msg => {
panel.webview.onDidReceiveMessage((msg) => {
if (msg.type === "INSPECT_NATIVE_SYMBOL") {
const uri = make_docs_uri(msg.data.native_class, msg.data.symbol_name);
vscode.commands.executeCommand("vscode.open", uri);

View File

@@ -3,7 +3,7 @@ import { SymbolKind } from "vscode-languageclient";
import * as Prism from "prismjs";
import * as csharp from "prismjs/components/prism-csharp";
import { marked } from "marked";
import { GodotNativeSymbol } from "../lsp/gdscript.capabilities";
import type { GodotNativeSymbol } from "./documentation_types";
import { get_extension_uri } from "../utils";
import yabbcode = require("ya-bbcode");
@@ -14,7 +14,7 @@ const parser = new yabbcode();
const wtf = csharp;
marked.setOptions({
highlight: function (code, lang) {
highlight: (code, lang) => {
if (lang === "gdscript") {
return Prism.highlight(code, GDScriptGrammar, lang);
}
@@ -69,7 +69,7 @@ export function make_html_content(webview: vscode.Webview, symbol: GodotNativeSy
`;
}
return /*html*/`<!DOCTYPE html>
return /*html*/ `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -78,16 +78,16 @@ export function make_html_content(webview: vscode.Webview, symbol: GodotNativeSy
<title>${symbol.name}</title>
</head>
<body style="line-height: 16pt;">
<body style="line-height: scaleFactor%; font-size: scaleFactor%; margin-right: bodyMargin">
<main>
${make_symbol_document(symbol)}
</main>
<canvas id='map'></canvas>
<canvas id='minimap' style="display: displayMinimap"></canvas>
<script src="${pagemapJsUri}"></script>
<script>
pagemap(document.querySelector('#map'), ${options});
pagemap(document.querySelector('#minimap'), ${options});
${initialFocus};
var vscode = acquireVsCodeApi();
@@ -128,128 +128,94 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
const ret_type = make_link(parts[2] || "void", undefined);
let args = (parts[1] || "").replace(
/\:\s([A-z0-9_]+)(\,\s*)?/g,
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2"
': <a href="" onclick="inspect(\'$1\')">$1</a>$2',
);
args = args.replace(/\s=\s(.*?)[\,\)]/g, "");
return `${ret_type} ${with_class ? `${classlink}.` : ""}${element(
"a",
s.name,
{ href: `#${s.name}` }
)}( ${args} )`;
return `${ret_type} ${with_class ? `${classlink}.` : ""}${element("a", s.name, {
href: `#${s.name}`,
})}( ${args} )`;
}
function make_symbol_elements(
s: GodotNativeSymbol,
with_class = false
): { index?: string; body: string } {
function make_symbol_elements(s: GodotNativeSymbol, with_class = false): { index?: string; body: string } {
switch (s.kind) {
case SymbolKind.Property:
case SymbolKind.Variable:
{
// var Control.anchor_left: float
const parts = /\.([A-z_0-9]+)\:\s(.*)$/.exec(s.detail);
if (!parts) {
return;
}
const type = make_link(parts[2], undefined);
const name = element("a", s.name, { href: `#${s.name}` });
const title = element(
"h4",
`${type} ${with_class ? `${classlink}.` : ""}${s.name}`
);
const doc = element(
"p",
format_documentation(s.documentation, symbol.native_class)
);
const div = element("div", title + doc);
return {
index: type + " " + name,
body: div,
};
case SymbolKind.Variable: {
// var Control.anchor_left: float
const parts = /\.([A-z_0-9]+)\:\s(.*)$/.exec(s.detail);
if (!parts) {
return;
}
break;
case SymbolKind.Constant:
{
// const Control.FOCUS_ALL: FocusMode = 2
// const Control.NOTIFICATION_RESIZED = 40
const parts = /\.([A-Za-z_0-9]+)(\:\s*)?([A-z0-9_\.]+)?\s*=\s*(.*)$/.exec(
s.detail
);
if (!parts) {
return;
}
const type = make_link(parts[3] || "int", undefined);
const name = parts[1];
const value = element("code", parts[4]);
const type = make_link(parts[2], undefined);
const name = element("a", s.name, { href: `#${s.name}` });
const title = element("h4", `${type} ${with_class ? `${classlink}.` : ""}${s.name}`);
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
const div = element("div", title + doc);
return {
index: `${type} ${name}`,
body: div,
};
}
case SymbolKind.Constant: {
// const Control.FOCUS_ALL: FocusMode = 2
// const Control.NOTIFICATION_RESIZED = 40
const parts = /\.([A-Za-z_0-9]+)(\:\s*)?([A-z0-9_\.]+)?\s*=\s*(.*)$/.exec(s.detail);
if (!parts) {
return;
}
const type = make_link(parts[3] || "int", undefined);
const name = parts[1];
const value = element("code", parts[4]);
const title = element(
"p",
`${type} ${with_class ? `${classlink}.` : ""}${name} = ${value}`
);
const doc = element(
"p",
format_documentation(s.documentation, symbol.native_class)
);
const div = element("div", title + doc);
return {
body: div,
};
const title = element("p", `${type} ${with_class ? `${classlink}.` : ""}${name} = ${value}`);
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
const div = element("div", title + doc);
return {
body: div,
};
}
case SymbolKind.Event: {
const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
if (!parts) {
return;
}
break;
case SymbolKind.Event:
{
const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
if (!parts) {
return;
}
const args = (parts[2] || "").replace(
/\:\s([A-z0-9_]+)(\,\s*)?/g,
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2"
);
const title = element(
"p",
`${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""
}${s.name}( ${args} )`
);
const doc = element(
"p",
format_documentation(s.documentation, symbol.native_class)
);
const div = element("div", title + doc);
return {
body: div,
};
}
break;
const args = (parts[2] || "").replace(
/\:\s([A-z0-9_]+)(\,\s*)?/g,
': <a href="" onclick="inspect(\'$1\')">$1</a>$2',
);
const title = element(
"p",
`${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""}${s.name}( ${args} )`,
);
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
const div = element("div", title + doc);
return {
body: div,
};
}
case SymbolKind.Method:
case SymbolKind.Function:
{
const signature = make_function_signature(s, with_class);
const title = element("h4", signature);
const doc = element(
"p",
format_documentation(s.documentation, symbol.native_class)
);
const div = element("div", title + doc);
return {
index: signature,
body: div,
};
}
break;
case SymbolKind.Function: {
const signature = make_function_signature(s, with_class);
const title = element("h4", signature);
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
const div = element("div", title + doc);
return {
index: signature,
body: div,
};
}
default:
break;
}
}
if (symbol.kind == SymbolKind.Class) {
if (symbol.kind === SymbolKind.Class) {
let doc = element("h2", `Class: ${symbol.name}`);
if (symbol.class_info.inherits) {
const inherits = make_link(symbol.class_info.inherits, undefined);
doc += element("p", `Inherits: ${inherits}`);
}
if (symbol.class_info && symbol.class_info.extended_classes) {
if (symbol.class_info?.extended_classes) {
let inherited = "";
for (const c of symbol.class_info.extended_classes) {
inherited += (inherited ? ", " : " ") + make_link(c, c);
@@ -267,28 +233,30 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
let propertyies = "";
let others = "";
for (const s of symbol.children as GodotNativeSymbol[]) {
const elements = make_symbol_elements(s);
switch (s.kind) {
case SymbolKind.Property:
case SymbolKind.Variable:
properties_index += element("li", elements.index);
propertyies += element("li", elements.body, { id: s.name });
break;
case SymbolKind.Constant:
constants += element("li", elements.body, { id: s.name });
break;
case SymbolKind.Event:
signals += element("li", elements.body, { id: s.name });
break;
case SymbolKind.Method:
case SymbolKind.Function:
methods_index += element("li", elements.index);
methods += element("li", elements.body, { id: s.name });
break;
default:
others += element("li", elements.body, { id: s.name });
break;
if (symbol.children) {
for (const s of symbol.children as GodotNativeSymbol[]) {
const elements = make_symbol_elements(s);
switch (s.kind) {
case SymbolKind.Property:
case SymbolKind.Variable:
properties_index += element("li", elements.index);
propertyies += element("li", elements.body, { id: s.name });
break;
case SymbolKind.Constant:
constants += element("li", elements.body, { id: s.name });
break;
case SymbolKind.Event:
signals += element("li", elements.body, { id: s.name });
break;
case SymbolKind.Method:
case SymbolKind.Function:
methods_index += element("li", elements.index);
methods += element("li", elements.body, { id: s.name });
break;
default:
others += element("li", elements.body, { id: s.name });
break;
}
}
}
@@ -308,19 +276,18 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
add_group("Other Members", others);
doc += element("script", `var godot_class = "${symbol.native_class}";`);
return doc;
} else {
let doc = "";
const elements = make_symbol_elements(symbol, true);
if (elements.index) {
const symbols: SymbolKind[] = [SymbolKind.Function, SymbolKind.Method];
if (!symbols.includes(symbol.kind)) {
doc += element("h2", elements.index);
}
}
doc += element("div", elements.body);
return doc;
}
let doc = "";
const elements = make_symbol_elements(symbol, true);
if (elements.index) {
const symbols: SymbolKind[] = [SymbolKind.Function, SymbolKind.Method];
if (!symbols.includes(symbol.kind)) {
doc += element("h2", elements.index);
}
}
doc += element("div", elements.body);
return doc;
}
function element<K extends keyof HTMLElementTagNameMap>(
@@ -328,7 +295,7 @@ function element<K extends keyof HTMLElementTagNameMap>(
content: string,
props = {},
new_line?: boolean,
indent?: string
indent?: string,
) {
let props_str = "";
for (const key in props) {
@@ -336,38 +303,36 @@ function element<K extends keyof HTMLElementTagNameMap>(
props_str += ` ${key}="${props[key]}"`;
}
}
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""
}`;
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""}`;
}
function make_link(classname: string, symbol: string) {
if (!symbol || symbol == classname) {
if (!symbol || symbol === classname) {
return element("a", classname, {
onclick: `inspect('${classname}')`,
href: "",
});
} else {
return element("a", `${classname}.${symbol}`, {
onclick: `inspect('${classname}', '${symbol}')`,
href: "",
});
}
return element("a", `${classname}.${symbol}`, {
onclick: `inspect('${classname}', '${symbol}')`,
href: "",
});
}
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;", "\"");
html = html.replaceAll("&quot;", '"');
for (const match of html.matchAll(/\[codeblock].*?\[\/codeblock]/gs)) {
let block = match[0];
@@ -390,24 +355,21 @@ function format_documentation(bbcode: string, classname: string) {
html = html.replaceAll("<br/> ", "");
// [param <name>]
html = html.replaceAll(
/\[param\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g,
"<code>$1</code>"
);
html = html.replaceAll(/\[param\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g, "<code>$1</code>");
// [method <name>]
html = html.replaceAll(
/\[method\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g,
`<a href="" onclick="inspect('${classname}', '$1')">$1</a>`
`<a href="" onclick="inspect('${classname}', '$1')">$1</a>`,
);
// [<reference>]
html = html.replaceAll(
/\[(\w+)\]/g,
`<a href="" onclick="inspect('$1')">$1</a>` // eslint-disable-line quotes
`<a href="" onclick="inspect('$1')">$1</a>`, // eslint-disable-line quotes
);
// [method <class>.<name>]
html = html.replaceAll(
/\[\w+\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\.(\w+)\]/g,
`<a href="" onclick="inspect('$1', '$2')">$1.$2</a>` // eslint-disable-line quotes
`<a href="" onclick="inspect('$1', '$2')">$1.$2</a>`, // eslint-disable-line quotes
);
return html;
@@ -466,8 +428,10 @@ const GDScriptGrammar = {
punctuation: /\./,
},
},
keyword: /\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
builtin: /\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
keyword:
/\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
builtin:
/\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
boolean: /\b(?:true|false)\b/,
number: /(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
operator: /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,

View File

@@ -1,4 +1,4 @@
import { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient";
import type { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient";
export interface NativeSymbolInspectParams {
native_class: string;

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}`,
);
}
// 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

@@ -2,8 +2,9 @@ import {
TreeItem,
TreeItemCollapsibleState,
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;
@@ -16,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(
@@ -28,11 +29,11 @@ export class SceneNode extends TreeItem {
) {
super(label, collapsibleState);
const iconName = className + ".svg";
const iconName = `${className}.svg`;
this.iconPath = {
light: path.join(iconDir, "light", iconName),
dark: path.join(iconDir, "dark", iconName),
light: Uri.file(path.join(iconDir, "light", iconName)),
dark: Uri.file(path.join(iconDir, "dark", iconName)),
};
}
@@ -52,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);
}
}
@@ -78,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,6 +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);
}
return projectDir;
}
@@ -46,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();
}
@@ -101,8 +124,82 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
return vscode.Uri.joinPath(vscode.Uri.file(dir), resPath.substring("res://".length));
}
type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
type VERIFY_RESULT = {
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;
godotPath: string;
version?: string;
@@ -139,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

@@ -1,10 +1,10 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
import { AddressInfo, createServer } from "net";
import * as fs from "node:fs";
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";
@@ -21,12 +21,12 @@ export async function find_file(file: string): Promise<vscode.Uri | null> {
if (results.length === 1) {
return results[0];
}
return null;
}
export async function get_free_port(): Promise<number> {
return new Promise(res => {
return new Promise((res) => {
const srv = createServer();
srv.listen(0, () => {
const port = (srv.address() as AddressInfo).port;
@@ -45,7 +45,7 @@ export function make_docs_uri(path: string, fragment?: string) {
/**
* Can be used to convert a conventional node name to a snake_case variable name.
*
*
* @example
* ```ts
* nodeNameToVar("MyNode") // my_node
@@ -54,10 +54,39 @@ export function make_docs_uri(path: string, fragment?: string) {
* ```
*/
export function node_name_to_snake(name: string): string {
const snakeCase: string = name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").toLowerCase();
if (snakeCase.startsWith("_")) {
return snakeCase.substring(1);
}
return snakeCase;
const snakeCase: string = name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").toLowerCase();
if (snakeCase.startsWith("_")) {
return snakeCase.substring(1);
}
return snakeCase;
}
export const ansi = {
reset: "\u001b[0;37m",
red: "\u001b[0;31m",
green: "\u001b[0;32m",
yellow: "\u001b[0;33m",
blue: "\u001b[0;34m",
purple: "\u001b[0;35m",
cyan: "\u001b[0;36m",
white: "\u001b[0;37m",
bright: {
red: "\u001b[1;31m",
green: "\u001b[1;32m",
yellow: "\u001b[1;33m",
blue: "\u001b[1;34m",
purple: "\u001b[1;35m",
cyan: "\u001b[1;36m",
white: "\u001b[1;37m",
},
dim: {
red: "\u001b[1;2;31m",
green: "\u001b[1;2;32m",
yellow: "\u001b[1;2;33m",
blue: "\u001b[1;2;34m",
purple: "\u001b[1;2;35m",
cyan: "\u001b[1;2;36m",
white: "\u001b[1;2;37m",
},
} as const;

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|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": {
@@ -263,15 +263,19 @@
"name": "constant.numeric.integer.hexadecimal.gdscript"
},
{
"match": "[-]?([0-9][0-9_]+\\.[0-9_]*(e[\\-\\+]?[0-9_]+)?)",
"match": "\\.[0-9][0-9_]*([eE][+-]?[0-9_]+)?",
"name": "constant.numeric.float.gdscript"
},
{
"match": "[-]?(\\.[0-9][0-9_]*(e[\\-\\+]?[0-9_]+)?)",
"match": "([0-9][0-9_]*)\\.[0-9_]*([eE][+-]?[0-9_]+)?",
"name": "constant.numeric.float.gdscript"
},
{
"match": "[-]?([0-9][0-9_]*e[\\-\\+]?\\[0-9_])",
"match": "([0-9][0-9_]*)?\\.[0-9_]*([eE][+-]?[0-9_]+)",
"name": "constant.numeric.float.gdscript"
},
{
"match": "[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" }
}
}
]
},
@@ -367,7 +376,7 @@
"match": "\\b([A-Z][a-zA-Z_0-9]*)\\.([A-Z_0-9]+)",
"captures": {
"1": { "name": "entity.name.type.class.gdscript" },
"2": { "name": "constant.language.gdscript" }
"2": { "name": "variable.other.enummember.gdscript" }
}
},
"class_name": {
@@ -386,7 +395,7 @@
},
"builtin_get_node_shorthand_quoted": {
"name": "string.quoted.gdscript meta.literal.nodepath.gdscript constant.character.escape.gdscript",
"begin": "(?:(\\$)|(&|\\^|@))(\"|')",
"begin": "(?:(\\$|%)|(&|\\^|@))(\"|')",
"beginCaptures": {
"1": { "name": "keyword.control.flow.gdscript" },
"2": { "name": "variable.other.enummember.gdscript" }
@@ -430,22 +439,22 @@
]
},
"annotations": {
"match": "(@)(export|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|abstract)\\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(OS|GDScript|Vector2|Vector2i|Vector3|Vector3i|Color|Rect2|Rect2i|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|Transform3D|AABB|String|Color|NodePath|Object|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray|bool|int|float|StringName|Quaternion|PackedByteArray|PackedInt32Array|PackedInt64Array|PackedFloat32Array|PackedFloat64Array|PackedStringArray|PackedVector2Array|PackedVector2iArray|PackedVector3Array|PackedVector3iArray|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": {
"match": "\\b([A-Z_][A-Z_0-9]*)\\b",
"name": "constant.language.gdscript"
"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": [
@@ -498,13 +507,8 @@
"1": { "name": "keyword.language.gdscript storage.type.function.gdscript" },
"2": { "name": "entity.name.function.gdscript" }
},
"end": "(:|(?=[#'\"\\n]))",
"end2": "(\\s*(\\-\\>)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:)",
"endCaptures2": {
"1": { "name": "punctuation.separator.annotation.result.gdscript" },
"2": { "name": "keyword.language.void.gdscript" },
"3": { "name": "entity.name.type.class.gdscript markup.italic" }
},
"end": "(:)",
"endCaptures": { "1": { "name": "punctuation.section.function.begin.gdscript" } },
"patterns": [
{ "include": "#parameters" },
{ "include": "#line_continuation" },
@@ -535,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",
@@ -547,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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
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
func _ready() -> void:
var local_label := label
var local_self_var_through_label := label.parent_var
var local_classA = ClassA.new()
var local_classB = ClassB.new()
local_classA.member_classB = local_classB
local_classB.member_classA = local_classA
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
# var dict = {}
# dict["self_ref"] = dict
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_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,12 @@
[gd_scene load_steps=3 format=3 uid="uid://bsonfthpqa3dx"]
[ext_resource type="Script" path="res://ExtensiveVars.gd" id="1_fnilr"]
[ext_resource type="Script" path="res://ExtensiveVars_Label.gd" id="2_jijf2"]
[node name="ExtensiveVars" type="Node2D"]
script = ExtResource("1_fnilr")
[node name="Label" type="Label" parent="."]
text = "Extensive Vars scene"
script = ExtResource("2_jijf2")
metadata/_edit_use_anchors_ = true

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More