mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2025-12-31 13:48:24 +03:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7844979c90 | ||
|
|
9297920d73 | ||
|
|
8059ba89c2 | ||
|
|
2490d0cdad | ||
|
|
7c70ac2753 | ||
|
|
e7b9530a7f | ||
|
|
002cfa18a3 | ||
|
|
489db36e85 | ||
|
|
996a7aefe6 | ||
|
|
2e9117870d | ||
|
|
aee83dd2a4 | ||
|
|
6ddf05d4a1 | ||
|
|
f648c37353 | ||
|
|
709fa1bbad | ||
|
|
694feea1bc | ||
|
|
fd637d0641 | ||
|
|
c33982d38e | ||
|
|
43bb36ca30 | ||
|
|
96510971f4 | ||
|
|
0a632d62b5 | ||
|
|
4404b76006 | ||
|
|
1c32bbb1cb | ||
|
|
86ae182088 | ||
|
|
658270e742 | ||
|
|
170d3d4819 | ||
|
|
1a84a57647 | ||
|
|
9b16946ba9 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -29,7 +29,7 @@ body:
|
||||
Use the **Help > About** menu to see your current version.
|
||||
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.2.0"
|
||||
placeholder: "2.4.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -18,7 +18,7 @@ body:
|
||||
description: >
|
||||
Specify the Git commit hash if using a development or non-official build.
|
||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||
placeholder: 4.2.2.stable, 4.3.rc (88d932506)
|
||||
placeholder: 4.3.stable, 4.4.dev1 (28a72fa43)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -29,7 +29,7 @@ body:
|
||||
Use the **Help > About** menu to see your current version.
|
||||
Specify the Git commit hash if using a development or non-official build.
|
||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||
placeholder: "1.91.1"
|
||||
placeholder: "1.93.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -40,7 +40,7 @@ body:
|
||||
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
|
||||
Specify the Git commit hash if using a development or non-official build.
|
||||
If you use a custom build, please test if your issue is reproducible in official builds too.
|
||||
placeholder: "2.1.0"
|
||||
placeholder: "2.3.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -4,3 +4,5 @@ node_modules
|
||||
.vscode-test
|
||||
workspace.code-workspace
|
||||
.history
|
||||
.godot
|
||||
*.tmp
|
||||
@@ -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
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -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
48
.vscode/test_files.code-snippets
vendored
Normal 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"
|
||||
},
|
||||
}
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# 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)
|
||||
- [Suppress "workspace/symbol" not found error](https://github.com/godotengine/godot-vscode-plugin/pull/723)
|
||||
- [Capitalize the drive letter in Windows absolute paths](https://github.com/godotengine/godot-vscode-plugin/pull/727)
|
||||
|
||||
### 2.2.0
|
||||
|
||||
- [Add partial debugger support for new types (such as typed arrays)](https://github.com/godotengine/godot-vscode-plugin/pull/715)
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"noUselessElse": "off"
|
||||
"noUselessElse": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
body {
|
||||
margin-right: 200px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#map {
|
||||
#minimap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
211
package-lock.json
generated
211
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "godot-tools",
|
||||
"version": "2.2.0",
|
||||
"version": "2.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "godot-tools",
|
||||
"version": "2.2.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"
|
||||
|
||||
39
package.json
39
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "godot-tools",
|
||||
"displayName": "godot-tools",
|
||||
"icon": "icon.png",
|
||||
"version": "2.2.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",
|
||||
@@ -251,6 +251,18 @@
|
||||
"type": "object",
|
||||
"title": "Godot Tools",
|
||||
"properties": {
|
||||
"godotTools.documentation.pageScale": {
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"minimum": 50,
|
||||
"maximum": 200,
|
||||
"description": "Scale factor (%) to apply to the Godot documentation viewer."
|
||||
},
|
||||
"godotTools.documentation.displayMinimap":{
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Whether to display the minimap for the Godot documentation viewer."
|
||||
},
|
||||
"godotTools.editorPath.godot3": {
|
||||
"type": "string",
|
||||
"default": "godot3",
|
||||
@@ -289,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",
|
||||
@@ -874,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",
|
||||
@@ -887,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",
|
||||
@@ -896,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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
359
src/debugger/godot4/debugger_variables.test.ts
Normal file
359
src/debugger/godot4/debugger_variables.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
101
src/formatter/snapshots/README.md
Normal file
101
src/formatter/snapshots/README.md
Normal 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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
5
src/formatter/snapshots/double_dot_notation.gd
Normal file
5
src/formatter/snapshots/double_dot_notation.gd
Normal 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)
|
||||
25
src/formatter/snapshots/float_notation.gd
Normal file
25
src/formatter/snapshots/float_notation.gd
Normal 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))
|
||||
4
src/formatter/snapshots/lambda_multiline_spacing.gd
Normal file
4
src/formatter/snapshots/lambda_multiline_spacing.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
# --- IN ---
|
||||
poll_animation.on_complete(func() -> void:
|
||||
highlight.visible = false
|
||||
)
|
||||
17
src/formatter/snapshots/leave_strings_alone.gd
Normal file
17
src/formatter/snapshots/leave_strings_alone.gd
Normal 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},
|
||||
}
|
||||
"""
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"maxEmptyLines": 1
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
|
||||
|
||||
|
||||
class Test:
|
||||
|
||||
|
||||
|
||||
|
||||
func _ready():
|
||||
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
func test():
|
||||
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
# comments
|
||||
func with_comments():
|
||||
|
||||
pass
|
||||
@@ -1,14 +0,0 @@
|
||||
class Test:
|
||||
|
||||
func _ready():
|
||||
|
||||
pass
|
||||
|
||||
func test():
|
||||
|
||||
pass
|
||||
|
||||
# comments
|
||||
func with_comments():
|
||||
|
||||
pass
|
||||
@@ -1,28 +0,0 @@
|
||||
|
||||
|
||||
|
||||
class Test:
|
||||
|
||||
|
||||
|
||||
|
||||
func _ready():
|
||||
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
func test():
|
||||
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
# comments
|
||||
func with_comments():
|
||||
|
||||
|
||||
pass
|
||||
@@ -1,20 +0,0 @@
|
||||
class Test:
|
||||
|
||||
|
||||
func _ready():
|
||||
|
||||
|
||||
pass
|
||||
|
||||
|
||||
func test():
|
||||
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# comments
|
||||
func with_comments():
|
||||
|
||||
|
||||
pass
|
||||
72
src/formatter/snapshots/max_empty_lines.gd
Normal file
72
src/formatter/snapshots/max_empty_lines.gd
Normal 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
|
||||
23
src/formatter/snapshots/multiline_leading_minus.gd
Normal file
23
src/formatter/snapshots/multiline_leading_minus.gd
Normal 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
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
6
src/formatter/snapshots/scientific_notation.gd
Normal file
6
src/formatter/snapshots/scientific_notation.gd
Normal 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
|
||||
14
src/formatter/snapshots/test.gd
Normal file
14
src/formatter/snapshots/test.gd
Normal 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'
|
||||
@@ -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));
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
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" });
|
||||
|
||||
enum ManagerStatus {
|
||||
INITIALIZING,
|
||||
INITIALIZING_LSP,
|
||||
PENDING,
|
||||
PENDING_LSP,
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
RETRYING,
|
||||
INITIALIZING = 0,
|
||||
INITIALIZING_LSP = 1,
|
||||
PENDING = 2,
|
||||
PENDING_LSP = 3,
|
||||
DISCONNECTED = 4,
|
||||
CONNECTED = 5,
|
||||
RETRYING = 6,
|
||||
}
|
||||
|
||||
export class ClientConnectionManager {
|
||||
@@ -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() {
|
||||
@@ -126,16 +125,18 @@ export class ClientConnectionManager {
|
||||
|
||||
if (result.version[2] < minimumVersion) {
|
||||
const message = `Cannot launch headless LSP: Headless LSP mode is only available on v${targetVersion} or newer, but the specified Godot executable is v${result.version}.`;
|
||||
vscode.window.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore").then(item => {
|
||||
if (item === "Select Godot executable") {
|
||||
select_godot_executable(settingName);
|
||||
} else if (item === "Open Settings") {
|
||||
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
|
||||
} else if (item === "Disable Headless LSP") {
|
||||
set_configuration("lsp.headless", false);
|
||||
prompt_for_reload();
|
||||
}
|
||||
});
|
||||
vscode.window
|
||||
.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore")
|
||||
.then((item) => {
|
||||
if (item === "Select Godot executable") {
|
||||
select_godot_executable(settingName);
|
||||
} else if (item === "Open Settings") {
|
||||
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
|
||||
} else if (item === "Disable Headless LSP") {
|
||||
set_configuration("lsp.headless", false);
|
||||
prompt_for_reload();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,7 +198,7 @@ export class ClientConnectionManager {
|
||||
if (this.target === TargetLSP.HEADLESS) {
|
||||
options = ["Restart LSP", ...options];
|
||||
}
|
||||
vscode.window.showInformationMessage(message, ...options).then(item => {
|
||||
vscode.window.showInformationMessage(message, ...options).then((item) => {
|
||||
if (item === "Restart LSP") {
|
||||
this.connect_to_language_server();
|
||||
}
|
||||
@@ -267,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;
|
||||
@@ -303,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;
|
||||
}
|
||||
@@ -324,7 +325,7 @@ export class ClientConnectionManager {
|
||||
options = ["Open workspace with Godot Editor", ...options];
|
||||
}
|
||||
|
||||
vscode.window.showErrorMessage(message, ...options).then(item => {
|
||||
vscode.window.showErrorMessage(message, ...options).then((item) => {
|
||||
if (item === "Retry") {
|
||||
this.connect_to_language_server();
|
||||
}
|
||||
|
||||
@@ -1,92 +1,105 @@
|
||||
import EventEmitter from "node:events";
|
||||
import * as vscode from "vscode";
|
||||
import { LanguageClient, NotificationMessage, RequestMessage, ResponseMessage } from "vscode-languageclient/node";
|
||||
import { EventEmitter } from "events";
|
||||
import { get_configuration, createLogger } from "../utils";
|
||||
import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
|
||||
import {
|
||||
LanguageClient,
|
||||
type LanguageClientOptions,
|
||||
type NotificationMessage,
|
||||
type RequestMessage,
|
||||
type ResponseMessage,
|
||||
type ServerOptions,
|
||||
} from "vscode-languageclient/node";
|
||||
|
||||
import { globals } from "../extension";
|
||||
import { createLogger, get_configuration } from "../utils";
|
||||
import { MessageIO } from "./MessageIO";
|
||||
|
||||
const log = createLogger("lsp.client", { output: "Godot LSP" });
|
||||
|
||||
export enum ClientStatus {
|
||||
PENDING,
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
PENDING = 0,
|
||||
DISCONNECTED = 1,
|
||||
CONNECTED = 2,
|
||||
}
|
||||
|
||||
export enum TargetLSP {
|
||||
HEADLESS,
|
||||
EDITOR,
|
||||
HEADLESS = 0,
|
||||
EDITOR = 1,
|
||||
}
|
||||
|
||||
const CUSTOM_MESSAGE = "gdscript_client/";
|
||||
export type Target = {
|
||||
host: string;
|
||||
port: number;
|
||||
type: TargetLSP;
|
||||
};
|
||||
|
||||
type HoverResult = {
|
||||
contents: {
|
||||
kind: string;
|
||||
value: string;
|
||||
};
|
||||
range: {
|
||||
end: {
|
||||
character: number;
|
||||
line: number;
|
||||
};
|
||||
start: {
|
||||
character: number;
|
||||
line: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type HoverResponseMesssage = {
|
||||
id: number;
|
||||
jsonrpc: string;
|
||||
result: HoverResult;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -106,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) {
|
||||
@@ -177,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)) {
|
||||
@@ -212,11 +229,8 @@ export default class GDScriptLanguageClient extends LanguageClient {
|
||||
}
|
||||
|
||||
private on_connected() {
|
||||
if (this._initialize_request) {
|
||||
this.io.writer.write(this._initialize_request);
|
||||
}
|
||||
this.status = ClientStatus.CONNECTED;
|
||||
|
||||
|
||||
const host = get_configuration("lsp.serverHost");
|
||||
log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
|
||||
}
|
||||
@@ -232,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;
|
||||
}
|
||||
}
|
||||
@@ -240,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 && message.result && message.result.range && message.result.contents) {
|
||||
message.result.range = undefined;
|
||||
}
|
||||
|
||||
// What does this do?
|
||||
if (message && message.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) {
|
||||
const method = (message.method as string).substring(CUSTOM_MESSAGE.length, message.method.length);
|
||||
if (this[method]) {
|
||||
const ret = this[method](message.params);
|
||||
if (ret) {
|
||||
this.io.writer.write(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,41 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
|
||||
import { createLogger } from "../utils";
|
||||
|
||||
const log = createLogger("lsp.buf");
|
||||
|
||||
const DefaultSize: number = 8192;
|
||||
const CR: number = Buffer.from('\r', 'ascii')[0];
|
||||
const LF: number = Buffer.from('\n', 'ascii')[0];
|
||||
const CRLF: string = '\r\n';
|
||||
const CR: number = Buffer.from("\r", "ascii")[0];
|
||||
const LF: number = Buffer.from("\n", "ascii")[0];
|
||||
const CRLF: string = "\r\n";
|
||||
|
||||
type Headers = { [key: string]: string };
|
||||
|
||||
export default class MessageBuffer {
|
||||
private encoding: BufferEncoding = "utf8";
|
||||
private index = 0;
|
||||
private buffer: Buffer = Buffer.allocUnsafe(DefaultSize);
|
||||
|
||||
private encoding: BufferEncoding;
|
||||
private index: number;
|
||||
private buffer: Buffer;
|
||||
private nextMessageLength: number;
|
||||
private messageToken: number;
|
||||
private partialMessageTimer: NodeJS.Timeout | undefined;
|
||||
private _partialMessageTimeout = 10000;
|
||||
|
||||
constructor(encoding: string = 'utf8') {
|
||||
this.encoding = encoding as BufferEncoding;
|
||||
this.index = 0;
|
||||
this.buffer = Buffer.allocUnsafe(DefaultSize);
|
||||
}
|
||||
constructor(private reader) {}
|
||||
|
||||
public append(chunk: Buffer | String): void {
|
||||
var toAppend: Buffer = <Buffer>chunk;
|
||||
if (typeof (chunk) === 'string') {
|
||||
var str = <string>chunk;
|
||||
var bufferLen = Buffer.byteLength(str, this.encoding);
|
||||
public append(chunk: Buffer | string): void {
|
||||
let toAppend: Buffer = <Buffer>chunk;
|
||||
if (typeof chunk === "string") {
|
||||
const str = <string>chunk;
|
||||
const bufferLen = Buffer.byteLength(str, this.encoding);
|
||||
toAppend = Buffer.allocUnsafe(bufferLen);
|
||||
toAppend.write(str, 0, bufferLen, this.encoding);
|
||||
}
|
||||
if (this.buffer.length - this.index >= toAppend.length) {
|
||||
toAppend.copy(this.buffer, this.index, 0, toAppend.length);
|
||||
} else {
|
||||
var newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
|
||||
const newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
|
||||
if (this.index === 0) {
|
||||
this.buffer = Buffer.allocUnsafe(newSize);
|
||||
toAppend.copy(this.buffer, 0, 0, toAppend.length);
|
||||
@@ -42,29 +48,34 @@ export default class MessageBuffer {
|
||||
this.index += toAppend.length;
|
||||
}
|
||||
|
||||
public tryReadHeaders(): { [key: string]: string; } | undefined {
|
||||
let result: { [key: string]: string; } | undefined = undefined;
|
||||
public tryReadHeaders(): Headers | undefined {
|
||||
let current = 0;
|
||||
while (current + 3 < this.index && (this.buffer[current] !== CR || this.buffer[current + 1] !== LF || this.buffer[current + 2] !== CR || this.buffer[current + 3] !== LF)) {
|
||||
while (
|
||||
current + 3 < this.index &&
|
||||
(this.buffer[current] !== CR ||
|
||||
this.buffer[current + 1] !== LF ||
|
||||
this.buffer[current + 2] !== CR ||
|
||||
this.buffer[current + 3] !== LF)
|
||||
) {
|
||||
current++;
|
||||
}
|
||||
// No header / body separator found (e.g CRLFCRLF)
|
||||
if (current + 3 >= this.index) {
|
||||
return result;
|
||||
return undefined;
|
||||
}
|
||||
result = Object.create(null);
|
||||
let headers = this.buffer.toString('ascii', 0, current).split(CRLF);
|
||||
headers.forEach((header) => {
|
||||
let index: number = header.indexOf(':');
|
||||
const result = Object.create(null);
|
||||
const headers = this.buffer.toString("ascii", 0, current).split(CRLF);
|
||||
for (const header of headers) {
|
||||
const index: number = header.indexOf(":");
|
||||
if (index === -1) {
|
||||
throw new Error('Message header must separate key and value using :');
|
||||
throw new Error("Message header must separate key and value using :");
|
||||
}
|
||||
let key = header.substr(0, index);
|
||||
let value = header.substr(index + 1).trim();
|
||||
result![key] = value;
|
||||
});
|
||||
const key = header.substr(0, index);
|
||||
const value = header.substr(index + 1).trim();
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
let nextStart = current + 4;
|
||||
const nextStart = current + 4;
|
||||
this.buffer = this.buffer.slice(nextStart);
|
||||
this.index = this.index - nextStart;
|
||||
return result;
|
||||
@@ -74,14 +85,73 @@ export default class MessageBuffer {
|
||||
if (this.index < length) {
|
||||
return null;
|
||||
}
|
||||
let result = this.buffer.toString(this.encoding, 0, length);
|
||||
let nextStart = length;
|
||||
const result = this.buffer.toString(this.encoding, 0, length);
|
||||
const nextStart = length;
|
||||
this.buffer.copy(this.buffer, 0, nextStart);
|
||||
this.index = this.index - nextStart;
|
||||
return result;
|
||||
}
|
||||
|
||||
public get numberOfBytes(): number {
|
||||
return this.index;
|
||||
public ready() {
|
||||
if (this.nextMessageLength === -1) {
|
||||
const headers = this.tryReadHeaders();
|
||||
if (!headers) {
|
||||
return;
|
||||
}
|
||||
const contentLength = headers["Content-Length"];
|
||||
if (!contentLength) {
|
||||
log.warn("Header must provide a Content-Length property.");
|
||||
return;
|
||||
}
|
||||
const length = Number.parseInt(contentLength);
|
||||
if (Number.isNaN(length)) {
|
||||
log.warn("Content-Length value must be a number.");
|
||||
return;
|
||||
}
|
||||
this.nextMessageLength = length;
|
||||
}
|
||||
const msg = this.tryReadContent(this.nextMessageLength);
|
||||
if (!msg) {
|
||||
log.warn("haven't recieved full message");
|
||||
this.setPartialMessageTimer();
|
||||
return;
|
||||
}
|
||||
this.clearPartialMessageTimer();
|
||||
this.nextMessageLength = -1;
|
||||
this.messageToken++;
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.nextMessageLength = -1;
|
||||
this.messageToken = 0;
|
||||
this.partialMessageTimer = undefined;
|
||||
}
|
||||
|
||||
private clearPartialMessageTimer(): void {
|
||||
if (this.partialMessageTimer) {
|
||||
clearTimeout(this.partialMessageTimer);
|
||||
this.partialMessageTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private setPartialMessageTimer(): void {
|
||||
this.clearPartialMessageTimer();
|
||||
if (this._partialMessageTimeout <= 0) {
|
||||
return;
|
||||
}
|
||||
this.partialMessageTimer = setTimeout(
|
||||
(token, timeout) => {
|
||||
this.partialMessageTimer = undefined;
|
||||
if (token === this.messageToken) {
|
||||
this.reader.firePartialMessage({ messageToken: token, waitingTime: timeout });
|
||||
this.setPartialMessageTimer();
|
||||
}
|
||||
},
|
||||
this._partialMessageTimeout,
|
||||
this.messageToken,
|
||||
this._partialMessageTimeout,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {
|
||||
AbstractMessageReader,
|
||||
MessageReader,
|
||||
DataCallback,
|
||||
Disposable,
|
||||
RequestMessage,
|
||||
ResponseMessage,
|
||||
NotificationMessage,
|
||||
type MessageReader,
|
||||
type DataCallback,
|
||||
type Disposable,
|
||||
type RequestMessage,
|
||||
type ResponseMessage,
|
||||
type NotificationMessage,
|
||||
AbstractMessageWriter,
|
||||
MessageWriter
|
||||
type MessageWriter,
|
||||
} from "vscode-jsonrpc";
|
||||
import { EventEmitter } from "events";
|
||||
import { WebSocket, Data } from "ws";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { Socket } from "net";
|
||||
import MessageBuffer from "./MessageBuffer";
|
||||
import { createLogger } from "../utils";
|
||||
@@ -20,243 +19,132 @@ const log = createLogger("lsp.io", { output: "Godot LSP" });
|
||||
export type Message = RequestMessage | ResponseMessage | NotificationMessage;
|
||||
|
||||
export class MessageIO extends EventEmitter {
|
||||
reader: MessageIOReader = null;
|
||||
writer: MessageIOWriter = null;
|
||||
reader = new MessageIOReader(this);
|
||||
writer = new MessageIOWriter(this);
|
||||
|
||||
public send_message(message: string) {
|
||||
// virtual
|
||||
}
|
||||
requestFilter: (msg: RequestMessage) => RequestMessage = (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); resolve(); });
|
||||
socket.on("data", this.on_message.bind(this));
|
||||
socket.on("end", this.on_disconnected.bind(this));
|
||||
socket.on("close", this.on_disconnected.bind(this));
|
||||
socket.on("error", this.on_error.bind(this));
|
||||
|
||||
socket.on("connect", () => {
|
||||
this.socket = socket;
|
||||
|
||||
while (this.messageCache.length > 0) {
|
||||
const msg = this.messageCache.shift();
|
||||
this.socket.write(msg);
|
||||
}
|
||||
|
||||
this.emit("connected");
|
||||
resolve();
|
||||
});
|
||||
socket.on("data", (chunk: Buffer) => {
|
||||
this.emit("data", chunk.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 = parseInt(contentLength);
|
||||
if (isNaN(length)) {
|
||||
throw new Error("Content-Length value must be a number.");
|
||||
}
|
||||
this.nextMessageLength = length;
|
||||
// Take the encoding form the header. For compatibility
|
||||
// treat both utf-8 and utf8 as node utf8
|
||||
}
|
||||
var msg = this.buffer.tryReadContent(this.nextMessageLength);
|
||||
if (msg === null) {
|
||||
/** We haven't received the full message yet. */
|
||||
this.setPartialMessageTimer();
|
||||
const msg = this.buffer.ready();
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
this.clearPartialMessageTimer();
|
||||
this.nextMessageLength = -1;
|
||||
this.messageToken++;
|
||||
var json = JSON.parse(msg);
|
||||
|
||||
log.debug("rx:", json);
|
||||
this.callback(json);
|
||||
// callback
|
||||
this.io.on_message_callback(json);
|
||||
}
|
||||
}
|
||||
|
||||
private clearPartialMessageTimer(): void {
|
||||
if (this.partialMessageTimer) {
|
||||
clearTimeout(this.partialMessageTimer);
|
||||
this.partialMessageTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private setPartialMessageTimer(): void {
|
||||
this.clearPartialMessageTimer();
|
||||
if (this._partialMessageTimeout <= 0) {
|
||||
return;
|
||||
}
|
||||
this.partialMessageTimer = setTimeout((token, timeout) => {
|
||||
this.partialMessageTimer = undefined;
|
||||
if (token === this.messageToken) {
|
||||
this.firePartialMessage({ messageToken: token, waitingTime: timeout });
|
||||
this.setPartialMessageTimer();
|
||||
const json = JSON.parse(msg);
|
||||
// allow message to be modified
|
||||
let modified: ResponseMessage | NotificationMessage;
|
||||
if ("id" in json) {
|
||||
modified = this.io.responseFilter(json);
|
||||
} else if ("method" in json) {
|
||||
modified = this.io.notificationFilter(json);
|
||||
} else {
|
||||
log.warn("rx [unhandled]:", json);
|
||||
}
|
||||
}, this._partialMessageTimeout, this.messageToken, this._partialMessageTimeout);
|
||||
|
||||
if (!modified) {
|
||||
log.debug("rx [discarded]:", json);
|
||||
return;
|
||||
}
|
||||
log.debug("rx:", modified);
|
||||
this.callback(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ContentLength: string = "Content-Length: ";
|
||||
const CRLF = "\r\n";
|
||||
export class MessageIOWriter extends AbstractMessageWriter implements MessageWriter {
|
||||
private io: MessageIO;
|
||||
private encoding: BufferEncoding;
|
||||
private errorCount: number;
|
||||
|
||||
public constructor(io: MessageIO, encoding: BufferEncoding = "utf8") {
|
||||
constructor(public io: MessageIO) {
|
||||
super();
|
||||
this.io = io;
|
||||
this.io.writer = this;
|
||||
this.encoding = encoding as BufferEncoding;
|
||||
this.errorCount = 0;
|
||||
this.io.on("error", (error: any) => this.fireError(error));
|
||||
this.io.on("close", () => this.fireClose());
|
||||
}
|
||||
|
||||
public end(): void {
|
||||
|
||||
}
|
||||
|
||||
public write(msg: Message): Promise<void> {
|
||||
if ((msg as RequestMessage).method === "didChangeWatchedFiles") {
|
||||
async write(msg: RequestMessage) {
|
||||
const modified = this.io.requestFilter(msg);
|
||||
if (!modified) {
|
||||
log.debug("tx [discarded]:", msg);
|
||||
return;
|
||||
}
|
||||
const json = JSON.stringify(msg);
|
||||
const contentLength = Buffer.byteLength(json, this.encoding);
|
||||
log.debug("tx:", modified);
|
||||
const json = JSON.stringify(modified);
|
||||
|
||||
const headers: string[] = [
|
||||
ContentLength, contentLength.toString(), CRLF,
|
||||
CRLF
|
||||
];
|
||||
const contentLength = Buffer.byteLength(json, "utf-8").toString();
|
||||
const message = `Content-Length: ${contentLength}\r\n\r\n${json}`;
|
||||
try {
|
||||
// callback
|
||||
this.io.on_send_message(msg);
|
||||
// Header must be written in ASCII encoding
|
||||
this.io.send_message(headers.join(""));
|
||||
// Now write the content. This can be written in any encoding
|
||||
|
||||
log.debug("tx:", msg);
|
||||
this.io.send_message(json);
|
||||
this.io.write(message);
|
||||
this.errorCount = 0;
|
||||
} catch (error) {
|
||||
this.errorCount++;
|
||||
this.fireError(error, msg, this.errorCount);
|
||||
this.fireError(error, modified, this.errorCount);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
end(): void {}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as vscode from "vscode";
|
||||
import {
|
||||
Uri,
|
||||
Range,
|
||||
TextDocument,
|
||||
CancellationToken,
|
||||
type TextDocument,
|
||||
type CancellationToken,
|
||||
DocumentLink,
|
||||
DocumentLinkProvider,
|
||||
ExtensionContext,
|
||||
type DocumentLinkProvider,
|
||||
type ExtensionContext,
|
||||
} from "vscode";
|
||||
import { SceneParser } from "../scene_tools";
|
||||
import { convert_resource_path_to_uri, createLogger } from "../utils";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
import type {
|
||||
CancellationToken,
|
||||
CustomDocument,
|
||||
CustomDocumentOpenContext,
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
Uri,
|
||||
WebviewPanel,
|
||||
} from "vscode";
|
||||
import { NotificationMessage } from "vscode-jsonrpc";
|
||||
import {
|
||||
import type { NotificationMessage } from "vscode-jsonrpc";
|
||||
import type {
|
||||
NativeSymbolInspectParams,
|
||||
GodotNativeSymbol,
|
||||
GodotNativeClassInfo,
|
||||
GodotCapabilities,
|
||||
} from "../lsp/gdscript.capabilities";
|
||||
} from "./documentation_types";
|
||||
import { make_html_content } from "./documentation_builder";
|
||||
import { createLogger, get_extension_uri, make_docs_uri } from "../utils";
|
||||
import { createLogger, get_configuration, get_extension_uri, make_docs_uri } from "../utils";
|
||||
import { globals } from "../extension";
|
||||
|
||||
const log = createLogger("providers.docs");
|
||||
@@ -37,9 +37,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
},
|
||||
supportsMultipleEditorsPerDocument: true,
|
||||
};
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerCustomEditorProvider("gddoc", this, options),
|
||||
);
|
||||
context.subscriptions.push(vscode.window.registerCustomEditorProvider("gddoc", this, options));
|
||||
}
|
||||
|
||||
public register_capabilities(message: NotificationMessage) {
|
||||
@@ -63,23 +61,28 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
}
|
||||
|
||||
public async list_native_classes() {
|
||||
const classname = await vscode.window.showQuickPick(
|
||||
[...this.classInfo.keys()].sort(),
|
||||
{
|
||||
placeHolder: "Type godot class name here",
|
||||
canPickMany: false,
|
||||
}
|
||||
);
|
||||
const classname = await vscode.window.showQuickPick([...this.classInfo.keys()].sort(), {
|
||||
placeHolder: "Type godot class name here",
|
||||
canPickMany: false,
|
||||
});
|
||||
if (classname) {
|
||||
vscode.commands.executeCommand("vscode.open", make_docs_uri(classname));
|
||||
}
|
||||
}
|
||||
|
||||
public openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): CustomDocument {
|
||||
return { uri: uri, dispose: () => { } };
|
||||
public openCustomDocument(
|
||||
uri: Uri,
|
||||
openContext: CustomDocumentOpenContext,
|
||||
token: CancellationToken,
|
||||
): CustomDocument {
|
||||
return { uri: uri, dispose: () => {} };
|
||||
}
|
||||
|
||||
public async resolveCustomEditor(document: CustomDocument, panel: WebviewPanel, token: CancellationToken): Promise<void> {
|
||||
public async resolveCustomEditor(
|
||||
document: CustomDocument,
|
||||
panel: WebviewPanel,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const className = document.uri.path.split(".")[0];
|
||||
const target = document.uri.fragment;
|
||||
let symbol: GodotNativeSymbol = null;
|
||||
@@ -89,7 +92,7 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
};
|
||||
|
||||
while (!this.ready) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
symbol = this.symbolDb.get(className);
|
||||
@@ -109,9 +112,21 @@ export class GDDocumentationProvider implements CustomReadonlyEditorProvider {
|
||||
if (!this.htmlDb.has(className)) {
|
||||
this.htmlDb.set(className, make_html_content(panel.webview, symbol, target));
|
||||
}
|
||||
panel.webview.html = this.htmlDb.get(className);
|
||||
|
||||
const scaleFactor = get_configuration("documentation.pageScale");
|
||||
panel.webview.html = this.htmlDb.get(className).replaceAll("scaleFactor", scaleFactor);
|
||||
|
||||
const displayMinimap = get_configuration("documentation.displayMinimap");
|
||||
if (displayMinimap) {
|
||||
panel.webview.html = this.htmlDb.get(className).replace("displayMinimap", "initial;");
|
||||
panel.webview.html = this.htmlDb.get(className).replace("bodyMargin", "200px;");
|
||||
} else {
|
||||
panel.webview.html = this.htmlDb.get(className).replace("bodyMargin", "0px;");
|
||||
panel.webview.html = this.htmlDb.get(className).replace("displayMinimap", "none;");
|
||||
}
|
||||
|
||||
panel.iconPath = get_extension_uri("resources/godot_icon.svg");
|
||||
panel.webview.onDidReceiveMessage(msg => {
|
||||
panel.webview.onDidReceiveMessage((msg) => {
|
||||
if (msg.type === "INSPECT_NATIVE_SYMBOL") {
|
||||
const uri = make_docs_uri(msg.data.native_class, msg.data.symbol_name);
|
||||
vscode.commands.executeCommand("vscode.open", uri);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SymbolKind } from "vscode-languageclient";
|
||||
import * as Prism from "prismjs";
|
||||
import * as csharp from "prismjs/components/prism-csharp";
|
||||
import { marked } from "marked";
|
||||
import { GodotNativeSymbol } from "../lsp/gdscript.capabilities";
|
||||
import type { GodotNativeSymbol } from "./documentation_types";
|
||||
import { get_extension_uri } from "../utils";
|
||||
import yabbcode = require("ya-bbcode");
|
||||
|
||||
@@ -14,7 +14,7 @@ const parser = new yabbcode();
|
||||
const wtf = csharp;
|
||||
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
highlight: (code, lang) => {
|
||||
if (lang === "gdscript") {
|
||||
return Prism.highlight(code, GDScriptGrammar, lang);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export function make_html_content(webview: vscode.Webview, symbol: GodotNativeSy
|
||||
`;
|
||||
}
|
||||
|
||||
return /*html*/`<!DOCTYPE html>
|
||||
return /*html*/ `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -78,16 +78,16 @@ export function make_html_content(webview: vscode.Webview, symbol: GodotNativeSy
|
||||
|
||||
<title>${symbol.name}</title>
|
||||
</head>
|
||||
<body style="line-height: 16pt;">
|
||||
<body style="line-height: scaleFactor%; font-size: scaleFactor%; margin-right: bodyMargin">
|
||||
<main>
|
||||
${make_symbol_document(symbol)}
|
||||
</main>
|
||||
|
||||
<canvas id='map'></canvas>
|
||||
<canvas id='minimap' style="display: displayMinimap"></canvas>
|
||||
|
||||
<script src="${pagemapJsUri}"></script>
|
||||
<script>
|
||||
pagemap(document.querySelector('#map'), ${options});
|
||||
pagemap(document.querySelector('#minimap'), ${options});
|
||||
${initialFocus};
|
||||
|
||||
var vscode = acquireVsCodeApi();
|
||||
@@ -128,128 +128,94 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
|
||||
const ret_type = make_link(parts[2] || "void", undefined);
|
||||
let args = (parts[1] || "").replace(
|
||||
/\:\s([A-z0-9_]+)(\,\s*)?/g,
|
||||
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2"
|
||||
': <a href="" onclick="inspect(\'$1\')">$1</a>$2',
|
||||
);
|
||||
args = args.replace(/\s=\s(.*?)[\,\)]/g, "");
|
||||
return `${ret_type} ${with_class ? `${classlink}.` : ""}${element(
|
||||
"a",
|
||||
s.name,
|
||||
{ href: `#${s.name}` }
|
||||
)}( ${args} )`;
|
||||
return `${ret_type} ${with_class ? `${classlink}.` : ""}${element("a", s.name, {
|
||||
href: `#${s.name}`,
|
||||
})}( ${args} )`;
|
||||
}
|
||||
|
||||
function make_symbol_elements(
|
||||
s: GodotNativeSymbol,
|
||||
with_class = false
|
||||
): { index?: string; body: string } {
|
||||
function make_symbol_elements(s: GodotNativeSymbol, with_class = false): { index?: string; body: string } {
|
||||
switch (s.kind) {
|
||||
case SymbolKind.Property:
|
||||
case SymbolKind.Variable:
|
||||
{
|
||||
// var Control.anchor_left: float
|
||||
const parts = /\.([A-z_0-9]+)\:\s(.*)$/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const type = make_link(parts[2], undefined);
|
||||
const name = element("a", s.name, { href: `#${s.name}` });
|
||||
const title = element(
|
||||
"h4",
|
||||
`${type} ${with_class ? `${classlink}.` : ""}${s.name}`
|
||||
);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: type + " " + name,
|
||||
body: div,
|
||||
};
|
||||
case SymbolKind.Variable: {
|
||||
// var Control.anchor_left: float
|
||||
const parts = /\.([A-z_0-9]+)\:\s(.*)$/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SymbolKind.Constant:
|
||||
{
|
||||
// const Control.FOCUS_ALL: FocusMode = 2
|
||||
// const Control.NOTIFICATION_RESIZED = 40
|
||||
const parts = /\.([A-Za-z_0-9]+)(\:\s*)?([A-z0-9_\.]+)?\s*=\s*(.*)$/.exec(
|
||||
s.detail
|
||||
);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const type = make_link(parts[3] || "int", undefined);
|
||||
const name = parts[1];
|
||||
const value = element("code", parts[4]);
|
||||
const type = make_link(parts[2], undefined);
|
||||
const name = element("a", s.name, { href: `#${s.name}` });
|
||||
const title = element("h4", `${type} ${with_class ? `${classlink}.` : ""}${s.name}`);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: `${type} ${name}`,
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
case SymbolKind.Constant: {
|
||||
// const Control.FOCUS_ALL: FocusMode = 2
|
||||
// const Control.NOTIFICATION_RESIZED = 40
|
||||
const parts = /\.([A-Za-z_0-9]+)(\:\s*)?([A-z0-9_\.]+)?\s*=\s*(.*)$/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const type = make_link(parts[3] || "int", undefined);
|
||||
const name = parts[1];
|
||||
const value = element("code", parts[4]);
|
||||
|
||||
const title = element(
|
||||
"p",
|
||||
`${type} ${with_class ? `${classlink}.` : ""}${name} = ${value}`
|
||||
);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
const title = element("p", `${type} ${with_class ? `${classlink}.` : ""}${name} = ${value}`);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
case SymbolKind.Event: {
|
||||
const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SymbolKind.Event:
|
||||
{
|
||||
const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
const args = (parts[2] || "").replace(
|
||||
/\:\s([A-z0-9_]+)(\,\s*)?/g,
|
||||
": <a href=\"\" onclick=\"inspect('$1')\">$1</a>$2"
|
||||
);
|
||||
const title = element(
|
||||
"p",
|
||||
`${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""
|
||||
}${s.name}( ${args} )`
|
||||
);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
break;
|
||||
const args = (parts[2] || "").replace(
|
||||
/\:\s([A-z0-9_]+)(\,\s*)?/g,
|
||||
': <a href="" onclick="inspect(\'$1\')">$1</a>$2',
|
||||
);
|
||||
const title = element(
|
||||
"p",
|
||||
`${with_class ? `signal ${with_class ? `${classlink}.` : ""}` : ""}${s.name}( ${args} )`,
|
||||
);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
case SymbolKind.Method:
|
||||
case SymbolKind.Function:
|
||||
{
|
||||
const signature = make_function_signature(s, with_class);
|
||||
const title = element("h4", signature);
|
||||
const doc = element(
|
||||
"p",
|
||||
format_documentation(s.documentation, symbol.native_class)
|
||||
);
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: signature,
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case SymbolKind.Function: {
|
||||
const signature = make_function_signature(s, with_class);
|
||||
const title = element("h4", signature);
|
||||
const doc = element("p", format_documentation(s.documentation, symbol.native_class));
|
||||
const div = element("div", title + doc);
|
||||
return {
|
||||
index: signature,
|
||||
body: div,
|
||||
};
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (symbol.kind == SymbolKind.Class) {
|
||||
if (symbol.kind === SymbolKind.Class) {
|
||||
let doc = element("h2", `Class: ${symbol.name}`);
|
||||
if (symbol.class_info.inherits) {
|
||||
const inherits = make_link(symbol.class_info.inherits, undefined);
|
||||
doc += element("p", `Inherits: ${inherits}`);
|
||||
}
|
||||
|
||||
if (symbol.class_info && symbol.class_info.extended_classes) {
|
||||
if (symbol.class_info?.extended_classes) {
|
||||
let inherited = "";
|
||||
for (const c of symbol.class_info.extended_classes) {
|
||||
inherited += (inherited ? ", " : " ") + make_link(c, c);
|
||||
@@ -267,28 +233,30 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
|
||||
let propertyies = "";
|
||||
let others = "";
|
||||
|
||||
for (const s of symbol.children as GodotNativeSymbol[]) {
|
||||
const elements = make_symbol_elements(s);
|
||||
switch (s.kind) {
|
||||
case SymbolKind.Property:
|
||||
case SymbolKind.Variable:
|
||||
properties_index += element("li", elements.index);
|
||||
propertyies += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
case SymbolKind.Constant:
|
||||
constants += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
case SymbolKind.Event:
|
||||
signals += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
case SymbolKind.Method:
|
||||
case SymbolKind.Function:
|
||||
methods_index += element("li", elements.index);
|
||||
methods += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
default:
|
||||
others += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
if (symbol.children) {
|
||||
for (const s of symbol.children as GodotNativeSymbol[]) {
|
||||
const elements = make_symbol_elements(s);
|
||||
switch (s.kind) {
|
||||
case SymbolKind.Property:
|
||||
case SymbolKind.Variable:
|
||||
properties_index += element("li", elements.index);
|
||||
propertyies += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
case SymbolKind.Constant:
|
||||
constants += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
case SymbolKind.Event:
|
||||
signals += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
case SymbolKind.Method:
|
||||
case SymbolKind.Function:
|
||||
methods_index += element("li", elements.index);
|
||||
methods += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
default:
|
||||
others += element("li", elements.body, { id: s.name });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,19 +276,18 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
|
||||
add_group("Other Members", others);
|
||||
doc += element("script", `var godot_class = "${symbol.native_class}";`);
|
||||
|
||||
return doc;
|
||||
} else {
|
||||
let doc = "";
|
||||
const elements = make_symbol_elements(symbol, true);
|
||||
if (elements.index) {
|
||||
const symbols: SymbolKind[] = [SymbolKind.Function, SymbolKind.Method];
|
||||
if (!symbols.includes(symbol.kind)) {
|
||||
doc += element("h2", elements.index);
|
||||
}
|
||||
}
|
||||
doc += element("div", elements.body);
|
||||
return doc;
|
||||
}
|
||||
let doc = "";
|
||||
const elements = make_symbol_elements(symbol, true);
|
||||
if (elements.index) {
|
||||
const symbols: SymbolKind[] = [SymbolKind.Function, SymbolKind.Method];
|
||||
if (!symbols.includes(symbol.kind)) {
|
||||
doc += element("h2", elements.index);
|
||||
}
|
||||
}
|
||||
doc += element("div", elements.body);
|
||||
return doc;
|
||||
}
|
||||
|
||||
function element<K extends keyof HTMLElementTagNameMap>(
|
||||
@@ -328,7 +295,7 @@ function element<K extends keyof HTMLElementTagNameMap>(
|
||||
content: string,
|
||||
props = {},
|
||||
new_line?: boolean,
|
||||
indent?: string
|
||||
indent?: string,
|
||||
) {
|
||||
let props_str = "";
|
||||
for (const key in props) {
|
||||
@@ -336,28 +303,26 @@ function element<K extends keyof HTMLElementTagNameMap>(
|
||||
props_str += ` ${key}="${props[key]}"`;
|
||||
}
|
||||
}
|
||||
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""
|
||||
}`;
|
||||
return `${indent || ""}<${tag} ${props_str}>${content}</${tag}>${new_line ? "\n" : ""}`;
|
||||
}
|
||||
|
||||
function make_link(classname: string, symbol: string) {
|
||||
if (!symbol || symbol == classname) {
|
||||
if (!symbol || symbol === classname) {
|
||||
return element("a", classname, {
|
||||
onclick: `inspect('${classname}')`,
|
||||
href: "",
|
||||
});
|
||||
} else {
|
||||
return element("a", `${classname}.${symbol}`, {
|
||||
onclick: `inspect('${classname}', '${symbol}')`,
|
||||
href: "",
|
||||
});
|
||||
}
|
||||
return element("a", `${classname}.${symbol}`, {
|
||||
onclick: `inspect('${classname}', '${symbol}')`,
|
||||
href: "",
|
||||
});
|
||||
}
|
||||
|
||||
function make_codeblock(code: string, language: string) {
|
||||
const lines = code.split("\n");
|
||||
const indent = lines[0].match(/^\s*/)[0].length;
|
||||
code = lines.map(line => line.slice(indent)).join("\n");
|
||||
code = lines.map((line) => line.slice(indent)).join("\n");
|
||||
return marked.parse(`\`\`\`${language}\n${code}\n\`\`\``);
|
||||
}
|
||||
|
||||
@@ -367,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(""", "\"");
|
||||
html = html.replaceAll(""", '"');
|
||||
|
||||
for (const match of html.matchAll(/\[codeblock].*?\[\/codeblock]/gs)) {
|
||||
let block = match[0];
|
||||
@@ -390,24 +355,21 @@ function format_documentation(bbcode: string, classname: string) {
|
||||
|
||||
html = html.replaceAll("<br/> ", "");
|
||||
// [param <name>]
|
||||
html = html.replaceAll(
|
||||
/\[param\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g,
|
||||
"<code>$1</code>"
|
||||
);
|
||||
html = html.replaceAll(/\[param\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g, "<code>$1</code>");
|
||||
// [method <name>]
|
||||
html = html.replaceAll(
|
||||
/\[method\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\]/g,
|
||||
`<a href="" onclick="inspect('${classname}', '$1')">$1</a>`
|
||||
`<a href="" onclick="inspect('${classname}', '$1')">$1</a>`,
|
||||
);
|
||||
// [<reference>]
|
||||
html = html.replaceAll(
|
||||
/\[(\w+)\]/g,
|
||||
`<a href="" onclick="inspect('$1')">$1</a>` // eslint-disable-line quotes
|
||||
`<a href="" onclick="inspect('$1')">$1</a>`, // eslint-disable-line quotes
|
||||
);
|
||||
// [method <class>.<name>]
|
||||
html = html.replaceAll(
|
||||
/\[\w+\s+(@?[A-Z_a-z][A-Z_a-z0-9]*?)\.(\w+)\]/g,
|
||||
`<a href="" onclick="inspect('$1', '$2')">$1.$2</a>` // eslint-disable-line quotes
|
||||
`<a href="" onclick="inspect('$1', '$2')">$1.$2</a>`, // eslint-disable-line quotes
|
||||
);
|
||||
|
||||
return html;
|
||||
@@ -466,8 +428,10 @@ const GDScriptGrammar = {
|
||||
punctuation: /\./,
|
||||
},
|
||||
},
|
||||
keyword: /\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
|
||||
builtin: /\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
|
||||
keyword:
|
||||
/\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
|
||||
builtin:
|
||||
/\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
|
||||
boolean: /\b(?:true|false)\b/,
|
||||
number: /(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
|
||||
operator: /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient";
|
||||
import type { DocumentSymbol, Range, SymbolKind } from "vscode-languageclient";
|
||||
|
||||
export interface NativeSymbolInspectParams {
|
||||
native_class: string;
|
||||
@@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +33,10 @@ export async function get_project_dir(): Promise<string | undefined> {
|
||||
}
|
||||
projectFile = file;
|
||||
projectDir = path.dirname(file);
|
||||
if (os.platform() === "win32") {
|
||||
// capitalize the drive letter in windows absolute paths
|
||||
projectDir = projectDir[0].toUpperCase() + projectDir.slice(1);
|
||||
}
|
||||
return projectDir;
|
||||
}
|
||||
|
||||
@@ -101,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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
40
test_projects/test-dap-project-godot4/.vscode/launch.json
vendored
Normal file
40
test_projects/test-dap-project-godot4/.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
test_projects/test-dap-project-godot4/BuiltInTypes.gd
Normal file
39
test_projects/test-dap-project-godot4/BuiltInTypes.gd
Normal 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
|
||||
11
test_projects/test-dap-project-godot4/BuiltInTypes.tscn
Normal file
11
test_projects/test-dap-project-godot4/BuiltInTypes.tscn
Normal 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"
|
||||
38
test_projects/test-dap-project-godot4/ExtensiveVars.gd
Normal file
38
test_projects/test-dap-project-godot4/ExtensiveVars.gd
Normal 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
|
||||
12
test_projects/test-dap-project-godot4/ExtensiveVars.tscn
Normal file
12
test_projects/test-dap-project-godot4/ExtensiveVars.tscn
Normal 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
|
||||
14
test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd
Normal file
14
test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd
Normal 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
|
||||
3
test_projects/test-dap-project-godot4/GlobalScript.gd
Normal file
3
test_projects/test-dap-project-godot4/GlobalScript.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
extends Node
|
||||
|
||||
var globalMember := "global member"
|
||||
8
test_projects/test-dap-project-godot4/Node1.gd
Normal file
8
test_projects/test-dap-project-godot4/Node1.gd
Normal 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.
|
||||
9
test_projects/test-dap-project-godot4/NodeVars.gd
Normal file
9
test_projects/test-dap-project-godot4/NodeVars.gd
Normal 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
|
||||
17
test_projects/test-dap-project-godot4/NodeVars.tscn
Normal file
17
test_projects/test-dap-project-godot4/NodeVars.tscn
Normal 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"
|
||||
8
test_projects/test-dap-project-godot4/ScopeVars.gd
Normal file
8
test_projects/test-dap-project-godot4/ScopeVars.gd
Normal 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")
|
||||
11
test_projects/test-dap-project-godot4/ScopeVars.tscn
Normal file
11
test_projects/test-dap-project-godot4/ScopeVars.tscn
Normal 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"
|
||||
5
test_projects/test-dap-project-godot4/TestClassA.gd
Normal file
5
test_projects/test-dap-project-godot4/TestClassA.gd
Normal file
@@ -0,0 +1,5 @@
|
||||
class_name TestClassA
|
||||
|
||||
var testclassa_member1 := "member1"
|
||||
|
||||
var testclassa_member2: Node
|
||||
19
test_projects/test-dap-project-godot4/project.godot
Normal file
19
test_projects/test-dap-project-godot4/project.godot
Normal 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"
|
||||
@@ -14,7 +14,8 @@
|
||||
"rootDir": "src",
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true
|
||||
"allowJs": true,
|
||||
"strictBindCallApply": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
Reference in New Issue
Block a user