Compare commits

..

22 Commits
2.3.0 ... 2.4.0

Author SHA1 Message Date
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
75 changed files with 2704 additions and 1485 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.97.2"
validations:
required: true
@@ -40,7 +40,7 @@ body:
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
Specify the Git commit hash if using a development or non-official build.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: "2.3.0"
placeholder: "2.4.0"
validations:
required: true

View File

@@ -5,6 +5,7 @@ jobs:
test:
name: Test
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
@@ -13,13 +14,39 @@ jobs:
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.2.0
with:
node-version: 16.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:
@@ -34,7 +61,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.2.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.0
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

@@ -5,5 +5,7 @@ module.exports = defineConfig(
// version: '1.84.0',
label: 'unitTests',
files: 'out/**/*.test.js',
launchArgs: ['--disable-extensions'],
workspaceFolder: './test_projects/test-dap-project-godot4',
}
);

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

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

15
.vscode/launch.json vendored
View File

@@ -5,6 +5,7 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
@@ -16,9 +17,13 @@
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"skipFiles": [
"**/extensionHostProcess.js",
"<node_internals>/**/*.js"
],
"preLaunchTask": "npm: watch",
"env": {
"VSCODE_DEBUG_MODE": true
"VSCODE_DEBUG_MODE": "true"
}
},
{
@@ -29,14 +34,18 @@
"args": [
"--profile=temp",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}/workspace.code-workspace"
"${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
"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,31 @@
# Changelog
### 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)

View File

@@ -15,7 +15,8 @@
"linter": {
"rules": {
"style": {
"noUselessElse": "off"
"noUselessElse": "off",
"useImportType": "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,7 +1,3 @@
body {
margin-right: 200px;
}
a {
text-decoration: none;
}

211
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "godot-tools",
"version": "2.3.0",
"version": "2.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "godot-tools",
"version": "2.3.0",
"version": "2.4.0",
"license": "MIT",
"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",
"global": "^4.4.0",
"marked": "^4.0.11",
@@ -25,11 +25,12 @@
},
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/chai-subset": "^1.3.5",
"@types/marked": "^4.0.8",
"@types/mocha": "^10.0.6",
"@types/node": "^18.15.0",
"@types/prismjs": "^1.16.8",
"@types/vscode": "^1.80.0",
"@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",
@@ -38,6 +39,7 @@
"@vscode/test-electron": "^2.3.8",
"@vscode/vsce": "^2.29.0",
"chai": "^4.3.10",
"chai-subset": "^1.6.0",
"esbuild": "^0.17.15",
"eslint": "^8.37.0",
"mocha": "^10.2.0",
@@ -47,7 +49,7 @@
"typescript": "^5.2.2"
},
"engines": {
"vscode": "^1.80.0"
"vscode": "^1.96.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -1067,6 +1069,15 @@
"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
"dev": true
},
"node_modules/@types/chai-subset": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz",
"integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==",
"dev": true,
"dependencies": {
"@types/chai": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.13",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
@@ -1104,9 +1115,9 @@
"dev": true
},
"node_modules/@types/vscode": {
"version": "1.82.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.82.0.tgz",
"integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==",
"version": "1.96.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz",
"integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==",
"dev": true
},
"node_modules/@types/ws": {
@@ -1414,20 +1425,20 @@
}
},
"node_modules/@vscode/debugadapter": {
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.64.0.tgz",
"integrity": "sha512-XygE985qmNCzJExDnam4bErK6FG9Ck8S5TRPDNESwkt7i3OXqw5a3vYb7Dteyhz9YMEf7hwhFoT46Mjc45nJUg==",
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.68.0.tgz",
"integrity": "sha512-D6gk5Fw2y4FV8oYmltoXpj+VAZexxJFopN/mcZ6YcgzQE9dgq2L45Aj3GLxScJOD6GeLILcxJIaA8l3v11esGg==",
"dependencies": {
"@vscode/debugprotocol": "1.64.0"
"@vscode/debugprotocol": "1.68.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@vscode/debugprotocol": {
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.64.0.tgz",
"integrity": "sha512-Zhf3KvB+J04M4HPE2yCvEILGVtPixXUQMLBvx4QcAtjhc5lnwlZbbt80LCsZO2B+2BH8RMgVXk3QQ5DEzEne2Q=="
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz",
"integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg=="
},
"node_modules/@vscode/test-cli": {
"version": "0.0.4",
@@ -1892,9 +1903,9 @@
}
},
"node_modules/ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"engines": {
"node": ">=6"
@@ -2192,9 +2203,9 @@
}
},
"node_modules/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
"integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
"integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
"dev": true,
"dependencies": {
"assertion-error": "^1.1.0",
@@ -2203,12 +2214,21 @@
"get-func-name": "^2.0.2",
"loupe": "^2.3.6",
"pathval": "^1.1.1",
"type-detect": "^4.0.8"
"type-detect": "^4.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chai-subset": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz",
"integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -2498,12 +2518,12 @@
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -4466,32 +4486,31 @@
"optional": true
},
"node_modules/mocha": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
"integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
"version": "10.8.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
"integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==",
"dev": true,
"dependencies": {
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.5.3",
"debug": "4.3.4",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.2.0",
"he": "1.2.0",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "5.0.1",
"ms": "2.1.3",
"nanoid": "3.3.3",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"workerpool": "6.2.1",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
"ansi-colors": "^4.1.3",
"browser-stdout": "^1.3.1",
"chokidar": "^3.5.3",
"debug": "^4.3.5",
"diff": "^5.2.0",
"escape-string-regexp": "^4.0.0",
"find-up": "^5.0.0",
"glob": "^8.1.0",
"he": "^1.2.0",
"js-yaml": "^4.1.0",
"log-symbols": "^4.1.0",
"minimatch": "^5.1.6",
"ms": "^2.1.3",
"serialize-javascript": "^6.0.2",
"strip-json-comments": "^3.1.1",
"supports-color": "^8.1.1",
"workerpool": "^6.5.1",
"yargs": "^16.2.0",
"yargs-parser": "^20.2.9",
"yargs-unparser": "^2.0.0"
},
"bin": {
"_mocha": "bin/_mocha",
@@ -4499,10 +4518,6 @@
},
"engines": {
"node": ">= 14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mochajs"
}
},
"node_modules/mocha/node_modules/argparse": {
@@ -4521,9 +4536,9 @@
}
},
"node_modules/mocha/node_modules/diff": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"dev": true,
"engines": {
"node": ">=0.3.1"
@@ -4541,6 +4556,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mocha/node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mocha/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4563,9 +4598,9 @@
}
},
"node_modules/mocha/node_modules/minimatch": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -4574,12 +4609,6 @@
"node": ">=10"
}
},
"node_modules/mocha/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/mocha/node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -4608,9 +4637,9 @@
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/mute-stream": {
@@ -4619,18 +4648,6 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
@@ -5247,9 +5264,9 @@
}
},
"node_modules/serialize-javascript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
@@ -5635,9 +5652,9 @@
}
},
"node_modules/ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
@@ -5774,9 +5791,9 @@
}
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
"integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
"dev": true,
"engines": {
"node": ">=4"
@@ -5951,9 +5968,9 @@
}
},
"node_modules/workerpool": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
"integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
"dev": true
},
"node_modules/wrap-ansi": {
@@ -6222,9 +6239,9 @@
}
},
"node_modules/yargs-parser": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
"engines": {
"node": ">=10"

View File

@@ -2,7 +2,7 @@
"name": "godot-tools",
"displayName": "godot-tools",
"icon": "icon.png",
"version": "2.3.0",
"version": "2.4.0",
"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",
@@ -258,6 +258,11 @@
"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",
@@ -296,21 +301,6 @@
"default": false,
"description": "Whether extra space should be removed from function parameter lists"
},
"godotTools.lsp.serverProtocol": {
"type": [
"string"
],
"enum": [
"ws",
"tcp"
],
"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"
],
"description": "The server protocol of the GDScript language server.\nYou must restart VSCode after changing this value."
},
"godotTools.lsp.serverHost": {
"type": "string",
"default": "127.0.0.1",
@@ -881,11 +871,12 @@
},
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/chai-subset": "^1.3.5",
"@types/marked": "^4.0.8",
"@types/mocha": "^10.0.6",
"@types/node": "^18.15.0",
"@types/prismjs": "^1.16.8",
"@types/vscode": "^1.80.0",
"@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",
@@ -894,6 +885,7 @@
"@vscode/test-electron": "^2.3.8",
"@vscode/vsce": "^2.29.0",
"chai": "^4.3.10",
"chai-subset": "^1.6.0",
"esbuild": "^0.17.15",
"eslint": "^8.37.0",
"mocha": "^10.2.0",
@@ -903,8 +895,8 @@
"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",
"global": "^4.4.0",
"marked": "^4.0.11",

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 = [];
@@ -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

