Compare commits

...

20 Commits
2.0.0 ... 2.1.0

Author SHA1 Message Date
Hugo Locurcio
80fefd2ceb Bump to version 2.1.0
Thanks to all contributors involved in the making of this release,
in particular DaelonSuzuka who has been central in this effort of
maintaining this extension!
2024-07-28 18:29:56 +02:00
David Kincaid
9750f8dfc6 Fix various syntax highlighting issues (#674)
* Fix variable declaration highlighting

* Clean up function call highlighting
2024-07-02 14:04:22 -04:00
David Kincaid
cca25099c4 More Formatter Fixes (#672)
* Fix nodepath function highlighting/tokenization

* Reverted dangerous line removal behavior change

* Fix detection of match keyword vs .match() function

* Rearrange formatter options

* Fix option default value

* Add biome linter/formatter config file

* Fix linter errors

* Add system to supply custom config values in tests

* Remove unused variable

* Implement tests for both formatter options

* Clean up formatter option handling

* Fix extra space inserted in list of nodepaths

* Add token rules for square and curly braces
2024-06-29 16:08:24 -04:00
Marvin Altemeier
6456a789af Improve dragging items from Scene Preview into source code (#661)
Co-authored-by: David Kincaid <daelonsuzuka@gmail.com>
2024-06-29 15:50:18 -04:00
David Kincaid
1ff626ba47 Fix object id decoded as wrong signedness. (#670)
* Fix id being decoded as an signed integer instead of an unsigned integer. (#660)

---------

Co-authored-by: karstensensensen <simonblsoerensen@gmail.com>
2024-06-24 17:43:58 -04:00
David Kincaid
c07fe37f30 Fix bad formatting on several operators (#605)
Many, many formatter and syntax highlighting improvements.
2024-06-24 16:48:44 -04:00
David Kincaid
17af8e20c9 Improve Scene Preview ergonomics (#665)
* Rename Scene Preview "pinning" to "locking"

* Add "refresh scene preview" command as button

* Expose scene preview commands to command palette

* Add openCurrentScene and openMainScript commands
2024-06-23 17:13:40 -04:00
David Kincaid
f55d36e86c Fix handling of editorPaths (#656) 2024-06-20 05:00:55 -04:00
Stephen Bell
df445b8390 improve macOS path resolution for app bundles (#632) 2024-04-29 19:55:52 -04:00
David Kincaid
6b009ea123 Fix relative godotPath values not resolving correctly (#655) 2024-04-29 16:24:52 -04:00
dependabot[bot]
2e6eedb196 Bump actions/upload-artifact from 4.3.2 to 4.3.3 (#651)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.3.2...v4.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-24 17:16:26 -04:00
dependabot[bot]
d9d834b165 Bump actions/upload-artifact from 4.3.1 to 4.3.2 (#648)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.3.1...v4.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 11:44:05 -04:00
Vladimir Pyatnitskiy
0fc399bbc3 Fix debugged process not being terminated when debugging session closes on Linux (#620) 2024-04-17 13:05:32 -04:00
Joseph Gilley
56a7871d06 Fix poor documentation formatting of class titles and inheritance chain (#628) 2024-04-17 12:59:18 -04:00
David Kincaid
019d87e5c7 Prevent document links from accidentally being resolved to your entire document (#639) 2024-04-17 12:45:54 -04:00
InfiniteXyy
a9508d965d Fix project not found when project.godot file is excluded (#635)
* Fix project not found when `project.godot` file is excluded

* Ignore user excludes when using workspace.findFiles

* Fix linter warnings

---------

Co-authored-by: David Kincaid <daelonsuzuka@gmail.com>
2024-04-06 13:45:47 -04:00
David Kincaid
24e72ecc36 Fix LSP connection attempts not resetting (#638)
* Fix linter warnings

* Fix LSP retry count not resetting on connection
2024-04-06 13:42:36 -04:00
Joseph Gilley
ecaf1db977 Improve codeblock formatting in documentation (#629)
* Improve codeblock formatting in documentation

Improves codeblock highlighting by:

 -  Adding context to the codeblock detailing which language the
    following code is.
 -  Setting the `pre` block's background to match the default
    background for `code` blocks to give a consistent experience.
 -  Setting a border radius for the `pre` block double that of the
    `code` block's default (4px, which is hard-coded).
2024-03-22 10:03:38 -04:00
Vladimir Pyatnitskiy
5cef963162 Fix child processes not being killed properly (#613) 2024-03-11 13:57:38 -04:00
David Kincaid
e89cb784da Fix broken scene file parser (#603) 2024-02-25 18:24:16 -05:00
49 changed files with 1778 additions and 357 deletions

View File

@@ -18,7 +18,7 @@ body:
description: >
Specify the Git commit hash if using a development or non-official build.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: 3.3.stable, 4.0.dev (3041becc6)
placeholder: 4.2.2.stable, 4.3.rc (88d932506)
validations:
required: true
@@ -29,7 +29,7 @@ body:
Use the **Help > About** menu to see your current version.
Specify the Git commit hash if using a development or non-official build.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: "1.64.2"
placeholder: "1.91.1"
validations:
required: true
@@ -40,7 +40,7 @@ body:
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
Specify the Git commit hash if using a development or non-official build.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: "2.0.0"
placeholder: "2.1.0"
validations:
required: true

View File

@@ -18,7 +18,7 @@ body:
description: >
Specify the Git commit hash if using a development or non-official build.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: 3.3.stable, 4.0.dev (3041becc6)
placeholder: 4.2.2.stable, 4.3.rc (88d932506)
validations:
required: true
@@ -29,7 +29,7 @@ body:
Use the **Help > About** menu to see your current version.
Specify the Git commit hash if using a development or non-official build.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: "1.64.2"
placeholder: "1.91.1"
validations:
required: true
@@ -40,7 +40,7 @@ body:
Open the **Extensions** side panel and click on the **godot-tools** extension to see your current version.
Specify the Git commit hash if using a development or non-official build.
If you use a custom build, please test if your issue is reproducible in official builds too.
placeholder: "2.0.0"
placeholder: "2.1.0"
validations:
required: true

View File

@@ -48,7 +48,7 @@ jobs:
ls -l godot-tools.vsix
- name: Upload extension VSIX
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.3
with:
name: godot-tools
path: godot-tools.vsix

View File

@@ -1,5 +1,26 @@
# Changelog
### 2.1.0
- [Improve dragging items from Scene Preview into source code](https://github.com/godotengine/godot-vscode-plugin/pull/661)
- [Improve macOS path resolution for app bundles](https://github.com/godotengine/godot-vscode-plugin/pull/632)
- [Improve codeblock formatting in documentation](https://github.com/godotengine/godot-vscode-plugin/pull/629)
- [Improve Scene Preview ergonomics](https://github.com/godotengine/godot-vscode-plugin/pull/665)
- "Pinning" in the scene preview is now referred to as "locking" to avoid confusion with pinning a scene as the debug/launch target.
- Added commands for opening the Scene Preview's target scene, and the "main script" of the target scene, if it exists.
- Added existing "refresh scene preview" command as a button.
- [Prevent document links from accidentally being resolved to your entire document](https://github.com/godotengine/godot-vscode-plugin/pull/639)
- [Fix poor documentation formatting of class titles and inheritance chain](https://github.com/godotengine/godot-vscode-plugin/pull/628)
- [Fix bad formatting on several operators](https://github.com/godotengine/godot-vscode-plugin/pull/605)
- [Fix various formatting issues](https://github.com/godotengine/godot-vscode-plugin/pull/672)
- [Fix various syntax highlighting issues](https://github.com/godotengine/godot-vscode-plugin/pull/674)
- [Fix Object ID decoded as wrong signedness](https://github.com/godotengine/godot-vscode-plugin/pull/670)
- [Fix project not found when `project.godot` file is excluded](https://github.com/godotengine/godot-vscode-plugin/pull/635)
- [Fix LSP connection attempts not resetting](https://github.com/godotengine/godot-vscode-plugin/pull/638)
- [Fix child processes not being killed properly](https://github.com/godotengine/godot-vscode-plugin/pull/613)
- [Fix broken scene file parser](https://github.com/godotengine/godot-vscode-plugin/pull/603)
- [Fix debugged process not being terminated when debugging session closes on Linux](https://github.com/godotengine/godot-vscode-plugin/pull/620)
### 2.0.0
- [**Rewrite debugger for Godot 4 support + improved maintainability**](https://github.com/godotengine/godot-vscode-plugin/pull/452)

15
biome.json Normal file
View File

@@ -0,0 +1,15 @@
{
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "tab",
"indentWidth": 4,
"lineWidth": 120,
"lineEnding": "lf",
"include": ["src/**/*.ts"]
},
"files": {
"include": ["src/**/*.ts"],
"ignore": ["node_modules"]
}
}

View File

@@ -1,11 +1,3 @@
.codeblock {
padding: 0.5em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
!background-color: #fdf6e3;
}
body {
margin-right: 200px;
}
@@ -22,3 +14,34 @@ a {
height: 100%;
z-index: 100;
}
pre:has(code) {
position: relative;
background-color: var(--vscode-textPreformat-background);
padding: 2.5em 1.5em 1em 1.5em;
border-radius: 8px;
overflow-x: auto;
}
pre code {
background-color: transparent !important;
padding-left: 0px;
padding-right: 0px;
}
pre:has(code[class*="language-"])::before {
position: absolute;
top: 0;
left: 0;
padding: 0.5em 1em;
color: var(--vscode-titlebar-activeForeground);
font-family: var(--vscode-font-family);
}
pre:has(code.language-gdscript)::before {
content: "GDScript";
}
pre:has(code.language-csharp)::before {
content: "C#";
}

972
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "godot-tools",
"displayName": "godot-tools",
"icon": "icon.png",
"version": "2.0.0",
"version": "2.1.0",
"description": "Tools for game development with Godot Engine and GDScript",
"repository": {
"type": "git",
@@ -149,19 +149,47 @@
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.refresh",
"title": "Refresh Scene Preview"
"title": "Refresh Scene Preview",
"icon": {
"light": "resources/godot_icons/light/Reload.svg",
"dark": "resources/godot_icons/dark/Reload.svg"
}
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.pin",
"title": "Pin Scene Preview",
"icon": "resources/pin_off.svg"
"command": "godotTools.scenePreview.openCurrentScene",
"title": "Open the Scene Preview's current scene",
"icon": {
"light": "resources/godot_icons/light/PackedScene.svg",
"dark": "resources/godot_icons/dark/PackedScene.svg"
}
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.unpin",
"title": "Unpin Scene Preview",
"icon": "resources/pin_on.svg"
"command": "godotTools.scenePreview.openMainScript",
"title": "Open the main script of the Scene Preview's current scene",
"icon": {
"light": "resources/godot_icons/light/Script.svg",
"dark": "resources/godot_icons/dark/Script.svg"
}
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.lock",
"title": "Lock Scene Preview",
"icon": {
"light": "resources/godot_icons/light/Unlock.svg",
"dark": "resources/godot_icons/dark/Unlock.svg"
}
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.unlock",
"title": "Unlock Scene Preview",
"icon": {
"light": "resources/godot_icons/light/Lock.svg",
"dark": "resources/godot_icons/dark/Lock.svg"
}
},
{
"category": "Godot Tools",
@@ -243,6 +271,24 @@
"default": true,
"description": "Whether to reveal the terminal when launching the Godot Editor"
},
"godotTools.formatter.maxEmptyLines": {
"type": "string",
"enum": [
"1",
"2"
],
"enumDescriptions": [
"1 empty line. A more compact style.",
"2 empty lines. Conforms to the official GDScript style guide."
],
"default": "2",
"description": "Number of empty lines allowed anywhere in the file"
},
"godotTools.formatter.denseFunctionParameters": {
"type": "boolean",
"default": false,
"description": "Whether extra space should be removed from function parameter lists"
},
"godotTools.lsp.serverProtocol": {
"type": [
"string"
@@ -628,10 +674,6 @@
"command": "godotTools.listGodotClasses",
"when": "godotTools.context.connectedToLSP"
},
{
"command": "godotTools.scenePreview.refresh",
"when": "false"
},
{
"command": "godotTools.scenePreview.goToDefinition",
"when": "false"
@@ -640,14 +682,6 @@
"command": "godotTools.scenePreview.openDocumentation",
"when": "false"
},
{
"command": "godotTools.scenePreview.pin",
"when": "false"
},
{
"command": "godotTools.scenePreview.unpin",
"when": "false"
},
{
"command": "godotTools.scenePreview.copyNodePath",
"when": "false"
@@ -693,14 +727,29 @@
"group": "navigation"
},
{
"command": "godotTools.scenePreview.pin",
"when": "view == scenePreview && !godotTools.context.scenePreview.pinned",
"group": "navigation"
"command": "godotTools.scenePreview.lock",
"when": "view == scenePreview && !godotTools.context.scenePreview.locked",
"group": "navigation@1"
},
{
"command": "godotTools.scenePreview.unpin",
"when": "view == scenePreview && godotTools.context.scenePreview.pinned",
"group": "navigation"
"command": "godotTools.scenePreview.unlock",
"when": "view == scenePreview && godotTools.context.scenePreview.locked",
"group": "navigation@1"
},
{
"command": "godotTools.scenePreview.refresh",
"when": "view == scenePreview",
"group": "navigation@2"
},
{
"command": "godotTools.scenePreview.openMainScript",
"when": "view == scenePreview",
"group": "navigation@3"
},
{
"command": "godotTools.scenePreview.openCurrentScene",
"when": "view == scenePreview",
"group": "navigation@4"
}
],
"view/item/context": [
@@ -836,7 +885,7 @@
"@typescript-eslint/parser": "^5.57.1",
"@vscode/test-cli": "^0.0.4",
"@vscode/test-electron": "^2.3.8",
"@vscode/vsce": "^2.21.0",
"@vscode/vsce": "^2.29.0",
"chai": "^4.3.10",
"esbuild": "^0.17.15",
"eslint": "^8.37.0",
@@ -858,7 +907,7 @@
"vscode-languageclient": "^7.0.0",
"vscode-oniguruma": "^2.0.1",
"vscode-textmate": "^9.0.0",
"ws": "^8.13.0",
"ws": "^8.17.1",
"ya-bbcode": "^4.0.0"
}
}

View File

@@ -107,12 +107,11 @@ export class ServerController {
if (args.editor_path) {
log.info("Using 'editor_path' variable from launch.json");
godotPath = args.editor_path.replace(/^"/, "").replace(/"$/, "");
log.info(`Verifying version of '${godotPath}'`);
result = verify_godot_version(godotPath, "3");
log.info(`Verifying version of '${args.editor_path}'`);
result = verify_godot_version(args.editor_path, "3");
godotPath = result.godotPath;
log.info(`Verification result: ${result.status}, version: "${result.version}"`);
switch (result.status) {
case "WRONG_VERSION": {
const projectVersion = await get_project_version();
@@ -129,16 +128,19 @@ export class ServerController {
this.abort();
return;
}
default: {
break;
}
}
} else {
log.info("Using 'editorPath.godot3' from settings");
const settingName = "editorPath.godot3";
godotPath = get_configuration(settingName).replace(/^"/, "").replace(/"$/, "");
godotPath = get_configuration(settingName);
log.info(`Verifying version of '${godotPath}'`);
result = verify_godot_version(godotPath, "3");
godotPath = result.godotPath;
log.info(`Verification result: ${result.status}, version: "${result.version}"`);
switch (result.status) {
@@ -221,7 +223,7 @@ export class ServerController {
}
log.info(`Launching game process using command: '${command}'`);
const debugProcess = subProcess("debug", command, { shell: true });
const debugProcess = subProcess("debug", command, { shell: true, detached: true });
debugProcess.stdout.on("data", (data) => { });
debugProcess.stderr.on("data", (data) => { });
@@ -375,10 +377,16 @@ export class ServerController {
break;
}
case "message:inspect_object": {
const id = BigInt(command.parameters[0]);
let id = BigInt(command.parameters[0]);
const className: string = command.parameters[1];
const properties: any[] = command.parameters[2];
// message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer,
// thus we need to convert it to its equivalent unsigned value here.
if (id < 0) {
id = id + BigInt(2) ** BigInt(64);
}
const rawObject = new RawObject(className);
properties.forEach((prop) => {
rawObject.set(prop[0], prop[5]);

View File

@@ -108,12 +108,11 @@ export class ServerController {
if (args.editor_path) {
log.info("Using 'editor_path' variable from launch.json");
godotPath = args.editor_path.replace(/^"/, "").replace(/"$/, "");
log.info(`Verifying version of '${godotPath}'`);
result = verify_godot_version(godotPath, "4");
log.info(`Verifying version of '${args.editor_path}'`);
result = verify_godot_version(args.editor_path, "4");
godotPath = result.godotPath;
log.info(`Verification result: ${result.status}, version: "${result.version}"`);
switch (result.status) {
case "WRONG_VERSION": {
const projectVersion = await get_project_version();
@@ -130,16 +129,19 @@ export class ServerController {
this.abort();
return;
}
default: {
break;
}
}
} else {
log.info("Using 'editorPath.godot4' from settings");
const settingName = "editorPath.godot4";
godotPath = get_configuration(settingName).replace(/^"/, "").replace(/"$/, "");
godotPath = get_configuration(settingName);
log.info(`Verifying version of '${godotPath}'`);
result = verify_godot_version(godotPath, "4");
godotPath = result.godotPath;
log.info(`Verification result: ${result.status}, version: "${result.version}"`);
switch (result.status) {
@@ -227,7 +229,7 @@ export class ServerController {
}
log.info(`Launching game process using command: '${command}'`);
const debugProcess = subProcess("debug", command, { shell: true });
const debugProcess = subProcess("debug", command, { shell: true, detached: true });
debugProcess.stdout.on("data", (data) => { });
debugProcess.stderr.on("data", (data) => { });
@@ -374,10 +376,16 @@ export class ServerController {
break;
}
case "scene:inspect_object": {
const id = BigInt(command.parameters[0]);
let id = BigInt(command.parameters[0]);
const className: string = command.parameters[1];
const properties: any[] = command.parameters[2];
// message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer,
// thus we need to convert it to its equivalent unsigned value here.
if (id < 0) {
id = id + BigInt(2) ** BigInt(64);
}
const rawObject = new RawObject(className);
properties.forEach((prop) => {
rawObject.set(prop[0], prop[5]);

View File

@@ -1,6 +1,6 @@
import * as path from "path";
import * as path from "node:path";
import * as vscode from "vscode";
import { attemptSettingsUpdate, get_extension_uri } from "./utils";
import { attemptSettingsUpdate, get_extension_uri, clean_godot_path } from "./utils";
import {
GDInlayHintsProvider,
GDHoverProvider,
@@ -81,9 +81,13 @@ export function activate(context: vscode.ExtensionContext) {
async function initial_setup() {
const projectVersion = await get_project_version();
if (projectVersion === undefined) {
// TODO: actually handle this?
return;
}
const settingName = `editorPath.godot${projectVersion[0]}`;
const godotPath = get_configuration(settingName);
const result = verify_godot_version(godotPath, projectVersion[0]);
const result = verify_godot_version(get_configuration(settingName), projectVersion[0]);
const godotPath = result.godotPath;
switch (result.status) {
case "SUCCESS": {
@@ -153,8 +157,8 @@ async function open_workspace_with_editor() {
const projectVersion = await get_project_version();
const settingName = `editorPath.godot${projectVersion[0]}`;
const godotPath = get_configuration(settingName).replace(/^"/, "").replace(/"$/, "");
const result = verify_godot_version(godotPath, projectVersion[0]);
const result = verify_godot_version(get_configuration(settingName), projectVersion[0]);
const godotPath = result.godotPath;
switch (result.status) {
case "SUCCESS": {
@@ -204,9 +208,7 @@ async function get_godot_path(): Promise<string|undefined> {
return undefined;
}
const settingName = `editorPath.godot${projectVersion[0]}`;
// Cleans up any surrounding quotes the user might put into the path.
const godotPath : string = get_configuration(settingName).replace(/^"/, "").replace(/"$/, "");
return godotPath;
return clean_godot_path(get_configuration(settingName));
}
class GodotEditorTerminal implements vscode.Pseudoterminal {

View File

@@ -1,8 +1,8 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
import * as path from "node:path";
import * as fs from "node:fs";
import { format_document } from "./textmate";
import { format_document, type FormatterOptions } from "./textmate";
import * as chai from "chai";
const expect = chai.expect;
@@ -10,6 +10,20 @@ const expect = chai.expect;
const dots = ["..", "..", ".."];
const basePath = path.join(__filename, ...dots);
function get_options(testFolderPath: string) {
const options: FormatterOptions = {
maxEmptyLines: 2,
denseFunctionParameters: false,
};
const optionsPath = path.join(testFolderPath, "config.json");
if (fs.existsSync(optionsPath)) {
const file = fs.readFileSync(optionsPath).toString();
const config = JSON.parse(file);
return { ...options, ...config } as FormatterOptions;
}
return options;
}
suite("GDScript Formatter Tests", () => {
// Search for all folders in the snapshots folder and run a test for each
// comparing the output of the formatter with the expected output.
@@ -18,15 +32,19 @@ suite("GDScript Formatter Tests", () => {
const snapshotsFolderPath = path.join(basePath, "src/formatter/snapshots");
const testFolders = fs.readdirSync(snapshotsFolderPath);
// biome-ignore lint/complexity/noForEach: <explanation>
testFolders.forEach((testFolder) => {
const testFolderPath = path.join(snapshotsFolderPath, testFolder);
if (fs.statSync(testFolderPath).isDirectory()) {
test(`Snapshot Test: ${testFolder}`, async () => {
const uriIn = vscode.Uri.file(path.join(testFolderPath, "in.gd"));
const uriOut = vscode.Uri.file(path.join(testFolderPath, "out.gd"));
const documentIn = await vscode.workspace.openTextDocument(uriIn);
const documentOut = await vscode.workspace.openTextDocument(uriOut);
const edits = format_document(documentIn);
const options = get_options(testFolderPath);
const edits = format_document(documentIn, options);
// Apply the formatting edits
const workspaceEdit = new vscode.WorkspaceEdit();
@@ -34,7 +52,9 @@ suite("GDScript Formatter Tests", () => {
await vscode.workspace.applyEdit(workspaceEdit);
// Compare the result with the expected output
expect(documentIn.getText().replace("\r\n", "\n")).to.equal(documentOut.getText().replace("\r\n", "\n"));
expect(documentIn.getText().replace("\r\n", "\n")).to.equal(
documentOut.getText().replace("\r\n", "\n"),
);
});
}
});

View File

View File

View File

@@ -26,6 +26,9 @@ func f():
a= 1.0* .2
a =1.0 * 2.
a = 10**10
a = min(10, 10**10)
a= a% b
a =1%2
@@ -36,3 +39,16 @@ func f():
var v = Vector2( 1 , - 1 )
var w = Vector2(1,10-1)
print( - 1 )
print( 1 - 1 )
print( - 1 + (1-1))
print( - 1 + (-1-1))
if a > - 1:
if a < - 1:
if a == - 1:
pass
return - 1

View File

@@ -26,6 +26,9 @@ func f():
a = 1.0 * .2
a = 1.0 * 2.
a = 10 ** 10
a = min(10, 10 ** 10)
a = a % b
a = 1 % 2
@@ -36,3 +39,16 @@ func f():
var v = Vector2(1, -1)
var w = Vector2(1, 10 - 1)
print(-1)
print(1 - 1)
print(-1 + (1 - 1))
print(-1 + (-1 - 1))
if a > -1:
if a < -1:
if a == -1:
pass
return -1

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,3 +8,7 @@ func f():
func g():
print(true and ( not false ) or ( true))
print(true and not false or not (true) )
func h():
print(true && ( not false ) || ( true))
print(true && not false || not (true) )

View File

@@ -8,3 +8,7 @@ func f():
func g():
print(true and (not false) or (true))
print(true and not false or not (true))
func h():
print(true && (not false) || (true))
print(true && not false || not (true))

View File

@@ -1,6 +0,0 @@
func test():
pass

View File

@@ -1,3 +0,0 @@
func test():
pass

View File

@@ -2,14 +2,14 @@ var a = 10
var b := 10
var c: int = 10
func f(b:=10):
return func(c:=10):
func f(b := 10):
return func(c := 10):
pass
func f(b: int=10):
return func(c: int=10):
func f(b: int = 10):
return func(c: int = 10):
pass
func f(b=10):
return func(c=10):
func f(b = 10):
return func(c = 10):
pass

View File

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

View File

@@ -0,0 +1,4 @@
func f(x):
match x:
var y when y>20:
pass

View File

@@ -0,0 +1,4 @@
func f(x):
match x:
var y when y > 20:
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,3 +69,6 @@ var a = $"%Child/GrandChild".some_method()
var a = $Child.get_node('GrandChild').some_method()
var a = $"Child".get_node('GrandChild').some_method()
var a = $"%Child".get_node('GrandChild').some_method()
func f():
$Child.add_child(%Unique)

View File

@@ -69,3 +69,6 @@ var a = $"%Child/GrandChild".some_method()
var a = $Child.get_node('GrandChild').some_method()
var a = $"Child".get_node('GrandChild').some_method()
var a = $"%Child".get_node('GrandChild').some_method()
func f():
$Child.add_child(%Unique)

View File

@@ -20,7 +20,7 @@ export const keywords = [
"is",
"master",
"mastersync",
"match",
"when",
"not",
"onready",
"or",
@@ -58,10 +58,14 @@ export const symbols = [
"&=",
"^=",
"|=",
"~=",
"<<=",
">>=",
":=",
"->",
"&",
"|",
"^",
"-",
"+",
"/",

View File

@@ -1,16 +1,17 @@
import { Range, TextDocument, TextEdit } from "vscode";
import * as fs from "fs";
import { TextEdit } from "vscode";
import type { TextDocument, TextLine } from "vscode";
import * as fs from "node:fs";
import * as vsctm from "vscode-textmate";
import * as oniguruma from "vscode-oniguruma";
import { keywords, symbols } from "./symbols";
import { get_extension_uri, createLogger } from "../utils";
import { get_configuration, get_extension_uri, createLogger } from "../utils";
const log = createLogger("formatter.tm");
// Promisify readFile
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (error, data) => error ? reject(error) : resolve(data));
fs.readFile(path, (error, data) => (error ? reject(error) : resolve(data)));
});
}
@@ -22,17 +23,21 @@ const wasmBin = fs.readFileSync(wasmPath).buffer;
const registry = new vsctm.Registry({
onigLib: oniguruma.loadWASM(wasmBin).then(() => {
return {
createOnigScanner(patterns) { return new oniguruma.OnigScanner(patterns); },
createOnigString(s) { return new oniguruma.OnigString(s); }
createOnigScanner(patterns) {
return new oniguruma.OnigScanner(patterns);
},
createOnigString(s) {
return new oniguruma.OnigString(s);
},
};
}),
loadGrammar: (scopeName) => {
if (scopeName === "source.gdscript") {
return readFile(grammarPath).then(data => vsctm.parseRawGrammar(data.toString(), grammarPath));
return readFile(grammarPath).then((data) => vsctm.parseRawGrammar(data.toString(), grammarPath));
}
// console.log(`Unknown scope name: ${scopeName}`);
return null;
}
},
});
interface Token {
@@ -47,6 +52,20 @@ interface Token {
skip?: boolean;
}
export interface FormatterOptions {
maxEmptyLines: 1 | 2;
denseFunctionParameters: boolean;
}
function get_formatter_options() {
const options: FormatterOptions = {
maxEmptyLines: get_configuration("formatter.maxEmptyLines") === "1" ? 1 : 2,
denseFunctionParameters: get_configuration("formatter.denseFunctionParameters"),
};
return options;
}
function parse_token(token: Token) {
if (token.scopes.includes("string.quoted.gdscript")) {
token.string = true;
@@ -56,6 +75,12 @@ function parse_token(token: Token) {
}
if (token.scopes.includes("meta.literal.nodepath.gdscript")) {
token.skip = true;
token.type = "nodepath";
return;
}
if (token.scopes.includes("keyword.control.flow.gdscript")) {
token.type = "keyword";
return;
}
if (keywords.includes(token.value)) {
token.type = "keyword";
@@ -65,6 +90,10 @@ function parse_token(token: Token) {
token.type = "symbol";
return;
}
// "preload" is highlighted as a keyword but it behaves like a function
if (token.value === "preload") {
return;
}
if (token.scopes.includes("keyword.language.gdscript")) {
token.type = "keyword";
return;
@@ -79,7 +108,7 @@ function parse_token(token: Token) {
}
}
function between(tokens: Token[], current: number) {
function between(tokens: Token[], current: number, options: FormatterOptions) {
const nextToken = tokens[current];
const prevToken = tokens[current - 1];
const next = nextToken.value;
@@ -89,26 +118,44 @@ function between(tokens: Token[], current: number) {
if (!prev) return "";
if (next === "##") return " ";
if (next === "#") return " ";
if (prevToken.skip && nextToken.skip) return "";
if (prev === "(") return "";
if (nextToken.param) {
if (prev === "-" && tokens[current - 2]?.value === ",") {
return "";
if (options.denseFunctionParameters) {
if (prev === "-") {
if (tokens[current - 2]?.value === "=") return "";
if (["keyword", "symbol"].includes(tokens[current - 2].type)) {
return "";
}
if ([",", "("].includes(tokens[current - 2]?.value)) {
return "";
}
}
if (next === "%") return " ";
if (prev === "%") return " ";
if (next === "=") {
if (tokens[current - 2]?.value === ":") return " ";
return "";
}
if (prev === "=") {
if (tokens[current - 3]?.value === ":") return " ";
return "";
}
if (prevToken?.type === "symbol") return " ";
if (nextToken.type === "symbol") return " ";
} else {
if (next === ":") {
if (tokens[current + 1]?.value === "=") return " ";
}
}
if (next === "%") return " ";
if (prev === "%") return " ";
if (next === "=") return "";
if (prev === "=") return "";
if (next === ":=") return "";
if (prev === ":=") return "";
if (prevToken?.type === "symbol") return " ";
if (nextToken.type === "symbol") return " ";
}
if (next === ":") {
if (["var", "const"].includes(tokens[current - 2]?.value)) {
if (tokens[current + 1]?.value !== "=") return "";
if (tokens[current + 1]?.value !== "=") return "";
return " ";
}
@@ -117,7 +164,10 @@ function between(tokens: Token[], current: number) {
if (prev === "@") return "";
if (prev === "-") {
if (tokens[current - 2]?.value === "(") {
if (["keyword", "symbol"].includes(tokens[current - 2].type)) {
return "";
}
if ([",", "(", "["].includes(tokens[current - 2]?.value)) {
return "";
}
}
@@ -132,8 +182,10 @@ function between(tokens: Token[], current: number) {
if (prev === ")" && nextToken.type === "keyword") return " ";
if (prev === "[" && nextToken.type === "symbol") return "";
if (prev === "[" && nextToken.type === "nodepath") return "";
if (prev === ":") return " ";
if (prev === ";") return " ";
if (prev === "##") return " ";
if (prev === "#") return " ";
if (next === "=") return " ";
if (prev === "=") return " ";
@@ -157,17 +209,26 @@ function between(tokens: Token[], current: number) {
let grammar = null;
registry.loadGrammar("source.gdscript").then(g => { grammar = g; });
registry.loadGrammar("source.gdscript").then((g) => {
grammar = g;
});
export function format_document(document: TextDocument): TextEdit[] {
function is_comment(line: TextLine): boolean {
return line.text[line.firstNonWhitespaceCharacterIndex] === "#";
}
export function format_document(document: TextDocument, _options?: FormatterOptions): TextEdit[] {
// quit early if grammar is not loaded
if (!grammar) {
return [];
}
const edits: TextEdit[] = [];
const options = _options ?? get_formatter_options();
let lineTokens: vsctm.ITokenizeLineResult = null;
let onlyEmptyLinesSoFar = true;
let emptyLineCount = 0;
for (let lineNum = 0; lineNum < document.lineCount; lineNum++) {
const line = document.lineAt(lineNum);
@@ -177,24 +238,29 @@ export function format_document(document: TextDocument): TextEdit[] {
if (onlyEmptyLinesSoFar) {
edits.push(TextEdit.delete(line.rangeIncludingLineBreak));
} else {
// Limit the number of consecutive empty lines
const maxEmptyLines: number = 1;
if (maxEmptyLines === 1) {
if (lineNum < document.lineCount - 1 && document.lineAt(lineNum + 1).isEmptyOrWhitespace) {
edits.push(TextEdit.delete(line.rangeIncludingLineBreak));
}
} else if (maxEmptyLines === 2) {
if (lineNum < document.lineCount - 2 && document.lineAt(lineNum + 1).isEmptyOrWhitespace && document.lineAt(lineNum + 2).isEmptyOrWhitespace) {
edits.push(TextEdit.delete(line.rangeIncludingLineBreak));
}
emptyLineCount++;
}
// delete empty lines at the end of the file
if (lineNum === document.lineCount - 1) {
for (let i = lineNum - emptyLineCount + 1; i < document.lineCount; i++) {
edits.push(TextEdit.delete(document.lineAt(i).rangeIncludingLineBreak));
}
}
continue;
}
onlyEmptyLinesSoFar = false;
// delete consecutive empty lines
if (emptyLineCount) {
for (let i = emptyLineCount - options.maxEmptyLines; i > 0; i--) {
edits.push(TextEdit.delete(document.lineAt(lineNum - i).rangeIncludingLineBreak));
}
emptyLineCount = 0;
}
// skip comments
if (line.text[line.firstNonWhitespaceCharacterIndex] === "#") {
if (is_comment(line)) {
continue;
}
@@ -228,7 +294,7 @@ export function format_document(document: TextDocument): TextEdit[] {
if (i > 0 && tokens[i - 1].string === true && tokens[i].string === true) {
nextLine += tokens[i].original;
} else {
nextLine += between(tokens, i) + tokens[i].value.trim();
nextLine += between(tokens, i, options) + tokens[i].value.trim();
}
}

View File

@@ -35,7 +35,7 @@ export class ClientConnectionManager {
private status: ManagerStatus = ManagerStatus.INITIALIZING;
private statusWidget: vscode.StatusBarItem = null;
private connectedVersion: string = "";
private connectedVersion = "";
constructor(private context: vscode.ExtensionContext) {
this.context = context;
@@ -105,9 +105,11 @@ export class ClientConnectionManager {
targetVersion = "4.2";
}
const settingName = `editorPath.godot${projectVersion[0]}`;
const godotPath = get_configuration(settingName).replace(/^"/, "").replace(/"$/, "");
let godotPath = get_configuration(settingName);
const result = verify_godot_version(godotPath, projectVersion[0]);
godotPath = result.godotPath;
switch (result.status) {
case "WRONG_VERSION": {
const message = `Cannot launch headless LSP: The current project uses Godot v${projectVersion}, but the specified Godot executable is v${result.version}`;
@@ -125,11 +127,11 @@ export class ClientConnectionManager {
if (result.version[2] < minimumVersion) {
const message = `Cannot launch headless LSP: Headless LSP mode is only available on v${targetVersion} or newer, but the specified Godot executable is v${result.version}.`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Open Settings", "Disable Headless LSP", "Ignore").then(item => {
if (item == "Select Godot executable") {
if (item === "Select Godot executable") {
select_godot_executable(settingName);
} else if (item == "Open Settings") {
} else if (item === "Open Settings") {
vscode.commands.executeCommand("workbench.action.openSettings", settingName);
} else if (item == "Disable Headless LSP") {
} else if (item === "Disable Headless LSP") {
set_configuration("lsp.headless", false);
prompt_for_reload();
}
@@ -192,7 +194,7 @@ export class ClientConnectionManager {
const message = `Connected to the GDScript language server at ${lspTarget}.`;
let options = ["Ok"];
if (this.target == TargetLSP.HEADLESS) {
if (this.target === TargetLSP.HEADLESS) {
options = ["Restart LSP", ...options];
}
vscode.window.showInformationMessage(message, ...options).then(item => {
@@ -262,6 +264,7 @@ export class ClientConnectionManager {
break;
case ClientStatus.CONNECTED:
this.retry = false;
this.reconnectionAttempts = 0;
set_context("connectedToLSP", true);
this.status = ManagerStatus.CONNECTED;
if (!this.client.started) {
@@ -271,7 +274,7 @@ export class ClientConnectionManager {
case ClientStatus.DISCONNECTED:
set_context("connectedToLSP", false);
if (this.retry) {
if (this.client.port != -1) {
if (this.client.port !== -1) {
this.status = ManagerStatus.INITIALIZING_LSP;
} else {
this.status = ManagerStatus.RETRYING;
@@ -317,15 +320,15 @@ export class ClientConnectionManager {
const message = `Couldn't connect to the GDScript language server at ${lspTarget}. Is the Godot editor or language server running?`;
let options = ["Retry", "Ignore"];
if (this.target == TargetLSP.EDITOR) {
if (this.target === TargetLSP.EDITOR) {
options = ["Open workspace with Godot Editor", ...options];
}
vscode.window.showErrorMessage(message, ...options).then(item => {
if (item == "Retry") {
if (item === "Retry") {
this.connect_to_language_server();
}
if (item == "Open workspace with Godot Editor") {
if (item === "Open workspace with Godot Editor") {
vscode.commands.executeCommand("godotTools.openEditor");
this.connect_to_language_server();
}

View File

@@ -21,7 +21,7 @@ export enum TargetLSP {
const CUSTOM_MESSAGE = "gdscript_client/";
export default class GDScriptLanguageClient extends LanguageClient {
public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") == "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") === "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
private _status_changed_callbacks: ((v: ClientStatus) => void)[] = [];
private _initialize_request: Message = null;
@@ -29,18 +29,18 @@ export default class GDScriptLanguageClient extends LanguageClient {
public target: TargetLSP = TargetLSP.EDITOR;
public port: number = -1;
public lastPortTried: number = -1;
public port = -1;
public lastPortTried = -1;
public sentMessages = new Map();
public lastSymbolHovered: string = "";
public lastSymbolHovered = "";
private _started: boolean = false;
private _started = false;
public get started(): boolean { return this._started; }
private _status: ClientStatus;
public get status(): ClientStatus { return this._status; }
public set status(v: ClientStatus) {
if (this._status != v) {
if (this._status !== v) {
this._status = v;
for (const callback of this._status_changed_callbacks) {
callback(v);
@@ -49,7 +49,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
public watch_status(callback: (v: ClientStatus) => void) {
if (this._status_changed_callbacks.indexOf(callback) == -1) {
if (this._status_changed_callbacks.indexOf(callback) === -1) {
this._status_changed_callbacks.push(callback);
}
}
@@ -95,7 +95,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
port = this.port;
}
if (this.target == TargetLSP.EDITOR) {
if (this.target === TargetLSP.EDITOR) {
if (port === 6005 || port === 6008) {
port = 6005;
}
@@ -117,7 +117,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
private on_send_message(message: RequestMessage) {
this.sentMessages.set(message.id, message);
if (message.method == "initialize") {
if (message.method === "initialize") {
this._initialize_request = message;
}
}
@@ -186,7 +186,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
const contents = message["contents"];
let decl: string;
if (contents instanceof Array) {
if (Array.isArray(contents)) {
decl = contents[0];
} else {
decl = contents.value;
@@ -196,7 +196,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
decl = decl.split("\n")[0].trim();
let match;
let match: RegExpMatchArray;
let result = undefined;
match = decl.match(/(?:func|const) (@?\w+)\.(\w+)/);
if (match) {
@@ -222,7 +222,7 @@ export default class GDScriptLanguageClient extends LanguageClient {
}
private on_disconnected() {
if (this.target == TargetLSP.EDITOR) {
if (this.target === TargetLSP.EDITOR) {
const host = get_configuration("lsp.serverHost");
let port = get_configuration("lsp.serverPort");

View File

@@ -62,7 +62,7 @@ export class GDDocumentLinkProvider implements DocumentLinkProvider {
links.push(link);
}
}
for (const match of text.matchAll(/res:\/\/[^"^']*/g)) {
for (const match of text.matchAll(/res:\/\/([^"'\n]*)/g)) {
const r = this.create_range(document, match);
const uri = await convert_resource_path_to_uri(match[0]);
if (uri instanceof Uri) {

View File

@@ -243,22 +243,12 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
}
if (symbol.kind == SymbolKind.Class) {
let doc = element("h2", `Native class ${symbol.name}`);
const parts = /extends\s+([A-z0-9]+)/.exec(symbol.detail);
let inherits = parts && parts.length > 1 ? parts[1] : "";
if (inherits) {
let inherits_chain = "";
let base_class = symbol.class_info[inherits];
while (base_class) {
inherits_chain += `${inherits_chain ? " >" : ""} ${make_link(
base_class.name,
undefined
)}`;
base_class = symbol.class_info[base_class.inherits];
}
inherits = `Inherits: ${inherits_chain}`;
doc += element("p", inherits);
let doc = element("h2", `Class: ${symbol.name}`);
if (symbol.class_info.inherits) {
const inherits = make_link(symbol.class_info.inherits, undefined);
doc += element("p", `Inherits: ${inherits}`);
}
if (symbol.class_info && symbol.class_info.extended_classes) {
let inherited = "";
for (const c of symbol.class_info.extended_classes) {
@@ -267,6 +257,8 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
doc += element("p", `Inherited by:${inherited}`);
}
doc += element("p", format_documentation(symbol.documentation, symbol.native_class));
let constants = "";
let signals = "";
let methods_index = "";
@@ -307,10 +299,6 @@ export function make_symbol_document(symbol: GodotNativeSymbol): string {
}
};
doc += element(
"p",
format_documentation(symbol.documentation, symbol.native_class)
);
add_group("Properties", properties_index);
add_group("Constants", constants);
add_group("Signals", signals);

View File

@@ -12,6 +12,7 @@ export class SceneParser {
constructor() {
if (SceneParser.instance) {
// biome-ignore lint/correctness/noConstructorReturn: <explanation>
return SceneParser.instance;
}
SceneParser.instance = this;
@@ -24,7 +25,7 @@ export class SceneParser {
if (this.scenes.has(path)) {
const scene = this.scenes.get(path);
if (scene.mtime == stats.mtimeMs) {
if (scene.mtime === stats.mtimeMs) {
return scene;
}
}
@@ -83,26 +84,32 @@ export class SceneParser {
const nodes = {};
let lastNode = null;
const nodeRegex = /\[node name="([\w]*)"(?: type="([\w]*)")?(?: parent="([\w\/.]*)")?(?: instance=ExtResource\(\s*"?([\w]+)"?\s*\))?\]/g;
const nodeRegex = /\[node.*/g;
for (const match of text.matchAll(nodeRegex)) {
const name = match[1];
const type = match[2] ? match[2] : "PackedScene";
let parent = match[3];
const instance = match[4] ? match[4] : 0;
const line = match[0];
const name = line.match(/name="([\w]+)"/)?.[1];
const type = line.match(/type="([\w]+)"/)?.[1] ?? "PackedScene";
let parent = line.match(/parent="([\w\/.]+)"/)?.[1];
const instance = line.match(/instance=ExtResource\(\s*"?([\w]+)"?\s*\)/)?.[1];
// leaving this in case we have a reason to use these node paths in the future
// const rawNodePaths = line.match(/node_paths=PackedStringArray\(([\w",\s]*)\)/)?.[1];
// const nodePaths = rawNodePaths?.split(",").forEach(x => x.trim().replace("\"", ""));
let _path = "";
let relativePath = "";
if (parent == undefined) {
if (parent === undefined) {
root = name;
_path = name;
} else if (parent == ".") {
} else if (parent === ".") {
parent = root;
relativePath = name;
_path = parent + "/" + name;
_path = `${parent}/${name}`;
} else {
relativePath = parent + "/" + name;
parent = root + "/" + parent;
_path = parent + "/" + name;
relativePath = `${parent}/${name}`;
parent = `${root}/${parent}`;
_path = `${parent}/${name}`;
}
if (lastNode) {
lastNode.body = text.slice(lastNode.position, match.index);
@@ -136,7 +143,7 @@ export class SceneParser {
}
node.contextValue += "hasResourcePath";
}
if (_path == root) {
if (_path === root) {
scene.root = node;
}
if (parent in nodes) {

View File

@@ -1,23 +1,23 @@
import * as vscode from "vscode";
import {
TreeDataProvider,
TreeDragAndDropController,
ExtensionContext,
type TreeDataProvider,
type TreeDragAndDropController,
type ExtensionContext,
EventEmitter,
Event,
TreeView,
ProviderResult,
TreeItem,
type Event,
type TreeView,
type ProviderResult,
type TreeItem,
TreeItemCollapsibleState,
window,
languages,
Uri,
CancellationToken,
FileDecoration,
DocumentDropEditProvider,
type Uri,
type CancellationToken,
type FileDecoration,
type DocumentDropEditProvider,
workspace,
} from "vscode";
import * as fs from "fs";
import * as fs from "node:fs";
import {
get_configuration,
find_file,
@@ -26,17 +26,20 @@ import {
register_command,
createLogger,
make_docs_uri,
node_name_to_snake,
} from "../utils";
import { SceneParser } from "./parser";
import { SceneNode, Scene } from "./types";
import type { SceneNode, Scene } from "./types";
const log = createLogger("scenes.preview");
export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDragAndDropController<SceneNode>, DocumentDropEditProvider {
export class ScenePreviewProvider
implements TreeDataProvider<SceneNode>, TreeDragAndDropController<SceneNode>, DocumentDropEditProvider
{
public dropMimeTypes = [];
public dragMimeTypes = [];
private tree: TreeView<SceneNode>;
private scenePreviewPinned = false;
private scenePreviewLocked = false;
private currentScene = "";
public parser = new SceneParser();
public scene: Scene;
@@ -52,7 +55,7 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
constructor(private context: ExtensionContext) {
this.tree = vscode.window.createTreeView("scenePreview", {
treeDataProvider: this,
dragAndDropController: this
dragAndDropController: this,
});
const selector = [
@@ -60,12 +63,14 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
{ language: "gdscript", scheme: "file" },
];
context.subscriptions.push(
register_command("scenePreview.pin", this.pin_preview.bind(this)),
register_command("scenePreview.unpin", this.unpin_preview.bind(this)),
register_command("scenePreview.lock", this.lock_preview.bind(this)),
register_command("scenePreview.unlock", this.unlock_preview.bind(this)),
register_command("scenePreview.copyNodePath", this.copy_node_path.bind(this)),
register_command("scenePreview.copyResourcePath", this.copy_resource_path.bind(this)),
register_command("scenePreview.openScene", this.open_scene.bind(this)),
register_command("scenePreview.openScript", this.open_script.bind(this)),
register_command("scenePreview.openCurrentScene", this.open_current_scene.bind(this)),
register_command("scenePreview.openCurrentScript", this.open_main_script.bind(this)),
register_command("scenePreview.goToDefinition", this.go_to_definition.bind(this)),
register_command("scenePreview.openDocumentation", this.open_documentation.bind(this)),
register_command("scenePreview.refresh", this.refresh.bind(this)),
@@ -82,21 +87,60 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
this.refresh();
}
public handleDrag(source: readonly SceneNode[], data: vscode.DataTransfer, token: vscode.CancellationToken): void | Thenable<void> {
public handleDrag(
source: readonly SceneNode[],
data: vscode.DataTransfer,
token: vscode.CancellationToken,
): void | Thenable<void> {
data.set("godot/path", new vscode.DataTransferItem(source[0].relativePath));
data.set("godot/class", new vscode.DataTransferItem(source[0].className));
data.set("godot/unique", new vscode.DataTransferItem(source[0].unique));
data.set("godot/label", new vscode.DataTransferItem(source[0].label));
}
public provideDocumentDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): vscode.ProviderResult<vscode.DocumentDropEdit> {
const path = dataTransfer.get("godot/path").value;
const className = dataTransfer.get("godot/class").value;
public provideDocumentDropEdits(
document: vscode.TextDocument,
position: vscode.Position,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): vscode.ProviderResult<vscode.DocumentDropEdit> {
const path: string = dataTransfer.get("godot/path").value;
const className: string = dataTransfer.get("godot/class").value;
const line = document.lineAt(position.line);
const unique = dataTransfer.get("godot/unique").value === "true";
const label: string = dataTransfer.get("godot/label").value;
// TODO: compare the source scene to the target file
// What should happen when you drag a node into a script that isn't the
// "main" script for that scene?
// Attempt to calculate a relative path that resolves correctly?
if (className) {
// For the root node, the path is empty and needs to be replaced with the node name
const savePath = path || label;
if (path && className) {
if (document.languageId === "gdscript") {
return new vscode.DocumentDropEdit(`$${path}`);
let qualifiedPath = `$${savePath}`;
if (unique) {
// For unique nodes, we can use the % syntax and drop the full path
qualifiedPath = `%${label}`;
}
if (line.text === "") {
// We assume that if the user is dropping a node in an empty line, they are at the top of
// the script and want to declare an onready variable
return new vscode.DocumentDropEdit(
`@onready var ${node_name_to_snake(label)}: ${className} = ${qualifiedPath}`,
);
}
// In any other place, we assume the user wants to get a reference to the node itself
return new vscode.DocumentDropEdit(qualifiedPath);
}
if (document.languageId === "csharp") {
return new vscode.DocumentDropEdit(`GetNode<${className}>("${path}")`);
return new vscode.DocumentDropEdit(`GetNode<${className}>("${savePath}")`);
}
}
}
@@ -106,7 +150,7 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
return;
}
setTimeout(async () => {
if (uri.fsPath == this.currentScene) {
if (uri.fsPath === this.currentScene) {
this.refresh();
} else {
const document = await vscode.workspace.openTextDocument(uri);
@@ -116,7 +160,7 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
}
public async refresh() {
if (this.scenePreviewPinned) {
if (this.scenePreviewLocked) {
return;
}
@@ -128,7 +172,7 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
if (!fileName.endsWith(".tscn")) {
const searchName = fileName.replace(".gd", ".tscn").replace(".cs", ".tscn");
if (mode == "anyFolder") {
if (mode === "anyFolder") {
const relatedScene = await find_file(searchName);
if (!relatedScene) {
return;
@@ -136,14 +180,14 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
fileName = relatedScene.fsPath;
}
if (mode == "sameFolder") {
if (mode === "sameFolder") {
if (fs.existsSync(searchName)) {
fileName = searchName;
} else {
return;
}
}
if (mode == "off") {
if (mode === "off") {
return;
}
}
@@ -162,20 +206,20 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
}
}
private pin_preview() {
this.scenePreviewPinned = true;
set_context("scenePreview.pinned", true);
private lock_preview() {
this.scenePreviewLocked = true;
set_context("scenePreview.locked", true);
}
private unpin_preview() {
this.scenePreviewPinned = false;
set_context("scenePreview.pinned", false);
private unlock_preview() {
this.scenePreviewLocked = false;
set_context("scenePreview.locked", false);
this.refresh();
}
private copy_node_path(item: SceneNode) {
if (item.unique) {
vscode.env.clipboard.writeText("%" + item.label);
vscode.env.clipboard.writeText(`%${item.label}`);
return;
}
vscode.env.clipboard.writeText(item.relativePath);
@@ -201,6 +245,26 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
}
}
private async open_current_scene() {
if (this.currentScene) {
const document = await vscode.workspace.openTextDocument(this.currentScene);
vscode.window.showTextDocument(document);
}
}
private async open_main_script() {
if (this.currentScene) {
const root = this.scene.root;
if (root?.hasScript) {
const path = this.scene.externalResources[root.scriptId].path;
const uri = await convert_resource_path_to_uri(path);
if (uri) {
vscode.window.showTextDocument(uri, { preview: true });
}
}
}
}
private async go_to_definition(item: SceneNode) {
const document = await vscode.workspace.openTextDocument(this.currentScene);
const start = document.positionAt(item.position);
@@ -216,7 +280,6 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
private tree_selection_changed(event: vscode.TreeViewSelectionChangeEvent<SceneNode>) {
// const item = event.selection[0];
// log(item.body);
// const editor = vscode.window.activeTextEditor;
// const range = editor.document.getText()
// editor.revealRange(range)
@@ -226,12 +289,10 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode>, TreeDr
if (!element) {
if (!this.scene?.root) {
return Promise.resolve([]);
} else {
return Promise.resolve([this.scene?.root]);
}
} else {
return Promise.resolve(element.children);
return Promise.resolve([this.scene?.root]);
}
return Promise.resolve(element.children);
}
public getTreeItem(element: SceneNode): TreeItem | Thenable<TreeItem> {
@@ -254,13 +315,13 @@ class UniqueDecorationProvider implements vscode.FileDecorationProvider {
return this.changeDecorationsEvent.event;
}
constructor(private previewer: ScenePreviewProvider) { }
constructor(private previewer: ScenePreviewProvider) {}
provideFileDecoration(uri: Uri, token: CancellationToken): FileDecoration | undefined {
if (uri.scheme !== "godot") return undefined;
const node = this.previewer.scene?.nodes.get(uri.path);
if (node && node.unique) {
if (node?.unique) {
return {
badge: "%",
};
@@ -274,13 +335,13 @@ class ScriptDecorationProvider implements vscode.FileDecorationProvider {
return this.changeDecorationsEvent.event;
}
constructor(private previewer: ScenePreviewProvider) { }
constructor(private previewer: ScenePreviewProvider) {}
provideFileDecoration(uri: Uri, token: CancellationToken): FileDecoration | undefined {
if (uri.scheme !== "godot") return undefined;
const node = this.previewer.scene?.nodes.get(uri.path);
if (node && node.hasScript) {
if (node?.hasScript) {
return {
badge: "S",
};

View File

@@ -15,13 +15,13 @@ export function is_debug_mode(): boolean {
export async function find_file(file: string): Promise<vscode.Uri | null> {
if (fs.existsSync(file)) {
return vscode.Uri.file(file);
} else {
const fileName = path.basename(file);
const results = await vscode.workspace.findFiles("**/" + fileName);
if (results.length == 1) {
return results[0];
}
}
const fileName = path.basename(file);
const results = await vscode.workspace.findFiles(`**/${fileName}`, null);
if (results.length === 1) {
return results[0];
}
return null;
}
@@ -38,7 +38,26 @@ export async function get_free_port(): Promise<number> {
export function make_docs_uri(path: string, fragment?: string) {
return vscode.Uri.from({
scheme: "gddoc",
path: path + ".gddoc",
path: `${path}.gddoc`,
fragment: fragment,
});
}
/**
* Can be used to convert a conventional node name to a snake_case variable name.
*
* @example
* ```ts
* nodeNameToVar("MyNode") // my_node
* nodeNameToVar("Sprite2D") // sprite_2d
* nodeNameToVar("UI") // ui
* ```
*/
export function node_name_to_snake(name: string): string {
const snakeCase: string = name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").toLowerCase();
if (snakeCase.startsWith("_")) {
return snakeCase.substring(1);
}
return snakeCase;
}

View File

@@ -1,7 +1,8 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
import { execSync } from "child_process";
import * as path from "node:path";
import * as fs from "node:fs";
import * as os from "node:os";
import { execSync } from "node:child_process";
let projectDir: string | undefined = undefined;
let projectFile: string | undefined = undefined;
@@ -9,11 +10,11 @@ let projectFile: string | undefined = undefined;
export async function get_project_dir(): Promise<string | undefined> {
let file = "";
if (vscode.workspace.workspaceFolders !== undefined) {
const files = await vscode.workspace.findFiles("**/project.godot");
const files = await vscode.workspace.findFiles("**/project.godot", null);
if (files.length === 0) {
return undefined;
}
}
if (files.length === 1) {
file = files[0].fsPath;
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
@@ -21,7 +22,7 @@ export async function get_project_dir(): Promise<string | undefined> {
}
} else if (files.length > 1) {
// if multiple project files, pick the top-most one
const best = files.reduce((a, b) => a.fsPath.length <= b.fsPath.length ? a : b);
const best = files.reduce((a, b) => (a.fsPath.length <= b.fsPath.length ? a : b));
if (best) {
file = best.fsPath;
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
@@ -70,14 +71,14 @@ export async function get_project_version(): Promise<string | undefined> {
return projectVersion;
}
export function find_project_file(start: string, depth: number = 20) {
export function find_project_file(start: string, depth = 20) {
// TODO: rename this, it's actually more like "find_parent_project_file"
// This function appears to be fast enough, but if speed is ever an issue,
// memoizing the result should be straightforward
if (start === ".") {
if (fs.existsSync("project.godot") && fs.statSync("project.godot").isFile()) {
return "project.godot";
}
}
return null;
}
const folder = path.dirname(start);
@@ -102,23 +103,47 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
type VERIFY_STATUS = "SUCCESS" | "WRONG_VERSION" | "INVALID_EXE";
type VERIFY_RESULT = {
status: VERIFY_STATUS,
version?: string,
}
status: VERIFY_STATUS;
godotPath: string;
version?: string;
};
export function verify_godot_version(godotPath: string, expectedVersion: "3" | "4" | string): VERIFY_RESULT {
let target = clean_godot_path(godotPath);
let output = "";
try {
const output = execSync(`"${godotPath}" --version`).toString().trim();
const pattern = /^(([34])\.([0-9]+)(?:\.[0-9]+)?)/m;
const match = output.match(pattern);
if (!match) {
return { status: "INVALID_EXE" };
}
if (match[2] !== expectedVersion) {
return { status: "WRONG_VERSION", version: match[1] };
}
return { status: "SUCCESS", version: match[1] };
output = execSync(`"${target}" --version`).toString().trim();
} catch {
return { status: "INVALID_EXE" };
if (path.isAbsolute(target)) {
return { status: "INVALID_EXE", godotPath: target };
}
const workspacePath = vscode.workspace.workspaceFolders[0].uri.fsPath;
target = path.resolve(workspacePath, target);
try {
output = execSync(`"${target}" --version`).toString().trim();
} catch {
return { status: "INVALID_EXE", godotPath: target };
}
}
const pattern = /^(([34])\.([0-9]+)(?:\.[0-9]+)?)/m;
const match = output.match(pattern);
if (!match) {
return { status: "INVALID_EXE", godotPath: target };
}
if (match[2] !== expectedVersion) {
return { status: "WRONG_VERSION", godotPath: target, version: match[1] };
}
return { status: "SUCCESS", godotPath: target, version: match[1] };
}
export function clean_godot_path(godotPath: string): string {
let target = godotPath.replace(/^"/, "").replace(/"$/, "");
if (os.platform() === "darwin" && target.endsWith(".app")) {
target = path.join(target, "Contents", "MacOS", "Godot");
}
return target;
}

View File

@@ -28,7 +28,7 @@ export function killSubProcesses(owner: string) {
} else if (process.platform === "darwin") {
execSync(`kill -9 ${c.pid}`);
} else {
process.kill(c.pid);
process.kill(-c.pid);
}
}
} catch {

View File

@@ -55,11 +55,10 @@
{ "include": "#nodepath_object" },
{ "include": "#nodepath_function" },
{ "include": "#strings" },
{ "include": "#builtin_classes" },
{ "include": "#const_vars" },
{ "include": "#keywords" },
{ "include": "#logic_operator" },
{ "include": "#compare_operator" },
{ "include": "#arithmetic_operator" },
{ "include": "#operators" },
{ "include": "#lambda_declaration" },
{ "include": "#class_declaration" },
{ "include": "#variable_declaration" },
@@ -70,6 +69,9 @@
{ "include": "#assignment_operator" },
{ "include": "#in_keyword" },
{ "include": "#control_flow" },
{ "include": "#match_keyword" },
{ "include": "#curly_braces" },
{ "include": "#square_braces" },
{ "include": "#round_braces" },
{ "include": "#function_call" },
{ "include": "#comment" },
@@ -77,13 +79,12 @@
{ "include": "#func" },
{ "include": "#letter" },
{ "include": "#numbers" },
{ "include": "#builtin_classes" },
{ "include": "#pascal_case_class" },
{ "include": "#line_continuation" }
]
},
"comment": {
"match": "(#).*$\\n?",
"match": "(##|#).*$\\n?",
"name": "comment.line.number-sign.gdscript",
"captures": { "1": { "name": "punctuation.definition.comment.number-sign.gdscript" } }
},
@@ -97,14 +98,37 @@
"name": "constant.character.escape.gdscript",
"match": "\\\\."
},
{ "include": "#string_formatting" }
{ "include": "#string_percent_placeholders" },
{ "include": "#string_bracket_placeholders" }
]
},
"string_formatting": {
"string_percent_placeholders": {
"name": "meta.format.percent.gdscript",
"match": "(?x)\n (\n % (\\([\\w\\s]*\\))?\n [-+#0 ]*\n (\\d+|\\*)? (\\.(\\d+|\\*))?\n ([hlL])?\n [diouxXeEfFgGcrsab%]\n )\n",
"captures": { "1": { "name": "constant.character.format.placeholder.other.gdscript" } }
},
"string_bracket_placeholders": {
"patterns": [
{
"name": "meta.format.brace.gdscript",
"match": "(?x)\n (\n {{ | }}\n | (?:\n {\n \\w* (\\.[[:alpha:]_]\\w* | \\[[^\\]'\"]+\\])*\n (![rsa])?\n ( : \\w? [<>=^]? [-+ ]? \\#?\n \\d* ,? (\\.\\d+)? [bcdeEfFgGnosxX%]? )?\n })\n )\n",
"captures": {
"1": { "name": "constant.character.format.placeholder.other.gdscript" },
"3": { "name": "storage.type.format.gdscript" },
"4": { "name": "storage.type.format.gdscript" }
}
},
{
"name": "meta.format.brace.gdscript",
"match": "(?x)\n (\n {\n \\w* (\\.[[:alpha:]_]\\w* | \\[[^\\]'\"]+\\])*\n (![rsa])?\n (:)\n [^'\"{}\\n]* (?:\n \\{ [^'\"}\\n]*? \\} [^'\"{}\\n]*\n )*\n }\n )\n",
"captures": {
"1": { "name": "constant.character.format.placeholder.other.gdscript" },
"3": { "name": "storage.type.format.gdscript" },
"4": { "name": "storage.type.format.gdscript" }
}
}
]
},
"nodepath_object": {
"name": "meta.literal.nodepath.gdscript",
"begin": "(NodePath)\\s*(?:\\()",
@@ -125,6 +149,8 @@
]
},
"nodepath_function": {
"name": "meta.function.gdscript",
"contentName": "meta.function.parameters.gdscript",
"begin": "(get_node_or_null|has_node|has_node_and_resource|find_node|get_node)\\s*(\\()",
"beginCaptures": {
"1": { "name": "entity.name.function.gdscript" },
@@ -143,7 +169,8 @@
"name": "keyword.control.flow"
}
]
}
},
{ "include": "#base_expression" }
]
},
"self": {
@@ -154,10 +181,6 @@
"match": "\\bfunc\\b",
"name": "keyword.language.gdscript"
},
"logic_operator": {
"match": "\\b(and|or|not|!)\\b",
"name": "keyword.operator.wordlike.gdscript"
},
"in_keyword": {
"patterns": [
{
@@ -180,12 +203,33 @@
}
]
},
"operators": {
"patterns": [
{ "include": "#wordlike_operator" },
{ "include": "#boolean_operator" },
{ "include": "#arithmetic_operator" },
{ "include": "#bitwise_operator" },
{ "include": "#compare_operator" }
]
},
"wordlike_operator": {
"match": "\\b(and|or|not)\\b",
"name": "keyword.operator.wordlike.gdscript"
},
"boolean_operator": {
"match": "(&&|\\|\\|)",
"name": "keyword.operator.boolean.gdscript"
},
"bitwise_operator": {
"match": "&|\\||<<=|>>=|<<|>>|\\^|~",
"name": "keyword.operator.bitwise.gdscript"
},
"compare_operator": {
"match": "<=|>=|==|<|>|!=",
"match": "<=|>=|==|<|>|!=|!",
"name": "keyword.operator.comparison.gdscript"
},
"arithmetic_operator": {
"match": "->|\\+=|-=|\\*=|/=|%=|&=|\\|=|\\*|/|%|\\+|-|<<|>>|&|\\||\\^|~|!",
"match": "->|\\+=|-=|\\*=|\\^=|/=|%=|&=|~=|\\|=|\\*\\*|\\*|/|%|\\+|-",
"name": "keyword.operator.arithmetic.gdscript"
},
"assignment_operator": {
@@ -193,11 +237,15 @@
"name": "keyword.operator.assignment.gdscript"
},
"control_flow": {
"match": "\\b(?:if|elif|else|while|break|continue|pass|return|match|yield|await)\\b",
"match": "\\b(?:if|elif|else|while|break|continue|pass|return|when|yield|await)\\b",
"name": "keyword.control.gdscript"
},
"match_keyword": {
"match": "^\n\\s*(match)",
"captures": { "1": { "name": "keyword.control.gdscript" } }
},
"keywords": {
"match": "\\b(?:class|class_name|is|onready|tool|static|export|as|void|enum|preload|assert|breakpoint|rpc|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace)\\b",
"match": "\\b(?:class|class_name|is|onready|tool|static|export|as|void|enum|assert|breakpoint|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace)\\b",
"name": "keyword.language.gdscript"
},
"letter": {
@@ -233,13 +281,11 @@
]
},
"variable_declaration": {
"name": "meta.variable.gdscript",
"begin": "\\b(?:(var)|(const))\\s+(?:(\\b[A-Z_][A-Z_0-9]*\\b)|([A-Za-z_]\\w*))\\s*",
"name": "meta.variable.declaration.gdscript",
"begin": "\\b(?:(var)|(const))\\b",
"beginCaptures": {
"1": { "name": "keyword.language.gdscript storage.type.var.gdscript" },
"2": { "name": "keyword.language.gdscript storage.type.const.gdscript" },
"3": { "name": "constant.language.gdscript" },
"4": { "name": "variable.other.gdscript" }
"2": { "name": "keyword.language.gdscript storage.type.const.gdscript" }
},
"end": "$|;",
"patterns": [
@@ -397,7 +443,7 @@
"name": "constant.language.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_0-9]*([A-Z]?[a-z_0-9]+)*[A-Z]?)\\b",
"name": "entity.name.type.class.gdscript"
},
"signal_declaration_bare": {
@@ -494,7 +540,7 @@
"beginCaptures": {
"1": { "name": "variable.parameter.function.language.gdscript" },
"2": { "name": "punctuation.separator.annotation.gdscript" },
"3": { "name": "entity.name.type.class.builtin.gdscript" }
"3": { "name": "entity.name.type.class.gdscript" }
},
"end": "(,)|(?=\\))",
"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
@@ -506,6 +552,26 @@
}
]
},
"curly_braces": {
"begin": "\\{",
"end": "\\}",
"beginCaptures": { "0": { "name": "punctuation.definition.dict.begin.gdscript" } },
"endCaptures": { "0": { "name": "punctuation.definition.dict.end.gdscript" } },
"patterns": [
{ "include": "#base_expression" },
{ "include": "#any_variable" }
]
},
"square_braces": {
"begin": "\\[",
"end": "\\]",
"beginCaptures": { "0": { "name": "punctuation.definition.list.begin.gdscript" } },
"endCaptures": { "0": { "name": "punctuation.definition.list.end.gdscript" } },
"patterns": [
{ "include": "#base_expression" },
{ "include": "#any_variable" }
]
},
"round_braces": {
"begin": "\\(",
"end": "\\)",
@@ -535,7 +601,7 @@
},
"any_method": {
"match": "\\b([A-Za-z_]\\w*)\\b(?=\\s*(?:[(]))",
"name": "support.function.any-method.gdscript"
"name": "entity.name.function.other.gdscript"
},
"any_variable": {
"match": "\\b(?<![@\\$#%])([A-Za-z_]\\w*)\\b(?![(])",
@@ -552,8 +618,7 @@
"function_call": {
"name": "meta.function-call.gdscript",
"comment": "Regular function call of the type \"name(args)\"",
"begin": "(?x)\n \\b(?=\n ([a-zA-Z_]\\w*) \\s* (\\()\n )\n",
"beginCaptures": { "2": { "name": "punctuation.definition.arguments.begin.gdscript" } },
"begin": "(?=\\b[a-zA-Z_]\\w*\\b\\()",
"end": "(\\))",
"endCaptures": { "1": { "name": "punctuation.definition.arguments.end.gdscript" } },
"patterns": [
@@ -564,17 +629,21 @@
"function_name": {
"patterns": [
{ "include": "#builtin_classes" },
{
"match": "\\b(preload)\\b",
"name": "keyword.language.gdscript"
},
{
"comment": "Some color schemas support meta.function-call.generic scope",
"name": "support.function.any-method.gdscript",
"match": "(?x)\n \\b ([a-zA-Z_]\\w*) \\b\n"
"match": "\\b([a-zA-Z_]\\w*)\\b",
"name": "entity.name.function.gdscript"
}
]
},
"function_arguments": {
"begin": "(\\()",
"end": "(?=\\))(?!\\)\\s*\\()",
"beginCaptures": { "1": { "name": "punctuation.definition.arguments.begin.gdscript" } },
"end": "(?=\\))(?!\\)\\s*\\()",
"contentName": "meta.function.parameters.gdscript",
"patterns": [
{

View File

@@ -45,6 +45,12 @@ func f():
super()
super.some_function()
match param3:
3:
print("param3 is 3!")
_:
print("param3 is not 3!")
for i in range(1): # `in` is a control keyword
print(i in range(1)) # `in` is an operator keyword