Compare commits

..

24 Commits

Author SHA1 Message Date
dependabot[bot]
dadf188b98 Bump actions/setup-node from 4.4.0 to 5.0.0 (#917)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 5.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.4.0...v5.0.0)

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

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

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

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

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

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

* Make inlay hints retrigger when the LSP connects

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

* Fix indentation

* Build the regex in the idiomatic way

* Add env syntax to configuration descriptions

* Add missing import

---------

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

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

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

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

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

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

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

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

* Removing exception tracking from debug_session.

* Replicate changes for Godot 3

---------

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

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-20 13:15:22 -04:00
dependabot[bot]
60cd57767b Bump actions/setup-node from 4.3.0 to 4.4.0 (#847)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.3.0...v4.4.0)

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

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

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

* Change message text

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-18 12:38:53 -04:00
Hugo Locurcio
d9ea6245d4 Bump to version 2.5.1 2025-03-15 20:58:11 +01:00
David Kincaid
e38db288b7 Add ability to suppress LSP error messages (#823) 2025-03-15 15:01:35 -04:00
49 changed files with 926 additions and 612 deletions

View File

@@ -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.5.0"
placeholder: "2.5.1"
validations:
required: true

View File

@@ -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.5.0"
placeholder: "2.5.1"
validations:
required: true

View File

@@ -11,10 +11,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v5.0.0
with:
node-version: 22.x
@@ -58,10 +58,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4.2.0
uses: actions/setup-node@v5.0.0
with:
node-version: 16.x
@@ -75,7 +75,7 @@ jobs:
ls -l godot-tools.vsix
- name: Upload extension VSIX
uses: actions/upload-artifact@v4.6.1
uses: actions/upload-artifact@v4.6.2
with:
name: godot-tools
path: godot-tools.vsix

View File

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

View File

@@ -1,5 +1,9 @@
# Changelog
### 2.5.1
- [Fix "Request textDocument/documentSymbol failed" error when opening a GDScript file](https://github.com/godotengine/godot-vscode-plugin/pull/823)
### 2.5.0
- [**Add `print_rich()` support to debug console**](https://github.com/godotengine/godot-vscode-plugin/pull/792)

View File

@@ -209,10 +209,9 @@ see [CONTRIBUTING.md](CONTRIBUTING.md)
### Why isn't IntelliSense displaying script members?
- GDScript is a dynamically typed script language. The language server can't
- GDScript is a gradually typed script language. The language server can't
infer all variable types.
- To increase the number of results displayed, open the **Editor Settings**,
go to the **Language Server** section then check **Enable Smart Resolve**.
- To increase the number of results displayed, use static typing in your scripts.
### Can Godot/VSCode load in my script changes automatically instead of showing a confirmation window?

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
"vcs": {
"defaultBranch": "master"
},
@@ -9,18 +10,22 @@
"indentWidth": 4,
"lineWidth": 120,
"lineEnding": "lf",
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts", "tools/**/*.ts"]
},
"files": {
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "tools/**/*.ts"],
"ignore": ["node_modules"]
},
"linter": {
"rules": {
"style": {
"noUselessElse": "off",
"useImportType": "off"
}
"useImportType": "off",
"noParameterAssign": "warn"
},
"suspicious": {
"noExplicitAny": "off"
}
}
}
}

48
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "godot-tools",
"version": "2.5.0",
"version": "2.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "godot-tools",
"version": "2.5.0",
"version": "2.5.1",
"license": "MIT",
"dependencies": {
"@vscode/debugadapter": "^1.68.0",
@@ -16,7 +16,7 @@
"global": "^4.4.0",
"marked": "^4.0.11",
"net": "^1.0.2",
"prismjs": "^1.17.1",
"prismjs": "^1.30.0",
"terminate": "^2.5.0",
"vscode-languageclient": "^9.0.1",
"vscode-oniguruma": "^2.0.1",
@@ -33,6 +33,7 @@
"@types/mocha": "^10.0.6",
"@types/node": "^18.19.75",
"@types/prismjs": "^1.16.8",
"@types/sinon": "^17.0.4",
"@types/vscode": "^1.96.0",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.57.1",
@@ -1424,6 +1425,21 @@
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
"dev": true
},
"node_modules/@types/sinon": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz",
"integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==",
"dev": true,
"dependencies": {
"@types/sinonjs__fake-timers": "*"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz",
"integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==",
"dev": true
},
"node_modules/@types/vscode": {
"version": "1.96.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz",
@@ -5618,9 +5634,10 @@
}
},
"node_modules/prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -6226,10 +6243,11 @@
}
},
"node_modules/tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"chownr": "^1.1.1",
@@ -6307,15 +6325,13 @@
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
"dev": true,
"dependencies": {
"rimraf": "^3.0.0"
},
"license": "MIT",
"engines": {
"node": ">=8.17.0"
"node": ">=14.14"
}
},
"node_modules/to-regex-range": {

View File

@@ -2,7 +2,7 @@
"name": "godot-tools",
"displayName": "godot-tools",
"icon": "icon.png",
"version": "2.5.0",
"version": "2.5.1",
"description": "Tools for game development with Godot Engine and GDScript",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"scripts": {
"format": "biome format --write --changed src",
"compile": "tsc -p ./",
"lint": "eslint ./src --quiet",
"lint": "biome lint src",
"watch": "tsc -watch -p ./",
"package": "vsce package",
"vscode:prepublish": "npm run esbuild-base -- --minify",
@@ -272,12 +272,12 @@
"godotTools.editorPath.godot3": {
"type": "string",
"default": "godot3",
"description": "Path to the Godot 3 editor executable"
"description": "Path to the Godot 3 editor executable. Supports environment variables using '${env:VAR_NAME}'."
},
"godotTools.editorPath.godot4": {
"type": "string",
"default": "godot",
"description": "Path to the Godot 4 editor executable"
"description": "Path to the Godot 4 editor executable. Supports environment variables using '${env:VAR_NAME}'."
},
"godotTools.editor.verbose": {
"type": "boolean",
@@ -307,6 +307,19 @@
"default": false,
"description": "Whether extra space should be removed from function parameter lists"
},
"godotTools.formatter.spacesBeforeEndOfLineComment": {
"type": "string",
"enum": [
"1",
"2"
],
"enumDescriptions": [
"1 space before EOL comments # Like this.",
"2 spaces before EOL comments  # Like this."
],
"default": "1",
"description": "Number of spaces before an end-of-line comment"
},
"godotTools.lsp.serverHost": {
"type": "string",
"default": "127.0.0.1",
@@ -642,32 +655,35 @@
"views": {
"debug": [
{
"id": "activeSceneTree",
"name": "Active Scene Tree"
"id": "godotTools.activeSceneTree",
"name": "Active Scene Tree",
"icon": "resources/godot_icon.svg"
},
{
"id": "inspectNode",
"name": "Inspector"
"id": "godotTools.nodeInspector",
"name": "Inspector",
"icon": "resources/godot_icon.svg"
}
],
"godotTools": [
{
"id": "scenePreview",
"name": "Scene Preview"
"id": "godotTools.scenePreview",
"name": "Scene Preview",
"icon": "resources/godot_icon.svg"
}
]
},
"viewsWelcome": [
{
"view": "activeSceneTree",
"view": "godotTools.activeSceneTree",
"contents": "Scene Tree data has not been requested"
},
{
"view": "inspectNode",
"view": "godotTools.nodeInspector",
"contents": "Node has not been inspected"
},
{
"view": "scenePreview",
"view": "godotTools.scenePreview",
"contents": "Open a Scene to see a preview of its structure"
}
],
@@ -721,92 +737,92 @@
"view/title": [
{
"command": "godotTools.debugger.refreshSceneTree",
"when": "view == activeSceneTree",
"when": "view == godotTools.activeSceneTree",
"group": "navigation"
},
{
"command": "godotTools.debugger.refreshInspector",
"when": "view == inspectNode",
"when": "view == godotTools.nodeInspector",
"group": "navigation"
},
{
"command": "godotTools.scenePreview.lock",
"when": "view == scenePreview && !godotTools.context.scenePreview.locked",
"when": "view == godotTools.scenePreview && !godotTools.context.scenePreview.locked",
"group": "navigation@1"
},
{
"command": "godotTools.scenePreview.unlock",
"when": "view == scenePreview && godotTools.context.scenePreview.locked",
"when": "view == godotTools.scenePreview && godotTools.context.scenePreview.locked",
"group": "navigation@1"
},
{
"command": "godotTools.scenePreview.refresh",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "navigation@2"
},
{
"command": "godotTools.scenePreview.openMainScript",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "navigation@3"
},
{
"command": "godotTools.scenePreview.openCurrentScene",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "navigation@4"
}
],
"view/item/context": [
{
"command": "godotTools.debugger.inspectNode",
"when": "view == activeSceneTree",
"when": "view == godotTools.activeSceneTree",
"group": "inline"
},
{
"command": "godotTools.debugger.inspectNode",
"when": "view == inspectNode && viewItem == remote_object",
"when": "view == godotTools.nodeInspector && viewItem == remote_object",
"group": "inline"
},
{
"command": "godotTools.debugger.editValue",
"when": "view == inspectNode && viewItem == editable_value",
"when": "view == godotTools.nodeInspector && viewItem == editable_value",
"group": "inline"
},
{
"command": "godotTools.scenePreview.goToDefinition",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "1@1"
},
{
"command": "godotTools.scenePreview.openDocumentation",
"when": "view == scenePreview",
"when": "view == godotTools.scenePreview",
"group": "1@1"
},
{
"command": "godotTools.scenePreview.copyNodePath",
"when": "view == scenePreview"
"when": "view == godotTools.scenePreview"
},
{
"command": "godotTools.scenePreview.copyResourcePath",
"when": "view == scenePreview && viewItem =~ /hasResourcePath/"
"when": "view == godotTools.scenePreview && viewItem =~ /hasResourcePath/"
},
{
"command": "godotTools.scenePreview.openScene",
"when": "view == scenePreview && viewItem =~ /openable/",
"when": "view == godotTools.scenePreview && viewItem =~ /openable/",
"group": "1@2"
},
{
"command": "godotTools.scenePreview.openScript",
"when": "view == scenePreview && viewItem =~ /hasScript/",
"when": "view == godotTools.scenePreview && viewItem =~ /hasScript/",
"group": "1@2"
},
{
"command": "godotTools.scenePreview.openScene",
"when": "view == scenePreview && viewItem =~ /openable/",
"when": "view == godotTools.scenePreview && viewItem =~ /openable/",
"group": "inline"
},
{
"command": "godotTools.scenePreview.openScript",
"when": "view == scenePreview && viewItem =~ /hasScript/",
"when": "view == godotTools.scenePreview && viewItem =~ /hasScript/",
"group": "inline"
}
],
@@ -884,6 +900,7 @@
"@types/mocha": "^10.0.6",
"@types/node": "^18.19.75",
"@types/prismjs": "^1.16.8",
"@types/sinon": "^17.0.4",
"@types/vscode": "^1.96.0",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.57.1",
@@ -912,7 +929,7 @@
"global": "^4.4.0",
"marked": "^4.0.11",
"net": "^1.0.2",
"prismjs": "^1.17.1",
"prismjs": "^1.30.0",
"terminate": "^2.5.0",
"vscode-languageclient": "^9.0.1",
"vscode-oniguruma": "^2.0.1",

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ import { build_sub_values, parse_next_scene_node, split_buffers } from "./helper
import { VariantDecoder } from "./variables/variant_decoder";
import { VariantEncoder } from "./variables/variant_encoder";
import { RawObject } from "./variables/variants";
import BBCodeToAnsi from 'bbcode-to-ansi';
import BBCodeToAnsi from "bbcode-to-ansi";
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
const socketLog = createLogger("debugger.socket");
@@ -31,11 +31,11 @@ const socketLog = createLogger("debugger.socket");
const bbcodeParser = new BBCodeToAnsi("\u001b[38;2;211;211;211m");
class Command {
public command: string = "";
public paramCount: number = -1;
public command = "";
public paramCount = -1;
public parameters: any[] = [];
public complete: boolean = false;
public threadId: number = 0;
public complete = false;
public threadId = 0;
}
export class ServerController {
@@ -48,7 +48,7 @@ export class ServerController {
private socket?: net.Socket;
private steppingOut = false;
private currentCommand: Command = undefined;
private didFirstOutput: boolean = false;
private didFirstOutput = false;
private connectedVersion = "";
public constructor(public session: GodotDebugSession) {}
@@ -430,8 +430,10 @@ export class ServerController {
this.didFirstOutput = true;
// this.request_scene_tree();
}
for (const output of command.parameters){
output[0].split("\n").forEach(line => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
for (const output of command.parameters) {
for (const line of output[0].split("\n")) {
debug.activeDebugConsole.appendLine(bbcodeParser.parse(line));
}
}
break;
}
@@ -589,7 +591,6 @@ export class ServerController {
if (this.exception.length === 0) {
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
} else {
this.session.set_exception(true);
this.session.sendEvent(new StoppedEvent("exception", 0, this.exception));
}
}
@@ -633,6 +634,7 @@ export class ServerController {
stackVars.globals.push({ name: parameters[i++], value: parameters[i++] });
}
// biome-ignore lint/complexity/noForEach: <custom forEach impl>
stackVars.forEach((item) => build_sub_values(item));
this.session.set_scopes(stackVars);

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,18 @@
import {
Breakpoint,
InitializedEvent,
LoggingDebugSession,
Source,
TerminatedEvent,
Thread,
Breakpoint,
InitializedEvent,
LoggingDebugSession,
Source,
TerminatedEvent,
Thread,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { Subject } from "await-notify";
import * as fs from "node:fs";
import { createLogger } from "../../utils";
import { GodotDebugData } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
import { InspectorProvider } from "../inspector_provider";
import { SceneTreeProvider } from "../scene_tree_provider";
import { ServerController } from "./server_controller";
import { VariablesManager } from "./variables/variables_manager";
@@ -23,11 +23,11 @@ export class GodotDebugSession extends LoggingDebugSession {
public controller = new ServerController(this);
public debug_data = new GodotDebugData(this);
public sceneTree: SceneTreeProvider;
private exception = false;
public inspector: InspectorProvider;
private configuration_done: Subject = new Subject();
private mode: "launch" | "attach" | "" = "";
public variables_manager: VariablesManager;
public variables_manager = new VariablesManager(this.controller);
public constructor(projectVersion: string) {
super();
@@ -81,7 +81,6 @@ export class GodotDebugSession extends LoggingDebugSession {
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
@@ -94,7 +93,6 @@ export class GodotDebugSession extends LoggingDebugSession {
this.mode = "attach";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.attach(args);
this.sendResponse(response);
@@ -111,27 +109,21 @@ export class GodotDebugSession extends LoggingDebugSession {
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
log.info("continueRequest", args);
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
log.info("nextRequest", args);
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
this.controller.next();
this.sendResponse(response);
}
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
log.info("pauseRequest", args);
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
this.controller.break();
this.sendResponse(response);
}
protected setBreakPointsRequest(
@@ -176,18 +168,14 @@ export class GodotDebugSession extends LoggingDebugSession {
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
log.info("stepInRequest", args);
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
this.controller.step();
this.sendResponse(response);
}
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
log.info("stepOutRequest", args);
if (!this.exception) {
this.controller.step_out();
this.sendResponse(response);
}
this.controller.step_out();
this.sendResponse(response);
}
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
@@ -297,8 +285,4 @@ export class GodotDebugSession extends LoggingDebugSession {
log.info("evaluateRequest response", response);
this.sendResponse(response);
}
public set_exception(exception: boolean) {
this.exception = true;
}
}

View File

@@ -44,13 +44,13 @@ export function get_sub_values(value: any): GodotVariable[] {
subValues = [];
for (const [key, val] of value.entries()) {
const name =
typeof key["stringify_value"] === "function"
typeof key.stringify_value === "function"
? `${key.type_name()}${key.stringify_value()}`
: `${key}`;
const godot_id = val instanceof ObjectId ? val.id : undefined;
subValues.push({ id: godot_id, name, value: val } as GodotVariable);
}
} else if (typeof value["sub_values"] === "function") {
} else if (typeof value.sub_values === "function") {
subValues = value.sub_values()?.map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
});

View File

@@ -1,10 +1,11 @@
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import * as fs from "node:fs";
import * as net from "node:net";
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import BBCodeToAnsi from "bbcode-to-ansi";
import { debug, window } from "vscode";
import {
VERIFY_RESULT,
ansi,
convert_resource_path_to_uri,
createLogger,
@@ -12,7 +13,6 @@ import {
get_free_port,
get_project_version,
verify_godot_version,
VERIFY_RESULT,
} from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
import { killSubProcesses, subProcess } from "../../utils/subspawn";
@@ -23,8 +23,6 @@ import { get_sub_values, parse_next_scene_node, split_buffers } from "./helpers"
import { VariantDecoder } from "./variables/variant_decoder";
import { VariantEncoder } from "./variables/variant_encoder";
import { RawObject } from "./variables/variants";
import { VariablesManager } from "./variables/variables_manager";
import BBCodeToAnsi from "bbcode-to-ansi";
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
const socketLog = createLogger("debugger.socket");
@@ -32,11 +30,11 @@ const socketLog = createLogger("debugger.socket");
const bbcodeParser = new BBCodeToAnsi("\u001b[38;2;211;211;211m");
class Command {
public command: string = "";
public paramCount: number = -1;
public parameters: any[] = [];
public complete: boolean = false;
public threadId: number = 0;
public command = "";
public paramCount = -1;
public parameters = [];
public complete = false;
public threadId = 0;
}
class GodotPartialStackVars {
@@ -136,10 +134,7 @@ export class ServerController {
public request_stack_frame_vars(stack_frame_id: number) {
if (this.partialStackVars !== undefined) {
log.warn(
"Partial stack frames have been requested, while existing request hasn't been completed yet." +
`Remaining stack_frames: ${this.partialStackVars.remaining}` +
`Current stack_frame_id: ${this.partialStackVars.stack_frame_id}` +
`Requested stack_frame_id: ${stack_frame_id}`,
`Partial stack frames have been requested, while existing request hasn't been completed yet.Remaining stack_frames: ${this.partialStackVars.remaining} Current stack_frame_id: ${this.partialStackVars.stack_frame_id} Requested stack_frame_id: ${stack_frame_id}`,
);
}
this.partialStackVars = new GodotPartialStackVars(stack_frame_id);
@@ -308,7 +303,7 @@ export class ServerController {
return;
}
socketLog.debug("rx:", data[0], data[0][2]);
socketLog.debug("rx:", data[0]);
const command = this.parse_message(data[0]);
this.handle_command(command);
}
@@ -411,11 +406,10 @@ export class ServerController {
this.set_exception("");
}
this.request_stack_dump();
this.session.variables_manager = new VariablesManager(this);
this.request_scene_tree();
break;
}
case "debug_exit":
this.session.variables_manager = undefined;
break;
case "message:click_ctrl":
// TODO: what is this?
@@ -497,8 +491,7 @@ export class ServerController {
}
if (typeof command.parameters[0] !== "string") {
log.error(
"Unexpected parameter type for 'stack_frame_var'. Expected string for name, got " +
typeof command.parameters[0],
`Unexpected parameter type for 'stack_frame_var'. Expected string for name, got ${typeof command.parameters[0]}`,
);
return;
}
@@ -507,23 +500,21 @@ export class ServerController {
(command.parameters[1] !== 0 && command.parameters[1] !== 1 && command.parameters[1] !== 2)
) {
log.error(
"Unexpected parameter type for 'stack_frame_var'. Expected number for scope, got " +
typeof command.parameters[1],
`Unexpected parameter type for 'stack_frame_var'. Expected number for scope, got ${typeof command.parameters[1]}`,
);
return;
}
if (typeof command.parameters[2] !== "number") {
log.error(
"Unexpected parameter type for 'stack_frame_var'. Expected number for type, got " +
typeof command.parameters[2],
`Unexpected parameter type for 'stack_frame_var'. Expected number for type, got ${typeof command.parameters[2]}`,
);
return;
}
var name: string = command.parameters[0];
var scope: 0 | 1 | 2 = command.parameters[1]; // 0 = locals, 1 = members, 2 = globals
var type: number = command.parameters[2];
var value: any = command.parameters[3];
var subValues: GodotVariable[] = get_sub_values(value);
const name: string = command.parameters[0];
const scope: 0 | 1 | 2 = command.parameters[1]; // 0 = locals, 1 = members, 2 = globals
const type: number = command.parameters[2];
const value: any = command.parameters[3];
const subValues: GodotVariable[] = get_sub_values(value);
this.partialStackVars.append(name, scope, type, value, subValues);
if (this.partialStackVars.remaining === 0) {
@@ -555,8 +546,11 @@ export class ServerController {
this.didFirstOutput = true;
// this.request_scene_tree();
}
const console = debug.activeDebugConsole;
for (const output of command.parameters[0]) {
output.split("\n").forEach((line) => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
for (const line of output.split("\n")) {
console.appendLine(bbcodeParser.parse(line));
}
}
break;
}
@@ -718,7 +712,6 @@ export class ServerController {
if (this.exception.length === 0) {
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
} else {
this.session.set_exception(true);
this.session.sendEvent(new StoppedEvent("exception", 0, this.exception));
}
}

View File

@@ -1,18 +1,20 @@
import { promises as fs } from "fs";
import * as path from "path";
import { promises as fs } from "node:fs";
import * as path from "node:path";
import * as vscode from "vscode";
import { DebugProtocol } from "@vscode/debugprotocol";
import chai from "chai";
import chaiSubset from "chai-subset";
var chaiAsPromised = import("chai-as-promised");
const chaiAsPromised = import("chai-as-promised");
// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
chaiAsPromised.then((module) => {
chai.use(module.default);
});
import { promisify } from "util";
import { execFile } from "child_process";
import { promisify } from "node:util";
import { execFile } from "node:child_process";
import { clean_godot_path } from "../../../utils";
const execFileAsync = promisify(execFile);
chai.use(chaiSubset);
@@ -35,7 +37,11 @@ async function getBreakpointLocations(scriptPath: string): Promise<{ [key: strin
const breakpoints: { [key: string]: vscode.Location } = {};
const breakpointRegex = /\b(breakpoint::.*)\b/g;
let match: RegExpExecArray | null;
while ((match = breakpointRegex.exec(script_content)) !== null) {
while (true) {
match = breakpointRegex.exec(script_content);
if (match === null) {
break;
}
const breakpointName = match[1];
const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1;
breakpoints[breakpointName] = new vscode.Location(
@@ -47,7 +53,7 @@ async function getBreakpointLocations(scriptPath: string): Promise<{ [key: strin
}
async function waitForActiveStackItemChange(
ms: number = 10000,
ms = 10000,
): Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((resolve, reject) => {
const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => {
@@ -66,7 +72,7 @@ async function waitForActiveStackItemChange(
return res;
}
async function getStackFrames(threadId: number = 1): Promise<DebugProtocol.StackFrame[]> {
async function getStackFrames(threadId = 1): Promise<DebugProtocol.StackFrame[]> {
// Ensure there is an active debug session
if (!vscode.debug.activeDebugSession) {
throw new Error("No active debug session found");
@@ -102,7 +108,7 @@ async function waitForBreakpoint(
const stackFrames = await getStackFrames();
if (
stackFrames[0].source.path !== breakpoint.location.uri.fsPath ||
stackFrames[0].line != breakpoint.location.range.start.line + 1
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}`,
@@ -111,9 +117,9 @@ async function waitForBreakpoint(
}
enum VariableScope {
Locals,
Members,
Globals,
Locals = 0,
Members = 1,
Globals = 2,
}
async function getVariablesForVSCodeID(vscode_id: number): Promise<DebugProtocol.Variable[]> {
@@ -124,13 +130,10 @@ async function getVariablesForVSCodeID(vscode_id: number): Promise<DebugProtocol
return variablesResponse?.variables || [];
}
async function getVariablesForScope(
scope: VariableScope,
stack_frame_id: number = 0,
): Promise<DebugProtocol.Variable[]> {
async function getVariablesForScope(scope: VariableScope, stack_frame_id = 0): Promise<DebugProtocol.Variable[]> {
const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", { frameId: stack_frame_id });
const scope_name = VariableScope[scope];
const scope_res = res_scopes.scopes.find((s) => s.name == scope_name);
const scope_res = res_scopes.scopes.find((s) => s.name === scope_name);
if (scope_res === undefined) {
throw new Error(`No ${scope_name} scope found in responce from "scopes" request`);
}
@@ -162,7 +165,7 @@ 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
let fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@@ -225,7 +228,9 @@ suite("DAP Integration Tests - Variable Scopes", () => {
// init the godot project by importing it in godot engine:
const config = vscode.workspace.getConfiguration("godotTools");
// config.update("editorPath.godot4", "godot4", vscode.ConfigurationTarget.Workspace);
var godot4_path = config.get<string>("editorPath.godot4");
const godot4_path = clean_godot_path(config.get<string>("editorPath.godot4"));
// get the path for currently opened project in vscode test instance:
console.log("Executing", [godot4_path, "--headless", "--import", workspaceFolder]);
const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", workspaceFolder], {
@@ -233,7 +238,9 @@ suite("DAP Integration Tests - Variable Scopes", () => {
cwd: workspaceFolder,
});
if (exec_res.stderr !== "") {
throw new Error(exec_res.stderr);
// TODO: was preventing tests from running
// throw new Error(exec_res.stderr);
console.log(exec_res.stderr);
}
console.log(exec_res.stdout);
});
@@ -261,13 +268,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
);
});
// test("sample test", async function() {
// expect(true).to.equal(true);
// expect([1,2,3]).to.be.unique;
// expect([1,1]).not.to.be.unique;
// });
test("should return correct scopes", async function () {
test("should return correct scopes", async () => {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(
breakpointLocations["breakpoint::ScopeVars::ClassFoo::test_function"],
@@ -290,7 +291,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
Globals: number;
}
> = new Map();
for (var stack_frame_id = 0; stack_frame_id < 3; stack_frame_id++) {
for (let stack_frame_id = 0; stack_frame_id < 3; stack_frame_id++) {
const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {
frameId: stack_frame_id,
});
@@ -324,7 +325,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
expect(vars_frame2_locals).to.containSubset([{ name: "str_var", value: "ScopeVars::_ready::local::str_var" }]);
})?.timeout(10000);
test("should return global variables", async function () {
test("should return global variables", async () => {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
@@ -340,7 +341,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
expect(variables).to.containSubset([{ name: "GlobalScript" }]);
})?.timeout(10000);
test("should return all local variables", async function () {
test("should return all local variables", async () => {
/** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
@@ -359,7 +360,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
expect(variables).to.containSubset([{ name: "self_var" }]);
})?.timeout(10000);
test("should return all member variables", async function () {
test("should return all member variables", async () => {
/** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
@@ -382,7 +383,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
]);
})?.timeout(10000);
test("should retrieve all built-in types correctly", async function () {
test("should retrieve all built-in types correctly", async () => {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);
@@ -423,7 +424,7 @@ suite("DAP Integration Tests - Variable Scopes", () => {
);
})?.timeout(10000);
test("should retrieve all complex variables correctly", async function () {
test("should retrieve all complex variables correctly", async () => {
const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]);
vscode.debug.addBreakpoints([breakpoint]);

View File

@@ -3,7 +3,7 @@ import chai from "chai";
import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
// import chaiAsPromised from "chai-as-promised";
// eslint-disable-next-line @typescript-eslint/no-var-requires
var chaiAsPromised = import("chai-as-promised");
const chaiAsPromised = import("chai-as-promised");
// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
chaiAsPromised.then((module) => {
@@ -12,7 +12,7 @@ chaiAsPromised.then((module) => {
const { expect } = chai;
suite("GodotObjectPromise", () => {
let clock;
let clock: sinon.SinonFakeTimers;
setup(() => {
clock = sinon.useFakeTimers(); // Use Sinon to control time

View File

@@ -1,9 +1,9 @@
import { DebugProtocol } from "@vscode/debugprotocol";
import { ServerController } from "../server_controller";
import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
import { GodotVariable } from "../../debug_runtime";
import { ObjectId } from "./variants";
import { ServerController } from "../server_controller";
import { GodotIdToVscodeIdMapper, GodotIdWithPath } from "./godot_id_to_vscode_id_mapper";
import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
import { ObjectId } from "./variants";
export interface VsCodeScopeIDs {
Locals: number;
@@ -27,7 +27,7 @@ export class VariablesManager {
* @returns an object with Locals, Members, and Globals vscode_ids
*/
public get_or_create_frame_scopes(stack_frame_id: number): VsCodeScopeIDs {
var scopes = this.frame_id_to_scopes_map.get(stack_frame_id);
let scopes = this.frame_id_to_scopes_map.get(stack_frame_id);
if (scopes === undefined) {
const frame_id = BigInt(stack_frame_id);
scopes = {} as VsCodeScopeIDs;
@@ -68,7 +68,7 @@ export class VariablesManager {
}
}
}
var variable_promise = this.godot_object_promises.get(godot_id);
let variable_promise = this.godot_object_promises.get(godot_id);
if (variable_promise === undefined) {
// variable not found, request one
if (godot_id < 0) {
@@ -153,8 +153,9 @@ export class VariablesManager {
let variable: GodotVariable;
const variable_names = variable_name.split(".");
let parent_id: bigint;
for (var i = 0; i < variable_names.length; i++) {
for (let i = 0; i < variable_names.length; i++) {
if (i === 0) {
// find the first part of variable_name in scopes. Locals first, then Members, then Globals
const vscode_scope_ids = this.get_or_create_frame_scopes(stack_frame_id);
@@ -162,11 +163,12 @@ export class VariablesManager {
const godot_ids = vscode_ids
.map((vscode_id) => this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id))
.map((godot_id_with_path) => godot_id_with_path.godot_id);
for (var godot_id of godot_ids) {
for (const godot_id of godot_ids) {
// check each scope for requested variable
const scope = await this.get_godot_object(godot_id);
variable = scope.sub_values.find((sv) => sv.name === variable_names[0]);
if (variable !== undefined) {
parent_id = godot_id;
break;
}
}
@@ -189,7 +191,7 @@ export class VariablesManager {
const parsed_variable = await this.parse_variable(
variable,
undefined,
godot_id,
parent_id,
[],
this.godot_id_to_vscode_id_mapper,
);
@@ -220,7 +222,7 @@ export class VariablesManager {
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") {
rendered_value = `${value}`;
@@ -233,6 +235,7 @@ export class VariablesManager {
new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),
);
} else if (value instanceof Map) {
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
rendered_value = value["class_name"] ?? `Dictionary(${value.size})`;
reference = mapper.get_or_create_vscode_id(
new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,10 +14,11 @@ import {
import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
import { killSubProcesses, subProcess } from "../utils/subspawn";
import GDScriptLanguageClient, { ClientStatus, TargetLSP } from "./GDScriptLanguageClient";
import { EventEmitter } from "vscode";
const log = createLogger("lsp.manager", { output: "Godot LSP" });
enum ManagerStatus {
export enum ManagerStatus {
INITIALIZING = 0,
INITIALIZING_LSP = 1,
PENDING = 2,
@@ -31,6 +32,9 @@ enum ManagerStatus {
export class ClientConnectionManager {
public client: GDScriptLanguageClient = null;
private statusChanged = new EventEmitter<ManagerStatus>();
onStatusChanged = this.statusChanged.event;
private reconnectionAttempts = 0;
private target: TargetLSP = TargetLSP.EDITOR;
@@ -40,8 +44,7 @@ export class ClientConnectionManager {
private connectedVersion = "";
constructor(private context: vscode.ExtensionContext) {
this.client = new GDScriptLanguageClient();
this.client.events.on("status", this.on_client_status_changed.bind(this));
this.create_new_client();
setInterval(() => {
this.retry_callback();
@@ -70,6 +73,14 @@ export class ClientConnectionManager {
this.connect_to_language_server();
}
private create_new_client() {
const port = this.client?.port ?? -1;
this.client?.events?.removeAllListeners();
this.client = new GDScriptLanguageClient();
this.client.port = port;
this.client.events.on("status", this.on_client_status_changed.bind(this));
}
private async connect_to_language_server() {
this.client.port = -1;
this.target = TargetLSP.EDITOR;
@@ -243,7 +254,7 @@ export class ClientConnectionManager {
text = "$(check) Connected";
tooltip = `Connected to the GDScript language server.\n${lspTarget}`;
if (this.connectedVersion) {
tooltip += `\n${this.connectedVersion}`;
tooltip += `\nGodot version: ${this.connectedVersion}`;
}
break;
case ManagerStatus.DISCONNECTED:
@@ -281,7 +292,9 @@ export class ClientConnectionManager {
}
break;
case ClientStatus.DISCONNECTED:
set_context("connectedToLSP", false);
// Disconnection is unrecoverable, since the server will not know that the reconnected client is the same.
// Create a new client with a clean state to prevent de-sync e.g. of client managed files.
this.create_new_client();
if (this.retry) {
if (this.client.port !== -1) {
this.status = ManagerStatus.INITIALIZING_LSP;
@@ -300,6 +313,7 @@ export class ClientConnectionManager {
default:
break;
}
this.statusChanged.fire(this.status);
this.update_status_widget();
}

View File

@@ -3,6 +3,7 @@ import * as path from "node:path";
import * as vscode from "vscode";
import {
LanguageClient,
MessageSignature,
type LanguageClientOptions,
type NotificationMessage,
type RequestMessage,
@@ -64,6 +65,26 @@ type ChangeWorkspaceNotification = {
};
};
type DocumentLinkResult = {
range: {
end: {
character: number;
line: number;
};
start: {
character: number;
line: number;
};
};
target: string;
};
type DocumentLinkResponseMessage = {
id: number;
jsonrpc: string;
result: DocumentLinkResult[];
};
export default class GDScriptLanguageClient extends LanguageClient {
public io: MessageIO = new MessageIO();
@@ -72,7 +93,6 @@ export default class GDScriptLanguageClient extends LanguageClient {
public port = -1;
public lastPortTried = -1;
public sentMessages = new Map();
private initMessage: RequestMessage;
private rejected = false;
events = new EventEmitter();
@@ -131,7 +151,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.io.connect(host, port);
}
async send_request(method: string, params) {
async send_request<R>(method: string, params): Promise<R> {
try {
return this.sendRequest(method, params);
} catch {
@@ -139,6 +159,38 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
}
handleFailedRequest<T>(
type: MessageSignature,
token: vscode.CancellationToken | undefined,
error: any,
defaultValue: T,
showNotification?: boolean,
): T {
if (type.method === "textDocument/documentSymbol") {
if (
error.message.includes("selectionRange must be contained in fullRange")
) {
log.warn(
`Request failed for method "${type.method}", suppressing notification - see issue #820`
);
return super.handleFailedRequest(
type,
token,
error,
defaultValue,
false
);
}
}
return super.handleFailedRequest(
type,
token,
error,
defaultValue,
showNotification
);
}
private request_filter(message: RequestMessage) {
if (this.rejected) {
if (message.method === "shutdown") {
@@ -148,9 +200,6 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
this.sentMessages.set(message.id, message);
if (!this.initMessage && message.method === "initialize") {
this.initMessage = message;
}
// discard outgoing messages that we know aren't supported
// if (message.method === "textDocument/didSave") {
// return false;
@@ -162,6 +211,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
return false;
}
if (message.method === "workspace/symbol") {
// Fixed on server side since Godot 4.5
return false;
}
@@ -192,6 +242,32 @@ export default class GDScriptLanguageClient extends LanguageClient {
(message as HoverResponseMesssage).result.contents.value = value;
}
} else if (sentMessage.method === "textDocument/documentLink") {
const results: DocumentLinkResult[] = (
message as DocumentLinkResponseMessage
).result;
if (!results) {
return message;
}
const final_result: DocumentLinkResult[] = [];
// at this point, Godot's LSP server does not
// return a valid path for resources identified
// by "uid://""
//
// this is a dirty hack to remove any "uid://"
// document links.
//
// to provide links for these, we will be relying on
// the internal DocumentLinkProvider instead.
for (const result of results) {
if (!result.target.startsWith("uid://")) {
final_result.push(result);
}
}
(message as DocumentLinkResponseMessage).result = final_result;
}
return message;
@@ -231,7 +307,10 @@ export default class GDScriptLanguageClient extends LanguageClient {
return message;
}
public async get_symbol_at_position(uri: vscode.Uri, position: vscode.Position) {
public async get_symbol_at_position(
uri: vscode.Uri,
position: vscode.Position
) {
const params = {
textDocument: { uri: uri.toString() },
position: { line: position.line, character: position.character },
@@ -274,10 +353,6 @@ export default class GDScriptLanguageClient extends LanguageClient {
const host = get_configuration("lsp.serverHost");
log.info(`connected to LSP at ${host}:${this.lastPortTried}`);
if (this.initMessage) {
this.send_request(this.initMessage.method, this.initMessage.params);
}
}
private on_disconnected() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,12 +43,10 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
scriptDecorator = new ScriptDecorationProvider(this);
private changeTreeEvent = new EventEmitter<void>();
public get onDidChangeTreeData(): Event<void> {
return this.changeTreeEvent.event;
}
onDidChangeTreeData = this.changeTreeEvent.event;
constructor(private context: ExtensionContext) {
this.tree = vscode.window.createTreeView("scenePreview", {
this.tree = vscode.window.createTreeView("godotTools.scenePreview", {
treeDataProvider: this,
dragAndDropController: this,
});
@@ -265,17 +263,19 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
element.collapsibleState = TreeItemCollapsibleState.None;
}
this.uniqueDecorator.changeDecorationsEvent.fire(element.resourceUri);
this.scriptDecorator.changeDecorationsEvent.fire(element.resourceUri);
this.uniqueDecorator.update(element.resourceUri);
this.scriptDecorator.update(element.resourceUri);
return element;
}
}
class UniqueDecorationProvider implements vscode.FileDecorationProvider {
public changeDecorationsEvent = new EventEmitter<Uri>();
get onDidChangeFileDecorations(): Event<Uri> {
return this.changeDecorationsEvent.event;
public emitter = new EventEmitter<Uri>();
onDidChangeFileDecorations = this.emitter.event;
update(uri: Uri) {
this.emitter.fire(uri);
}
constructor(private previewer: ScenePreviewProvider) {}
@@ -293,9 +293,11 @@ class UniqueDecorationProvider implements vscode.FileDecorationProvider {
}
class ScriptDecorationProvider implements vscode.FileDecorationProvider {
public changeDecorationsEvent = new EventEmitter<Uri>();
get onDidChangeFileDecorations(): Event<Uri> {
return this.changeDecorationsEvent.event;
public emitter = new EventEmitter<Uri>();
onDidChangeFileDecorations = this.emitter.event;
update(uri: Uri) {
this.emitter.fire(uri);
}
constructor(private previewer: ScenePreviewProvider) {}

View File

@@ -4,7 +4,7 @@ import {
MarkdownString,
Uri
} from "vscode";
import * as path from "path";
import * as path from "node:path";
import { get_extension_uri } from "../utils";
const iconDir = get_extension_uri("resources", "godot_icons").fsPath;
@@ -17,9 +17,9 @@ export class SceneNode extends TreeItem {
public text: string;
public position: number;
public body: string;
public unique: boolean = false;
public hasScript: boolean = false;
public scriptId: string = "";
public unique = false;
public hasScript = false;
public scriptId = "";
public children: SceneNode[] = [];
constructor(
@@ -29,7 +29,7 @@ export class SceneNode extends TreeItem {
) {
super(label, collapsibleState);
const iconName = className + ".svg";
const iconName = `${className}.svg`;
this.iconPath = {
light: Uri.file(path.join(iconDir, "light", iconName)),

View File

@@ -19,6 +19,10 @@ let projectDir: string | undefined = undefined;
let projectFile: string | undefined = undefined;
export async function get_project_dir(): Promise<string | undefined> {
if (projectDir && projectFile) {
return projectDir;
}
let file = "";
if (vscode.workspace.workspaceFolders !== undefined) {
const files = await vscode.workspace.findFiles("**/project.godot", null);
@@ -61,6 +65,10 @@ export async function get_project_file(): Promise<string | undefined> {
let projectVersion: string | undefined = undefined;
export async function get_project_version(): Promise<string | undefined> {
if (projectVersion) {
return projectVersion;
}
if (projectDir === undefined || projectFile === undefined) {
await get_project_dir();
}
@@ -127,6 +135,69 @@ export async function convert_uri_to_resource_path(uri: vscode.Uri): Promise<str
return `res://${relative_path}`;
}
const uidCache: Map<string, vscode.Uri | null> = new Map();
export async function convert_uids_to_uris(uids: string[]): Promise<Map<string, vscode.Uri>> {
const not_found_uids: string[] = [];
const uris: Map<string, vscode.Uri> = new Map();
let found_all = true;
for (const uid of uids) {
if (!uid.startsWith("uid://")) {
continue;
}
if (uidCache.has(uid)) {
const uri = uidCache.get(uid);
if (fs.existsSync(uri.fsPath)) {
uris.set(uid, uri);
continue;
}
uidCache.delete(uid);
}
found_all = false;
not_found_uids.push(uid);
}
if (found_all) {
return uris;
}
const files = await vscode.workspace.findFiles("**/*.uid", null);
for (const file of files) {
const document = await vscode.workspace.openTextDocument(file);
const text = document.getText();
const match = text.match(/uid:\/\/([0-9a-z]*)/);
if (!match) {
continue;
}
const found_match = not_found_uids.indexOf(match[0]) >= 0;
const file_path = file.fsPath.substring(0, file.fsPath.length - ".uid".length);
if (!fs.existsSync(file_path)) {
continue;
}
const file_uri = vscode.Uri.file(file_path);
uidCache.set(match[0], file_uri);
if (found_match) {
uris.set(match[0], file_uri);
}
}
return uris;
}
export async function convert_uid_to_uri(uid: string): Promise<vscode.Uri | undefined> {
const uris = await convert_uids_to_uris([uid]);
return uris.get(uid);
}
export type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
export type VERIFY_RESULT = {
status: VERIFY_STATUS;
@@ -165,8 +236,22 @@ export function verify_godot_version(godotPath: string, expectedVersion: "3" | "
}
export function clean_godot_path(godotPath: string): string {
let target = godotPath.replace(/^"/, "").replace(/"$/, "");
let pathToClean = godotPath;
// check for environment variable syntax
// looking for: ${env:FOOBAR}
// extracts "FOOBAR"
const pattern = /\$\{env:(.+?)\}/;
const match = godotPath.match(pattern);
if (match && match.length >= 2) {
pathToClean = process.env[match[1]];
}
// strip leading and trailing quotes
let target = pathToClean.replace(/^"/, "").replace(/"$/, "");
// try to fix macos paths
if (os.platform() === "darwin" && target.endsWith(".app")) {
target = path.join(target, "Contents", "MacOS", "Godot");
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -76,7 +76,6 @@
{ "include": "#function_call" },
{ "include": "#region"},
{ "include": "#comment" },
{ "include": "#self" },
{ "include": "#func" },
{ "include": "#letter" },
{ "include": "#numbers" },
@@ -167,24 +166,20 @@
{
"begin": "(\"|')",
"end": "\\1",
"name": "string.quoted.gdscript meta.literal.nodepath.gdscript constant.character.escape",
"name": "string.quoted.gdscript meta.literal.nodepath.gdscript constant.character.escape.gdscript",
"patterns": [
{
"match": "%",
"name": "keyword.control.flow"
"name": "keyword.control.flow.gdscript"
}
]
},
{ "include": "#expression" }
]
},
"self": {
"match": "\\bself\\b",
"name": "variable.language.gdscript"
},
"func": {
"match": "\\bfunc\\b",
"name": "keyword.language.gdscript"
"name": "keyword.language.gdscript storage.type.function.gdscript"
},
"in_keyword": {
"patterns": [
@@ -250,7 +245,7 @@
"captures": { "1": { "name": "keyword.control.gdscript" } }
},
"keywords": {
"match": "\\b(?:class|class_name|abstract|is|onready|tool|static|export|as|void|enum|assert|breakpoint|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace|super)\\b",
"match": "\\b(?:class|class_name|is|onready|tool|static|export|as|enum|assert|breakpoint|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace|super|self)\\b",
"name": "keyword.language.gdscript"
},
"letter": {
@@ -444,14 +439,14 @@
]
},
"annotations": {
"match": "(@)(export|export_group|export_color_no_alpha|export_custom|export_dir|export_enum|export_exp_easing|export_file|export_flags|export_flags_2d_navigation|export_flags_2d_physics|export_flags_2d_render|export_flags_3d_navigation|export_flags_3d_physics|export_flags_3d_render|export_global_dir|export_global_file|export_multiline|export_node_path|export_placeholder|export_range|export_storage|icon|onready|rpc|tool|warning_ignore|static_unload)\\b",
"match": "(@)(abstract|export|export_category|export_color_no_alpha|export_custom|export_dir|export_enum|export_exp_easing|export_file|export_file_path|export_flags|export_flags_2d_navigation|export_flags_2d_physics|export_flags_2d_render|export_flags_3d_navigation|export_flags_3d_physics|export_flags_3d_render|export_flags_avoidance|export_global_dir|export_global_file|export_group|export_multiline|export_node_path|export_placeholder|export_range|export_storage|export_subgroup|export_tool_button|icon|onready|rpc|static_unload|tool|warning_ignore|warning_ignore_restore|warning_ignore_start)\\b",
"captures": {
"1": { "name": "entity.name.function.decorator.gdscript" },
"2": { "name": "entity.name.function.decorator.gdscript" }
}
},
"builtin_classes": {
"match": "(?<![^.]\\.|:)\\b(Vector2|Vector2i|Vector3|Vector3i|Vector4|Vector4i|Color|Rect2|Rect2i|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|Transform3D|AABB|String|Color|NodePath|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray|bool|int|float|Signal|Callable|StringName|Quaternion|Projection|PackedByteArray|PackedInt32Array|PackedInt64Array|PackedFloat32Array|PackedFloat64Array|PackedStringArray|PackedVector2Array|PackedVector2iArray|PackedVector3Array|PackedVector3iArray|PackedVector4Array|PackedColorArray|JSON|UPNP|OS|IP|JSONRPC|XRVRS)\\b",
"match": "(?<![^.]\\.|:)\\b(Vector2|Vector2i|Vector3|Vector3i|Vector4|Vector4i|Color|Rect2|Rect2i|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|Transform3D|AABB|String|Color|NodePath|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray|bool|int|float|Signal|Callable|StringName|Quaternion|Projection|PackedByteArray|PackedInt32Array|PackedInt64Array|PackedFloat32Array|PackedFloat64Array|PackedStringArray|PackedVector2Array|PackedVector2iArray|PackedVector3Array|PackedVector3iArray|PackedVector4Array|PackedColorArray|JSON|UPNP|OS|IP|JSONRPC|XRVRS|Variant|void)\\b",
"name": "entity.name.type.class.builtin.gdscript"
},
"const_vars": {
@@ -459,7 +454,7 @@
"name": "variable.other.constant.gdscript"
},
"pascal_case_class": {
"match": "\\b([A-Z]+[a-z_0-9]*([A-Z]?[a-z_0-9]+)*[A-Z]?)\\b",
"match": "\\b[A-Z]+(?:[a-z]+[A-Za-z0-9_]*)+\\b",
"name": "entity.name.type.class.gdscript"
},
"signal_declaration_bare": {
@@ -494,7 +489,7 @@
"end2": "(\\s*(\\-\\>)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:)",
"endCaptures2": {
"1": { "name": "punctuation.separator.annotation.result.gdscript" },
"2": { "name": "keyword.language.void.gdscript" },
"2": { "name": "entity.name.type.class.builtin.gdscript" },
"3": { "name": "entity.name.type.class.gdscript markup.italic" }
},
"patterns": [