@@ -309,13 +309,13 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig
break;
case "number":
if (is_float) {
new_parsed_value = parseFloat(value);
if (isNaN(new_parsed_value)) {
new_parsed_value = Number.parseFloat(value);
if (Number.isNaN(new_parsed_value)) {
return;
}
} else {
new_parsed_value = parseInt(value);
if (isNaN(new_parsed_value)) {
new_parsed_value = Number.parseInt(value);
if (Number.isNaN(new_parsed_value)) {
return;
}
}

View File

@@ -1,26 +1,32 @@
import * as fs from "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 * as fs from "node:fs";
import { debug } from "vscode";
import { createLogger } from "../../utils";
import { GodotDebugData, GodotStackVars, GodotVariable } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
import { 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);
@@ -32,10 +38,7 @@ export class GodotDebugSession extends LoggingDebugSession {
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,10 +82,7 @@ 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";
@@ -94,10 +94,7 @@ export class GodotDebugSession extends LoggingDebugSession {
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";
@@ -110,16 +107,13 @@ 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
) {
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
@@ -127,24 +121,20 @@ export class GodotDebugSession extends LoggingDebugSession {
}
}
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 +148,21 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
protected nextRequest(
response: DebugProtocol.NextResponse,
args: DebugProtocol.NextArguments
) {
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
}
protected pauseRequest(
response: DebugProtocol.PauseResponse,
args: DebugProtocol.PauseArguments
) {
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
) {
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 +178,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 +187,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 +207,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 +215,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 +225,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 +233,21 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
protected stepInRequest(
response: DebugProtocol.StepInResponse,
args: DebugProtocol.StepInArguments
) {
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
}
protected stepOutRequest(
response: DebugProtocol.StepOutResponse,
args: DebugProtocol.StepOutArguments
) {
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
if (!this.exception) {
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 +262,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 +280,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 +289,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,
),
);
}
});
@@ -354,7 +314,7 @@ export class GodotDebugSession extends LoggingDebugSession {
name: "local",
value: undefined,
sub_values: stackVars.locals,
scope_path: "@"
scope_path: "@",
},
{
name: "member",
@@ -370,20 +330,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 +354,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 +380,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 +390,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;
}
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 +451,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 +512,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

@@ -1,17 +1,28 @@
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";
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
const socketLog = createLogger("debugger.socket");
@@ -37,9 +48,7 @@ export class ServerController {
private didFirstOutput: boolean = false;
private connectedVersion = "";
public constructor(
public session: GodotDebugSession
) { }
public constructor(public session: GodotDebugSession) {}
public break() {
this.send_command("break");
@@ -87,12 +96,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 +108,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 +173,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 +224,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 +356,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 +384,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 +393,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 +427,100 @@ export class ServerController {
this.didFirstOutput = true;
// this.request_scene_tree();
}
command.parameters.forEach((line) => {
debug.activeDebugConsole.appendLine(line[0]);
});
const lines = command.parameters;
for (const line of lines) {
debug.activeDebugConsole.appendLine(ansi.bright.blue + line[0]);
}
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 +555,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;
}
@@ -506,9 +588,7 @@ export class ServerController {
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 +615,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 +631,7 @@ export class ServerController {
stackVars.globals.push({ name: parameters[i++], value: parameters[i++] });
}
stackVars.forEach(item => build_sub_values(item));
stackVars.forEach((item) => build_sub_values(item));
this.session.set_scopes(stackVars);
}

View File

@@ -1,26 +1,32 @@
import * as fs from "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 * as fs from "node:fs";
import { debug } from "vscode";
import { createLogger } from "../../utils";
import { GodotDebugData, GodotStackVars, GodotVariable } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
import { SceneTreeProvider } from "../scene_tree_provider";
import { is_variable_built_in_type, parse_variable } from "./helpers";
import { ServerController } from "./server_controller";
import { ObjectId, RawObject } from "./variables/variants";
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);
@@ -32,10 +38,7 @@ export class GodotDebugSession extends LoggingDebugSession {
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,8 +53,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,10 +83,8 @@ 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";
@@ -94,10 +96,8 @@ export class GodotDebugSession extends LoggingDebugSession {
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";
@@ -110,16 +110,15 @@ 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
) {
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
log.info("continueRequest", args);
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
@@ -127,24 +126,21 @@ export class GodotDebugSession extends LoggingDebugSession {
}
}
protected async evaluateRequest(
response: DebugProtocol.EvaluateResponse,
args: DebugProtocol.EvaluateArguments
) {
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
log.info("evaluateRequest", args);
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 +154,24 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
protected nextRequest(
response: DebugProtocol.NextResponse,
args: DebugProtocol.NextArguments
) {
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
log.info("nextRequest", args);
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
}
protected pauseRequest(
response: DebugProtocol.PauseResponse,
args: DebugProtocol.PauseArguments
) {
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
log.info("pauseRequest", args);
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
}
protected async scopesRequest(
response: DebugProtocol.ScopesResponse,
args: DebugProtocol.ScopesArguments
) {
protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
log.info("scopesRequest", args);
this.controller.request_stack_frame_vars(args.frameId);
await this.got_scope.wait(2000);
@@ -197,8 +187,9 @@ export class GodotDebugSession extends LoggingDebugSession {
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 +197,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 +217,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 +225,8 @@ export class GodotDebugSession extends LoggingDebugSession {
}
}
protected stackTraceRequest(
response: DebugProtocol.StackTraceResponse,
args: DebugProtocol.StackTraceArguments
) {
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,10 +236,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 +244,24 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
protected stepInRequest(
response: DebugProtocol.StepInResponse,
args: DebugProtocol.StepInArguments
) {
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
log.info("stepInRequest", args);
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
}
protected stepOutRequest(
response: DebugProtocol.StepOutResponse,
args: DebugProtocol.StepOutArguments
) {
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
log.info("stepOutRequest", args);
if (!this.exception) {
this.controller.step_out();
this.sendResponse(response);
}
}
protected terminateRequest(
response: DebugProtocol.TerminateResponse,
args: DebugProtocol.TerminateArguments
) {
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
log.info("terminateRequest", args);
if (this.mode === "launch") {
this.controller.stop();
this.sendEvent(new TerminatedEvent());
@@ -295,17 +270,19 @@ export class GodotDebugSession extends LoggingDebugSession {
}
protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
log.info("threadsRequest");
response.body = { threads: [new Thread(0, "thread_1")] };
this.sendResponse(response);
}
protected async variablesRequest(
response: DebugProtocol.VariablesResponse,
args: DebugProtocol.VariablesArguments
args: DebugProtocol.VariablesArguments,
) {
log.info("variablesRequest", args);
if (!this.all_scopes) {
response.body = {
variables: []
variables: [],
};
this.sendResponse(response);
return;
@@ -319,8 +296,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 +305,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,
),
);
}
});
@@ -354,7 +330,7 @@ export class GodotDebugSession extends LoggingDebugSession {
name: "local",
value: undefined,
sub_values: stackVars.locals,
scope_path: "@"
scope_path: "@",
},
{
name: "member",
@@ -370,100 +346,131 @@ 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();
if (this.ongoing_inspections.length === 0) {
if (this.ongoing_inspections.length === 0 && stackVars.remaining == 0) {
// in case if stackVars are empty, the this.ongoing_inspections will be empty also
// godot 4.3 generates empty stackVars with remaining > 0 on a breakpoint stop
// godot will continue sending `stack_frame_vars` until all `stackVars.remaining` are sent
// at this moment `stack_frame_vars` will call `set_scopes` again with cumulated stackVars
// TODO: godot won't send the recursive variable, see related https://github.com/godotengine/godot/issues/76019
// in that case the vscode extension fails to call this.got_scope.notify();
// hence the extension needs to be refactored to handle missing `stack_frame_vars` messages
this.previous_inspections = [];
this.got_scope.notify();
}
}
public set_inspection(id: bigint, replacement: GodotVariable) {
const variables = this.all_scopes.filter(
(va) => va && va.value instanceof ObjectId && va.value.id === id
);
public set_inspection(id: bigint, rawObject: RawObject, sub_values: GodotVariable[]) {
const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id);
variables.forEach((va) => {
for (const va of variables) {
const index = this.all_scopes.findIndex((va_id) => va_id === va);
if (index < 0) {
continue;
}
const old = this.all_scopes.splice(index, 1);
// GodotVariable instance will be different for different variables, even if the referenced object id is the same:
const replacement = {value: rawObject, sub_values: sub_values } as GodotVariable;
replacement.name = old[0].name;
replacement.scope_path = old[0].scope_path;
this.append_variable(replacement, index);
});
}
this.ongoing_inspections.splice(
this.ongoing_inspections.findIndex((va_id) => va_id === id),
1
);
const ongoing_inspections_index = this.ongoing_inspections.findIndex((va_id) => va_id === id);
if (ongoing_inspections_index >= 0) {
this.ongoing_inspections.splice(ongoing_inspections_index, 1);
}
this.previous_inspections.push(id);
// this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
// the `ongoing_inspections` is not empty, until all scopes are fully resolved
// once last inspection is resolved: Notify that we got full scope
this.previous_inspections = [];
this.got_scope.notify();
}
}
private add_to_inspections() {
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);
}
const scopes_to_check = this.all_scopes.filter((va) => va && va.value instanceof ObjectId);
for (const va of scopes_to_check) {
if (
!this.ongoing_inspections.includes(va.value.id) &&
!this.previous_inspections.includes(va.value.id)
) {
this.controller.request_inspect_object(va.value.id);
this.ongoing_inspections.push(va.value.id);
}
});
}
}
protected get_variable(expression: string, root: GodotVariable = null, index: 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;
}
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 +481,59 @@ 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 {
const entries = Array.from(root.value.entries());
const item = entries.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName);
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;
}
}
@@ -525,7 +542,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

@@ -0,0 +1,359 @@
import { promises as fs } from "fs";
import * as path from "path";
import * as vscode from "vscode";
import { DebugProtocol } from "@vscode/debugprotocol";
import chai from "chai";
import chaiSubset from "chai-subset";
import { promisify } from "util";
import { execFile } from "child_process";
const execFileAsync = promisify(execFile);
chai.use(chaiSubset);
const { expect } = chai;
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Given a path to a script, returns an object where each key is the name of a
* breakpoint (delimited by `breakpoint::`) and each value is the line number
* where the breakpoint appears in the script.
*
* @param scriptPath The path to the script to scan.
* @returns An object of breakpoint names to line numbers.
*/
async function getBreakpointLocations(scriptPath: string): Promise<{ [key: string]: vscode.Location }> {
const script_content = await fs.readFile(scriptPath, "utf-8");
const breakpoints: { [key: string]: vscode.Location } = {};
const breakpointRegex = /\b(breakpoint::.*)\b/g;
let match: RegExpExecArray | null;
while ((match = breakpointRegex.exec(script_content)) !== null) {
const breakpointName = match[1];
const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1;
breakpoints[breakpointName] = new vscode.Location(vscode.Uri.file(scriptPath), new vscode.Position(line - 1, 0));
}
return breakpoints;
}
async function waitForActiveStackItemChange(ms: number = 10000): Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((resolve, reject) => {
const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => {
debugListener.dispose();
resolve(vscode.debug.activeStackItem);
});
// Timeout fallback in case stack item never changes
setTimeout(() => {
debugListener.dispose();
console.warn();
reject(new Error(`The ActiveStackItem eventwas not changed within the timeout period of '${ms}'`));
}, ms);
});
return res;
}
async function getStackFrames(threadId: number = 1): Promise<DebugProtocol.StackFrame[]> {
// Ensure there is an active debug session
if (!vscode.debug.activeDebugSession) {
throw new Error("No active debug session found");
}
// corresponds to file://./debug_session.ts stackTraceRequest(...)
const stackTraceResponse = await vscode.debug.activeDebugSession.customRequest("stackTrace", {
threadId: threadId,
});
// Extract and return the stack frames
return stackTraceResponse.stackFrames || [];
}
async function waitForBreakpoint(breakpoint: vscode.SourceBreakpoint, timeoutMs: number, ctx?: Mocha.Context): Promise<void> {
const t0 = performance.now();
console.log(fmt(`Waiting for breakpoint ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}`));
const res = await waitForActiveStackItemChange(timeoutMs);
const t1 = performance.now();
console.log(fmt(`Waiting for breakpoint completed ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}, took ${t1 - t0}ms`));
const stackFrames = await getStackFrames();
if (stackFrames[0].source.path !== breakpoint.location.uri.fsPath || stackFrames[0].line != breakpoint.location.range.start.line+1) {
throw new Error(`Wrong breakpoint was hit. Expected: ${breakpoint.location.uri.fsPath}:${breakpoint.location.range.start.line+1}, Got: ${stackFrames[0].source.path}:${stackFrames[0].line}`);
}
}
enum VariableScope {
Locals = 1,
Members = 2,
Globals = 3
}
async function getVariablesForScope(scope: VariableScope): Promise<DebugProtocol.Variable[]> {
// corresponds to file://./debug_session.ts protected async variablesRequest
const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
variablesReference: scope
});
return variablesResponse?.variables || [];
}
async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise<any> {
// corresponds to file://./debug_session.ts protected async evaluateRequest
const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest("evaluate", {
context,
expression,
frameId
});
return evaluateResponse.body;
}
function formatMs(ms: number): string {
const seconds = Math.floor((ms / 1000) % 60);
const minutes = Math.floor((ms / (1000 * 60)) % 60);
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${(Math.round(ms) % 1000).toString().padStart(3, "0")}`;
}
function formatMessage(this: Mocha.Context, msg: string): string {
return `[${formatMs(performance.now()-this.testStart)}] ${msg}`;
}
var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn"): Promise<void> {
const t0 = performance.now();
const debugConfig: vscode.DebugConfiguration = {
type: "godot",
request: "launch",
name: "Godot Debug",
scene: scene,
additional_options: "--headless"
};
console.log(fmt(`Starting debugger for scene ${scene}`));
const res = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfig);
const t1 = performance.now();
console.log(fmt(`Starting debugger for scene ${scene} completed, took ${t1 - t0}ms`));
if (!res) {
throw new Error(`Failed to start debugging for scene ${scene}`);
}
}
suite("DAP Integration Tests - Variable Scopes", () => {
// workspaceFolder should match `.vscode-test.js`::workspaceFolder
const workspaceFolder = path.resolve(__dirname, "../../../test_projects/test-dap-project-godot4");
suiteSetup(async function() {
this.timeout(20000); // enough time to do `godot --import`
console.log("Environment Variables:");
for (const [key, value] of Object.entries(process.env)) {
console.log(`${key}: ${value}`);
}
// init the godot project by importing it in godot engine:
const config = vscode.workspace.getConfiguration("godotTools");
var godot4_path = config.get<string>("editorPath.godot4");
// get the path for currently opened project in vscode test instance:
var project_path = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
console.log("Executing", [godot4_path, "--headless", "--import", project_path]);
const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", project_path], {shell: true, cwd: project_path});
if (exec_res.stderr !== "") {
throw new Error(exec_res.stderr);
}
console.log(exec_res.stdout);
});
setup(async function() {
console.log(`➤ Test '${this?.currentTest.title}' starting`);
await vscode.commands.executeCommand("workbench.action.closeAllEditors");
if (vscode.debug.breakpoints) {
await vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
}
this.testStart = performance.now();
fmt = formatMessage.bind(this);
});
teardown(async function() {
await sleep(1000);
if (vscode.debug.activeDebugSession !== undefined) {
console.log("Closing debug session");
await vscode.debug.stopDebugging();
}
console.log(`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`);
});
test("sample test", async function() {
// await sleep(1000);
await startDebugging("ScopeVars.tscn");
});
test("should return correct scopes", async function() {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
await startDebugging("ScopeVars.tscn");
await waitForBreakpoint(breakpoint, 2000);
// TODO: current DAP needs a delay before it will return variables
console.log("Sleeping for 2 seconds");
await sleep(2000);
// corresponds to file://./debug_session.ts async scopesRequest
const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: 1});
expect(res_scopes).to.exist;
expect(res_scopes.scopes).to.exist;
const scopes = res_scopes.scopes;
expect(scopes.length).to.equal(3, "Expected 3 scopes");
expect(scopes[0].name).to.equal(VariableScope[VariableScope.Locals], "Expected Locals scope");
expect(scopes[0].variablesReference).to.equal(VariableScope.Locals, "Expected Locals variablesReference");
expect(scopes[1].name).to.equal(VariableScope[VariableScope.Members], "Expected Members scope");
expect(scopes[1].variablesReference).to.equal(VariableScope.Members, "Expected Members variablesReference");
expect(scopes[2].name).to.equal(VariableScope[VariableScope.Globals], "Expected Globals scope");
expect(scopes[2].variablesReference).to.equal(VariableScope.Globals, "Expected Globals variablesReference");
await sleep(1000);
await vscode.debug.stopDebugging();
})?.timeout(5000);
test("should return global variables", async function() {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
await startDebugging("ScopeVars.tscn");
await waitForBreakpoint(breakpoint, 2000);
// TODO: current DAP needs a delay before it will return variables
console.log("Sleeping for 2 seconds");
await sleep(2000);
const variables = await getVariablesForScope(VariableScope.Globals);
expect(variables).to.containSubset([{name: "GlobalScript"}]);
await sleep(1000);
await vscode.debug.stopDebugging();
})?.timeout(7000);
test("should return local variables", async function() {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
await startDebugging("ScopeVars.tscn");
await waitForBreakpoint(breakpoint, 2000);
// TODO: current DAP needs a delay before it will return variables
console.log("Sleeping for 2 seconds");
await sleep(2000);
const variables = await getVariablesForScope(VariableScope.Locals);
expect(variables.length).to.equal(2);
expect(variables).to.containSubset([{name: "local1"}]);
expect(variables).to.containSubset([{name: "local2"}]);
await sleep(1000);
await vscode.debug.stopDebugging();
})?.timeout(5000);
test("should return member variables", async function() {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
await startDebugging("ScopeVars.tscn");
await waitForBreakpoint(breakpoint, 2000);
// TODO: current DAP needs a delay before it will return variables
console.log("Sleeping for 2 seconds");
await sleep(2000);
const variables = await getVariablesForScope(VariableScope.Members);
expect(variables.length).to.equal(2);
expect(variables).to.containSubset([{name: "self"}]);
expect(variables).to.containSubset([{name: "member1"}]);
await sleep(1000);
await vscode.debug.stopDebugging();
})?.timeout(5000);
test("should retrieve all built-in types correctly", async function() {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
await startDebugging("BuiltInTypes.tscn");
await waitForBreakpoint(breakpoint, 2000);
// TODO: current DAP needs a delay before it will return variables
console.log("Sleeping for 2 seconds");
await sleep(2000);
const variables = await getVariablesForScope(VariableScope.Locals);
expect(variables).to.containSubset([{ name: "int_var", value: "42" }]);
expect(variables).to.containSubset([{ name: "float_var", value: "3.14" }]);
expect(variables).to.containSubset([{ name: "bool_var", value: "true" }]);
expect(variables).to.containSubset([{ name: "string_var", value: "Hello, Godot!" }]);
expect(variables).to.containSubset([{ name: "nil_var", value: "null" }]);
expect(variables).to.containSubset([{ name: "vector2", value: "Vector2(10, 20)" }]);
expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]);
expect(variables).to.containSubset([{ name: "rect2", value: "Rect2((0, 0) - (100, 50))" }]);
expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]);
// expect(variables).to.containSubset([{ name: "simple_array", value: "[1, 2, 3]" }]);
expect(variables).to.containSubset([{ name: "simple_array", value: "Array[3]" }]);
// expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]);
// expect(variables).to.containSubset([{ name: "nested_dict.sub_dict.sub_key", value: "99" }]);
expect(variables).to.containSubset([{ name: "nested_dict", value: "Dictionary[2]" }]);
// expect(variables).to.containSubset([{ name: "byte_array", value: "[0, 1, 2, 255]" }]);
expect(variables).to.containSubset([{ name: "byte_array", value: "Array[4]" }]);
// expect(variables).to.containSubset([{ name: "int32_array", value: "[100, 200, 300]" }]);
expect(variables).to.containSubset([{ name: "int32_array", value: "Array[3]" }]);
expect(variables).to.containSubset([{ name: "color_var", value: "Color(1, 0, 0, 1)" }]);
expect(variables).to.containSubset([{ name: "aabb_var", value: "AABB((0, 0, 0), (1, 1, 1))" }]);
expect(variables).to.containSubset([{ name: "plane_var", value: "Plane(0, 1, 0, -5)" }]);
expect(variables).to.containSubset([{ name: "callable_var", value: "Callable()" }]);
expect(variables).to.containSubset([{ name: "signal_var" }]);
const signal_var = variables.find(v => v.name === "signal_var");
expect(signal_var.value).to.match(/Signal\(member_signal\, <\d+>\)/, "Should be in format of 'Signal(member_signal, <28236055815>)'");
await sleep(1000);
await vscode.debug.stopDebugging();
})?.timeout(5000);
test("should retrieve all complex variables correctly", async function() {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
await startDebugging("ExtensiveVars.tscn");
await waitForBreakpoint(breakpoint, 2000);
// TODO: current DAP needs a delay before it will return variables
console.log("Sleeping for 2 seconds");
await sleep(2000);
const memberVariables = await getVariablesForScope(VariableScope.Members);
expect(memberVariables.length).to.equal(3);
expect(memberVariables).to.containSubset([{name: "self"}]);
expect(memberVariables).to.containSubset([{name: "self_var"}]);
const self = memberVariables.find(v => v.name === "self");
const self_var = memberVariables.find(v => v.name === "self_var");
expect(self.value).to.deep.equal(self_var.value);
const localVariables = await getVariablesForScope(VariableScope.Locals);
expect(localVariables.length).to.equal(4);
expect(localVariables).to.containSubset([
{ name: "local_label", value: "Label" },
{ name: "local_self_var_through_label", value: "Node2D" },
{ name: "local_classA", value: "RefCounted" },
{ name: "local_classB", value: "RefCounted" }
]);
await sleep(1000);
await vscode.debug.stopDebugging();
})?.timeout(15000);
});

View File

@@ -36,38 +36,40 @@ export function is_variable_built_in_type(va: GodotVariable) {
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) {
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;
});
if (value) {
if (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 (typeof value["sub_values"] === "function") {
subValues = value.sub_values()?.map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
});
}
}
va.sub_values = subValues;
for (let i = 0; i < subValues?.length; i++) {
subValues[i].sub_values = get_sub_values(subValues[i].value);
}
subValues?.forEach(build_sub_values);
return subValues;
}
export function parse_variable(va: GodotVariable, i?: number) {
@@ -103,7 +105,11 @@ export function parse_variable(va: GodotVariable, i?: number) {
array_type = "named";
reference = i ? i : 0;
} else {
rendered_value = `${value.type_name()}${value.stringify_value()}`;
try {
rendered_value = `${value.type_name()}${value.stringify_value()}`;
} catch (e) {
rendered_value = `${value}`;
}
reference = i ? i : 0;
}
}

View File

@@ -1,17 +1,28 @@
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, 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 * 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, 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");
@@ -34,13 +45,11 @@ export class ServerController {
private server?: net.Server;
private socket?: net.Socket;
private steppingOut = false;
private didFirstOutput: boolean = false;
private didFirstOutput = false;
private partialStackVars = new GodotStackVars();
private connectedVersion = "";
public constructor(
public session: GodotDebugSession
) { }
public constructor(public session: GodotDebugSession) {}
public break() {
this.send_command("break");
@@ -88,12 +97,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("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 +109,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");
@@ -169,17 +174,17 @@ export class ServerController {
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 +230,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,7 +341,7 @@ 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++];
@@ -347,7 +352,7 @@ export class ServerController {
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];
@@ -378,7 +383,7 @@ export class ServerController {
case "scene: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.
@@ -387,19 +392,18 @@ 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
);
}
const sub_values = get_sub_values(rawObject);
const inspect_callback = this.session.inspect_callbacks.get(BigInt(id));
if (inspect_callback !== undefined) {
const inspectedVariable = { name: "", value: rawObject, sub_values: sub_values } as GodotVariable;
inspect_callback(inspectedVariable.name, inspectedVariable);
this.session.inspect_callbacks.delete(BigInt(id));
}
this.session.set_inspection(id, inspectedVariable);
this.session.set_inspection(id, rawObject, sub_values);
break;
}
case "stack_dump": {
@@ -439,13 +443,102 @@ export class ServerController {
}
const lines = command.parameters[0];
for (const line of lines) {
debug.activeDebugConsole.appendLine(line);
debug.activeDebugConsole.appendLine(ansi.bright.blue + 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 +573,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;
}
@@ -518,15 +606,12 @@ export class ServerController {
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") {
commandArray.push(this.threadId);
}
@@ -558,8 +643,8 @@ export class ServerController {
throw new Error("More stack frame variables were sent than expected.");
}
const variable: GodotVariable = { name, value, type };
build_sub_values(variable);
const sub_values = get_sub_values(value);
const variable = { name, value, type, sub_values } as GodotVariable;
const scopeName = ["locals", "members", "globals"][scope];
this.partialStackVars[scopeName].push(variable);

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,15 @@ export class VariantDecoder {
return output;
}
private decode_ContainerTypeFlag(model: BufferModel, type: GDScriptTypes, bitOffset: number) {
const shiftedType = (type >> bitOffset) & 0b11;
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 +225,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 +268,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

@@ -54,7 +54,14 @@ export enum GDScriptTypes {
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 {
BUILTIN = 1,
CLASS_NAME = 2,
SCRIPT = 3,
}
export interface BufferModel {
buffer: Buffer;
@@ -464,7 +471,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,23 +1,13 @@
import {
TreeDataProvider,
EventEmitter,
Event,
ProviderResult,
TreeItem,
TreeItemCollapsibleState,
} from "vscode";
import { TreeDataProvider, EventEmitter, Event, ProviderResult, TreeItem, TreeItemCollapsibleState } from "vscode";
import { GodotVariable, RawObject, ObjectId } from "./debug_runtime";
export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
private _on_did_change_tree_data: EventEmitter<
private _on_did_change_tree_data: EventEmitter<RemoteProperty | undefined> = new EventEmitter<
RemoteProperty | undefined
> = new EventEmitter<RemoteProperty | undefined>();
>();
private tree: RemoteProperty | undefined;
public readonly onDidChangeTreeData: Event<RemoteProperty> | undefined = this
._on_did_change_tree_data.event;
constructor() {}
public readonly onDidChangeTreeData: Event<RemoteProperty> | undefined = this._on_did_change_tree_data.event;
public clean_up() {
if (this.tree) {
@@ -26,12 +16,7 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
}
}
public fill_tree(
element_name: string,
class_name: string,
object_id: number,
variable: GodotVariable
) {
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;
@@ -39,9 +24,7 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
this._on_did_change_tree_data.fire(undefined);
}
public getChildren(
element?: RemoteProperty
): ProviderResult<RemoteProperty[]> {
public getChildren(element?: RemoteProperty): ProviderResult<RemoteProperty[]> {
if (!this.tree) {
return Promise.resolve([]);
}
@@ -57,15 +40,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;
}
@@ -104,13 +83,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 +106,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 +131,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 +149,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 +168,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

@@ -5,6 +5,7 @@ import {
ProviderResult,
TreeItem,
TreeItemCollapsibleState,
Uri
} from "vscode";
import path = require("path");
import { get_extension_uri } from "../utils";
@@ -81,8 +82,8 @@ export class SceneNode extends TreeItem {
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

@@ -134,7 +134,7 @@ function copy_resource_path(uri: vscode.Uri) {
}
async function list_classes() {
await globals.lsp.client.list_classes();
await globals.docsProvider.list_native_classes();
}
async function switch_scene_script() {

View File

@@ -1,61 +1,199 @@
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,
};
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,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

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

@@ -1,3 +1,4 @@
# --- IN ---
func f():
# arithmetic
x += 1
@@ -5,11 +6,19 @@ func f():
x *= 1
x /= 1
x %= 1
x = 2 ** 2
x = 2 * -1
# 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

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,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,10 +50,11 @@ interface Token {
param?: boolean;
string?: boolean;
skip?: boolean;
identifier?: boolean;
}
export interface FormatterOptions {
maxEmptyLines: 1 | 2;
maxEmptyLines: 0 | 1 | 2;
denseFunctionParameters: boolean;
}
@@ -73,6 +74,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 +119,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) {
@@ -128,12 +140,17 @@ function between(tokens: Token[], current: number, options: FormatterOptions) {
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,8 +185,11 @@ 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 (nextToken.identifier) return " ";
if (next === "(") return " ";
if (current === 1) return "";
if (["keyword", "symbol"].includes(tokens[current - 2]?.type)) {
return "";
}
if ([",", "(", "["].includes(tokens[current - 2]?.value)) {
@@ -232,6 +252,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 +280,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 +309,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 +321,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,18 +1,19 @@
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";
const log = createLogger("lsp.manager", { output: "Godot LSP" });
@@ -38,10 +39,8 @@ 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.client = new GDScriptLanguageClient();
this.client.events.on("status", this.on_client_status_changed.bind(this));
setInterval(() => {
this.retry_callback();
@@ -60,7 +59,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)),
@@ -81,7 +80,7 @@ export class ClientConnectionManager {
}
this.reconnectionAttempts = 0;
this.client.connect_to_server(this.target);
this.client.connect(this.target);
}
private stop_language_server() {
@@ -269,7 +268,7 @@ export class ClientConnectionManager {
this.reconnectionAttempts = 0;
set_context("connectedToLSP", true);
this.status = ManagerStatus.CONNECTED;
if (!this.client.started) {
if (this.client.needsStart()) {
this.context.subscriptions.push(this.client.start());
}
break;
@@ -305,7 +304,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;
}

View File

@@ -1,21 +1,17 @@
import EventEmitter from "node:events";
import * as vscode from "vscode";
import {
LanguageClient,
type LanguageClientOptions,
type NotificationMessage,
type RequestMessage,
type ResponseMessage,
type ServerOptions,
} from "vscode-languageclient/node";
import { EventEmitter } from "node:events";
import { get_configuration, createLogger } from "../utils";
import {
type Message,
type MessageIO,
MessageIOReader,
MessageIOWriter,
TCPMessageIO,
WebSocketMessageIO,
} from "./MessageIO";
import { globals } from "../extension";
import { createLogger, get_configuration } from "../utils";
import { MessageIO } from "./MessageIO";
const log = createLogger("lsp.client", { output: "Godot LSP" });
@@ -30,80 +26,80 @@ export enum TargetLSP {
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;
};
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 _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) });
});
const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: "file", language: "gdscript" },
{ scheme: "untitled", language: "gdscript" },
],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.gd"),
},
{
// 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"),
},
},
);
};
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.target = target;
this.status = ClientStatus.PENDING;
@@ -123,70 +119,74 @@ 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) {
private request_filter(message: RequestMessage) {
this.sentMessages.set(message.id, message);
if (message.method === "initialize") {
this._initialize_request = message;
// discard outgoing messages that we know aren't supported
if (message.method === "didChangeWatchedFiles") {
return;
}
if (message.method === "workspace/symbol") {
return;
}
return message;
}
private on_message(message: ResponseMessage | NotificationMessage) {
const msgString = JSON.stringify(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");
// 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:///");
// 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;
}
}
if ("method" in message && message.method === "gdscript/capabilities") {
return message;
}
private notification_filter(message: NotificationMessage) {
if (message.method === "gdscript_client/changeWorkspace") {
//
}
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) {
@@ -194,13 +194,13 @@ export default class GDScriptLanguageClient extends LanguageClient {
textDocument: { uri: uri.toString() },
position: { line: position.line, character: position.character },
};
const response = await this.sendRequest("textDocument/hover", params);
const response: HoverResult = await this.sendRequest("textDocument/hover", params);
return this.parse_hover_response(response);
return this.parse_hover_result(response);
}
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)) {
@@ -229,9 +229,6 @@ 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");
@@ -249,7 +246,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;
}
}
@@ -257,39 +254,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?.result?.range && message.result.contents) {
message.result.range = undefined;
}
// What does this do?
if (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,21 +3,28 @@
* 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";
export default class MessageBuffer {
private encoding: BufferEncoding;
private index: number;
private buffer: Buffer;
type Headers = { [key: string]: string };
constructor(encoding = "utf8") {
this.encoding = encoding as BufferEncoding;
this.index = 0;
this.buffer = Buffer.allocUnsafe(DefaultSize);
}
export default class MessageBuffer {
private encoding: BufferEncoding = "utf8";
private index = 0;
private buffer: Buffer = Buffer.allocUnsafe(DefaultSize);
private nextMessageLength: number;
private messageToken: number;
private partialMessageTimer: NodeJS.Timeout | undefined;
private _partialMessageTimeout = 10000;
constructor(private reader) {}
public append(chunk: Buffer | string): void {
let toAppend: Buffer = <Buffer>chunk;
@@ -41,8 +48,7 @@ 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 &&
@@ -55,19 +61,19 @@ export default class MessageBuffer {
}
// No header / body separator found (e.g CRLFCRLF)
if (current + 3 >= this.index) {
return result;
return undefined;
}
result = Object.create(null);
const result = Object.create(null);
const headers = this.buffer.toString("ascii", 0, current).split(CRLF);
for (const header of headers) {
for (const header of headers) {
const index: number = header.indexOf(":");
if (index === -1) {
throw new Error("Message header must separate key and value using :");
}
const key = header.substr(0, index);
const value = header.substr(index + 1).trim();
result![key] = value;
}
result[key] = value;
}
const nextStart = current + 4;
this.buffer = this.buffer.slice(nextStart);
@@ -86,7 +92,66 @@ export default class MessageBuffer {
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

@@ -10,7 +10,6 @@ import {
type MessageWriter,
} from "vscode-jsonrpc";
import { EventEmitter } from "node:events";
import { WebSocket, type Data } from "ws";
import { Socket } from "net";
import MessageBuffer from "./MessageBuffer";
import { createLogger } from "../utils";
@@ -20,252 +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 = (msg) => msg;
responseFilter: (msg: ResponseMessage) => ResponseMessage = (msg) => msg;
notificationFilter: (msg: NotificationMessage) => NotificationMessage = (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);
this.socket = socket;
while (this.messageCache.length > 0) {
const msg = this.messageCache.shift();
this.socket.write(msg);
}
this.emit("connected");
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("data", (chunk: Buffer) => {
this.emit("data", chunk.toString());
});
// 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 = Number.parseInt(contentLength);
if (Number.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
}
const 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++;
const json = JSON.parse(msg);
// allow message to be modified
let modified: ResponseMessage | NotificationMessage;
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);
}
log.debug("rx:", json);
if (!modified) {
log.debug("rx [discarded]:", json);
return;
}
log.debug("rx:", modified);
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();
}
},
this._partialMessageTimeout,
this.messageToken,
this._partialMessageTimeout,
);
}
}
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> {
// discard outgoing messages that we know aren't supported
if ((msg as RequestMessage).method === "didChangeWatchedFiles") {
async write(msg: RequestMessage) {
const modified = this.io.requestFilter(msg);
if (!modified) {
log.debug("tx [discarded]:", msg);
return;
}
if ((msg as RequestMessage).method === "workspace/symbol") {
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

@@ -14,7 +14,7 @@ import type {
GodotNativeSymbol,
GodotNativeClassInfo,
GodotCapabilities,
} from "../lsp/gdscript.capabilities";
} from "./documentation_types";
import { make_html_content } from "./documentation_builder";
import { createLogger, get_configuration, get_extension_uri, make_docs_uri } from "../utils";
import { globals } from "../extension";
@@ -114,8 +114,17 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
}
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) => {
if (msg.type === "INSPECT_NATIVE_SYMBOL") {

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 type { GodotNativeSymbol } from "../lsp/gdscript.capabilities";
import type { GodotNativeSymbol } from "./documentation_types";
import { get_extension_uri } from "../utils";
import yabbcode = require("ya-bbcode");
@@ -78,12 +78,12 @@ export function make_html_content(webview: vscode.Webview, symbol: GodotNativeSy
<title>${symbol.name}</title>
</head>
<body style="line-height: scaleFactor%; font-size: scaleFactor%;">
<body style="line-height: scaleFactor%; font-size: scaleFactor%; margin-right: bodyMargin">
<main>
${make_symbol_document(symbol)}
</main>
<canvas id='minimap'></canvas>
<canvas id='minimap' style="display: displayMinimap"></canvas>
<script src="${pagemapJsUri}"></script>
<script>
@@ -128,7 +128,7 @@ 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, {
@@ -180,7 +180,7 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
}
const args = (parts[2] || "").replace(
/\:\s([A-z0-9_]+)(\,\s*)?/g,
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2",
': <a href="" onclick="inspect(\'$1\')">$1</a>$2',
);
const title = element(
"p",
@@ -233,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;
}
}
}
@@ -330,7 +332,7 @@ function format_documentation(bbcode: string, classname: string) {
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];

View File

@@ -131,7 +131,7 @@ export class ScenePreviewProvider
// 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}`,
`@onready var ${node_name_to_snake(label)}: ${className} = ${qualifiedPath}\n`,
);
}

View File

@@ -2,6 +2,7 @@ import {
TreeItem,
TreeItemCollapsibleState,
MarkdownString,
Uri
} from "vscode";
import * as path from "path";
import { get_extension_uri } from "../utils";
@@ -31,8 +32,8 @@ export class SceneNode extends TreeItem {
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)),
};
}

View File

@@ -1,7 +1,7 @@
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";
@@ -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

@@ -105,8 +105,8 @@ 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 type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
export type VERIFY_RESULT = {
status: VERIFY_STATUS;
godotPath: string;
version?: string;

View File

@@ -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|abstract|is|onready|tool|static|export|as|void|enum|assert|breakpoint|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace)\\b",
"name": "keyword.language.gdscript"
},
"letter": {
@@ -263,15 +263,15 @@
"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_]*[eE][+-]?[0-9_]+",
"name": "constant.numeric.float.gdscript"
},
{
@@ -367,7 +367,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 +386,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,19 +430,19 @@
]
},
"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": "(@)(export|export_group|export_color_no_alpha|export_custom|export_dir|export_enum|export_exp_easing|export_file|export_flags|export_flags_2d_navigation|export_flags_2d_physics|export_flags_2d_render|export_flags_3d_navigation|export_flags_3d_physics|export_flags_3d_render|export_global_dir|export_global_file|export_multiline|export_node_path|export_placeholder|export_range|export_storage|icon|onready|rpc|tool|warning_ignore|static_unload)\\b",
"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|super)\\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",
@@ -498,13 +498,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" },

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,39 @@
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 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,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,38 @@
extends Node2D
var self_var := self
@onready var label: ExtensiveVars_Label = $Label
class ClassA:
var member_classB
var member_self := self
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
# 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 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
pass

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,3 @@
extends Node
var globalMember := "global member"

View File

@@ -0,0 +1,8 @@
extends Node
@onready var parent_node: Node2D = $".."
@onready var sibling_node2: Node = $"../node2"
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.

View File

@@ -0,0 +1,9 @@
extends Node2D
@onready var node_1: Node = $node1
@onready var node_2: Node = $node2
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
print("breakpoint::NodeVars::_ready")
pass

View File

@@ -0,0 +1,17 @@
[gd_scene load_steps=3 format=3 uid="uid://xrjtth0d2nc5"]
[ext_resource type="Script" path="res://NodeVars.gd" id="1_6eeca"]
[ext_resource type="Script" path="res://Node1.gd" id="2_bl41t"]
[node name="NodeVars" type="Node2D"]
script = ExtResource("1_6eeca")
[node name="node1" type="Node" parent="."]
script = ExtResource("2_bl41t")
[node name="node2" type="Node" parent="."]
[node name="Label" type="Label" parent="."]
offset_right = 40.0
offset_bottom = 23.0
text = "NodeVars"

View File

@@ -0,0 +1,8 @@
extends Node
var member1 := TestClassA.new()
func _ready() -> void:
var local1 := TestClassA.new()
var local2 = GlobalScript.globalMember
print("breakpoint::ScopeVars::_ready")

View File

@@ -0,0 +1,11 @@
[gd_scene load_steps=2 format=3 uid="uid://g5gqewj2i2xs"]
[ext_resource type="Script" path="res://ScopeVars.gd" id="1_wtcpp"]
[node name="RootNode" type="Node"]
script = ExtResource("1_wtcpp")
[node name="Label" type="Label" parent="."]
offset_right = 40.0
offset_bottom = 23.0
text = "Godot test project"

View File

@@ -0,0 +1,5 @@
class_name TestClassA
var testclassa_member1 := "member1"
var testclassa_member2: Node

View File

@@ -0,0 +1,19 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Test DAP project godot4"
run/main_scene="res://ScopeVars.tscn"
config/features=PackedStringArray("4.3", "Forward Plus")
[autoload]
GlobalScript="*res://GlobalScript.gd"

View File

@@ -14,7 +14,8 @@
"rootDir": "src",
"strict": false,
"skipLibCheck": true,
"allowJs": true
"allowJs": true,
"strictBindCallApply": true
},
"exclude": [
"node_modules",