Browse Source

Add snapshot tests to formatter (#545)

* Add snapshot tests for formatter
* Add test runner to CI

---------

Co-authored-by: David Kincaid <[email protected]>
Sandy Gutierrez 1 năm trước cách đây
mục cha
commit
0a794ebc1b
43 tập tin đã thay đổi với 749 bổ sung145 xóa
  1. 30 3
      .github/workflows/ci.yml
  2. 1 0
      README.md
  3. 95 0
      package-lock.json
  4. 2 0
      package.json
  5. 30 6
      src/formatter/formatter.test.ts
  6. 35 0
      src/formatter/snapshots/arithmetic/in.gd
  7. 35 0
      src/formatter/snapshots/arithmetic/out.gd
  8. 10 0
      src/formatter/snapshots/arrays/in.gd
  9. 10 0
      src/formatter/snapshots/arrays/out.gd
  10. 2 0
      src/formatter/snapshots/assert/in.gd
  11. 2 0
      src/formatter/snapshots/assert/out.gd
  12. 2 0
      src/formatter/snapshots/await/in.gd
  13. 2 0
      src/formatter/snapshots/await/out.gd
  14. 10 0
      src/formatter/snapshots/boolean-operators/in.gd
  15. 10 0
      src/formatter/snapshots/boolean-operators/out.gd
  16. 3 1
      src/formatter/snapshots/consecutive-empty-lines-are-removed/in.gd
  17. 3 0
      src/formatter/snapshots/consecutive-empty-lines-are-removed/out.gd
  18. 7 0
      src/formatter/snapshots/enums/in.gd
  19. 7 0
      src/formatter/snapshots/enums/out.gd
  20. 17 0
      src/formatter/snapshots/indentation-style-is-preserved/in.gd
  21. 17 0
      src/formatter/snapshots/indentation-style-is-preserved/out.gd
  22. 15 0
      src/formatter/snapshots/initialization/in.gd
  23. 15 0
      src/formatter/snapshots/initialization/out.gd
  24. 5 0
      src/formatter/snapshots/lambda-functions/in.gd
  25. 5 0
      src/formatter/snapshots/lambda-functions/out.gd
  26. 8 0
      src/formatter/snapshots/line-breaks/in.gd
  27. 8 0
      src/formatter/snapshots/line-breaks/out.gd
  28. 68 0
      src/formatter/snapshots/nodepaths/in.gd
  29. 68 0
      src/formatter/snapshots/nodepaths/out.gd
  30. 7 0
      src/formatter/snapshots/reserved-words/in.gd
  31. 7 0
      src/formatter/snapshots/reserved-words/out.gd
  32. 7 0
      src/formatter/snapshots/return-expression/in.gd
  33. 7 0
      src/formatter/snapshots/return-expression/out.gd
  34. 8 0
      src/formatter/snapshots/return-type/in.gd
  35. 8 0
      src/formatter/snapshots/return-type/out.gd
  36. 3 0
      src/formatter/snapshots/semicolon/in.gd
  37. 3 0
      src/formatter/snapshots/semicolon/out.gd
  38. 5 0
      src/formatter/snapshots/strings/in.gd
  39. 5 0
      src/formatter/snapshots/strings/out.gd
  40. 0 5
      src/formatter/tests/test1.in.gd
  41. 46 14
      src/formatter/textmate.ts
  42. 112 116
      syntaxes/GDScript.tmLanguage.json
  43. 9 0
      syntaxes/examples/gdscript2.gd

+ 30 - 3
.github/workflows/ci.yml

@@ -2,8 +2,12 @@ name: Continuous integration
 on: [push, pull_request]
 
 jobs:
-  build:
-    runs-on: ubuntu-20.04
+  test:
+    name: Test
+    strategy:
+      matrix:
+        os: [macos-latest, ubuntu-latest, windows-latest]
+    runs-on: ${{ matrix.os }}
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -13,9 +17,32 @@ jobs:
         with:
           node-version: 16.x
 
+      - name: Install dependencies
+        run: npm install
+
+      - name: Run headless test
+        uses: coactions/setup-xvfb@v1
+        with:
+          run: |
+            npm run compile
+            npm test
+
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Install Node.js
+        uses: actions/[email protected]
+        with:
+          node-version: 16.x
+
+      - name: Install dependencies
+        run: npm install
+
       - name: Lint and build extension
         run: |
-          npm install
           npm run lint
           npm run package -- --out godot-tools.vsix
           ls -l godot-tools.vsix

+ 1 - 0
README.md

@@ -30,6 +30,7 @@ Godot 3.2 or later.
   - `ctrl+click` on any symbol to jump to its definition or **open its documentation**
   - `ctrl+click` on `res://resource/path` links
   - **hover previews on `res://resource/path` links**
+  - **builtin code formatter**
   - autocompletions
   - full typed GDScript support
   - optional "Smart Mode" to improve productivity with dynamically typed scripts

+ 95 - 0
package-lock.json

@@ -24,6 +24,7 @@
 				"ya-bbcode": "^4.0.0"
 			},
 			"devDependencies": {
+				"@types/chai": "^4.3.11",
 				"@types/marked": "^4.0.8",
 				"@types/mocha": "^10.0.6",
 				"@types/node": "^18.15.0",
@@ -36,6 +37,7 @@
 				"@vscode/test-cli": "^0.0.4",
 				"@vscode/test-electron": "^2.3.8",
 				"@vscode/vsce": "^2.21.0",
+				"chai": "^4.3.10",
 				"esbuild": "^0.17.15",
 				"eslint": "^8.37.0",
 				"mocha": "^10.2.0",
@@ -765,6 +767,12 @@
 			"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
 			"dev": true
 		},
+		"node_modules/@types/chai": {
+			"version": "4.3.11",
+			"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz",
+			"integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
+			"dev": true
+		},
 		"node_modules/@types/json-schema": {
 			"version": "7.0.13",
 			"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
@@ -1536,6 +1544,15 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/assertion-error": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+			"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+			"dev": true,
+			"engines": {
+				"node": "*"
+			}
+		},
 		"node_modules/await-notify": {
 			"version": "1.0.1",
 			"resolved": "https://registry.npmjs.org/await-notify/-/await-notify-1.0.1.tgz",
@@ -1723,6 +1740,24 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 		},
+		"node_modules/chai": {
+			"version": "4.3.10",
+			"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
+			"integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
+			"dev": true,
+			"dependencies": {
+				"assertion-error": "^1.1.0",
+				"check-error": "^1.0.3",
+				"deep-eql": "^4.1.3",
+				"get-func-name": "^2.0.2",
+				"loupe": "^2.3.6",
+				"pathval": "^1.1.1",
+				"type-detect": "^4.0.8"
+			},
+			"engines": {
+				"node": ">=4"
+			}
+		},
 		"node_modules/chalk": {
 			"version": "2.4.2",
 			"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -1737,6 +1772,18 @@
 				"node": ">=4"
 			}
 		},
+		"node_modules/check-error": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
+			"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
+			"dev": true,
+			"dependencies": {
+				"get-func-name": "^2.0.2"
+			},
+			"engines": {
+				"node": "*"
+			}
+		},
 		"node_modules/cheerio": {
 			"version": "1.0.0-rc.10",
 			"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz",
@@ -2020,6 +2067,18 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/deep-eql": {
+			"version": "4.1.3",
+			"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
+			"integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
+			"dev": true,
+			"dependencies": {
+				"type-detect": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=6"
+			}
+		},
 		"node_modules/deep-extend": {
 			"version": "0.6.0",
 			"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -2804,6 +2863,15 @@
 				"node": "6.* || 8.* || >= 10.*"
 			}
 		},
+		"node_modules/get-func-name": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+			"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
+			"dev": true,
+			"engines": {
+				"node": "*"
+			}
+		},
 		"node_modules/get-intrinsic": {
 			"version": "1.1.1",
 			"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
@@ -3448,6 +3516,15 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/loupe": {
+			"version": "2.3.7",
+			"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+			"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
+			"dev": true,
+			"dependencies": {
+				"get-func-name": "^2.0.1"
+			}
+		},
 		"node_modules/lru-cache": {
 			"version": "6.0.0",
 			"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -4053,6 +4130,15 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/pathval": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+			"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+			"dev": true,
+			"engines": {
+				"node": "*"
+			}
+		},
 		"node_modules/pause-stream": {
 			"version": "0.0.11",
 			"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
@@ -4866,6 +4952,15 @@
 				"node": ">= 0.8.0"
 			}
 		},
+		"node_modules/type-detect": {
+			"version": "4.0.8",
+			"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+			"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+			"dev": true,
+			"engines": {
+				"node": ">=4"
+			}
+		},
 		"node_modules/type-fest": {
 			"version": "0.20.2",
 			"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",

+ 2 - 0
package.json

@@ -802,6 +802,7 @@
 		}
 	},
 	"devDependencies": {
+		"@types/chai": "^4.3.11",
 		"@types/marked": "^4.0.8",
 		"@types/mocha": "^10.0.6",
 		"@types/node": "^18.15.0",
@@ -814,6 +815,7 @@
 		"@vscode/test-cli": "^0.0.4",
 		"@vscode/test-electron": "^2.3.8",
 		"@vscode/vsce": "^2.21.0",
+		"chai": "^4.3.10",
 		"esbuild": "^0.17.15",
 		"eslint": "^8.37.0",
 		"mocha": "^10.2.0",

+ 30 - 6
src/formatter/formatter.test.ts

@@ -1,17 +1,41 @@
-import * as assert from "assert";
 import * as vscode from "vscode";
 import * as path from "path";
+import * as fs from "fs";
 
 import { format_document } from "./textmate";
 
+import * as chai from "chai";
+const expect = chai.expect;
+
 const dots = ["..", "..", ".."];
 const basePath = path.join(__filename, ...dots);
 
 suite("GDScript Formatter Tests", () => {
-	test("Test Formatting", async () => {
-		const uri = vscode.Uri.file(path.join(basePath, "src/formatter/tests/test1.in.gd"));
-		const document = await vscode.workspace.openTextDocument(uri);
-		const edits = format_document(document);
-		assert.strictEqual(4, edits.length);
+	// Search for all folders in the snapshots folder and run a test for each
+	// comparing the output of the formatter with the expected output.
+	// To add a new test, create a new folder in the snapshots folder
+	// and add two files, `in.gd` and `out.gd` for the input and expected output.
+	const snapshotsFolderPath = path.join(basePath, "src/formatter/snapshots");
+	const testFolders = fs.readdirSync(snapshotsFolderPath);
+
+	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);
+
+				// Apply the formatting edits
+				const workspaceEdit = new vscode.WorkspaceEdit();
+				workspaceEdit.set(uriIn, edits);
+				await vscode.workspace.applyEdit(workspaceEdit);
+
+				// Compare the result with the expected output
+				expect(documentIn.getText()).to.equal(documentOut.getText());
+			});
+		}
 	});
 });

+ 35 - 0
src/formatter/snapshots/arithmetic/in.gd

@@ -0,0 +1,35 @@
+var a = 1
+var b = 2
+
+func f():
+	a = a+b
+	a  = .1+ 2
+	a= 1. +2
+	a =1.0 + .2
+	a = 1.0+ 2.
+
+	a =a -b
+	a = .1- 2
+	a= 1.-2
+	a = 1.0 - .2
+	a =1.0- 2.
+
+	a= a/ b
+	a = .1 /2
+	a =1. /2
+	a = 1.0 / .2
+	a =1.0/2.
+
+	a = a *b
+	a=.1* 2
+	a = 1. *2
+	a= 1.0* .2
+	a =1.0 * 2.
+
+	a= a%  b
+	a =1%2
+
+	a =-1
+	a=      +1
+
+	a = ((-1 + 2) * (3-4) / 5 * 6%( -7 + 8-9 - 10)) * (- 11 + 12) / (13*14 % 15 +16)

+ 35 - 0
src/formatter/snapshots/arithmetic/out.gd

@@ -0,0 +1,35 @@
+var a = 1
+var b = 2
+
+func f():
+	a = a + b
+	a = .1 + 2
+	a = 1. + 2
+	a = 1.0 + .2
+	a = 1.0 + 2.
+
+	a = a - b
+	a = .1 - 2
+	a = 1. - 2
+	a = 1.0 - .2
+	a = 1.0 - 2.
+
+	a = a / b
+	a = .1 / 2
+	a = 1. / 2
+	a = 1.0 / .2
+	a = 1.0 / 2.
+
+	a = a * b
+	a = .1 * 2
+	a = 1. * 2
+	a = 1.0 * .2
+	a = 1.0 * 2.
+
+	a = a % b
+	a = 1 % 2
+
+	a = -1
+	a = +1
+
+	a = ((-1 + 2) * (3 - 4) / 5 * 6 % (-7 + 8 - 9 - 10)) * (-11 + 12) / (13 * 14 % 15 + 16)

+ 10 - 0
src/formatter/snapshots/arrays/in.gd

@@ -0,0 +1,10 @@
+var primes = [2,   3,  5, 7, 11, 13, 17  ]
+var primes2 = [
+		2  ,
+		3   ,
+		5,
+		7 ,
+		11 ,
+		13,
+		17
+	]

+ 10 - 0
src/formatter/snapshots/arrays/out.gd

@@ -0,0 +1,10 @@
+var primes = [2, 3, 5, 7, 11, 13, 17]
+var primes2 = [
+		2,
+		3,
+		5,
+		7,
+		11,
+		13,
+		17
+	]

+ 2 - 0
src/formatter/snapshots/assert/in.gd

@@ -0,0 +1,2 @@
+func f():
+	assert  (1!= 2)

+ 2 - 0
src/formatter/snapshots/assert/out.gd

@@ -0,0 +1,2 @@
+func f():
+	assert(1 != 2)

+ 2 - 0
src/formatter/snapshots/await/in.gd

@@ -0,0 +1,2 @@
+func f():
+	await delay(10)

+ 2 - 0
src/formatter/snapshots/await/out.gd

@@ -0,0 +1,2 @@
+func f():
+	await delay(10)

+ 10 - 0
src/formatter/snapshots/boolean-operators/in.gd

@@ -0,0 +1,10 @@
+func f():
+	print(not true  )
+	if ( not   true) and\
+		 (not true ):
+		pass
+	print(not true )
+
+func g():
+	print(true and (  not false ) or (  true))
+	print(true and not false or not (true)  )

+ 10 - 0
src/formatter/snapshots/boolean-operators/out.gd

@@ -0,0 +1,10 @@
+func f():
+	print(not true)
+	if (not true) and \
+		 (not true):
+		pass
+	print(not true)
+
+func g():
+	print(true and (not false) or (true))
+	print(true and not false or not (true))

+ 3 - 1
src/formatter/tests/test1.out.gd → src/formatter/snapshots/consecutive-empty-lines-are-removed/in.gd

@@ -1,4 +1,6 @@
-extends Node
+
 
 func test():
+
 	pass
+

+ 3 - 0
src/formatter/snapshots/consecutive-empty-lines-are-removed/out.gd

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

+ 7 - 0
src/formatter/snapshots/enums/in.gd

@@ -0,0 +1,7 @@
+enum State {   A,B, C  }
+
+enum State2 {
+	A  ,
+	B ,
+	C
+}

+ 7 - 0
src/formatter/snapshots/enums/out.gd

@@ -0,0 +1,7 @@
+enum State {A, B, C}
+
+enum State2 {
+	A,
+	B,
+	C
+}

+ 17 - 0
src/formatter/snapshots/indentation-style-is-preserved/in.gd

@@ -0,0 +1,17 @@
+func test():
+  if true:
+    pass
+  else:
+    pass
+
+func test():
+    if true:
+        pass
+    else:
+        pass
+
+func test():
+	if true:
+		pass
+	else:
+		pass

+ 17 - 0
src/formatter/snapshots/indentation-style-is-preserved/out.gd

@@ -0,0 +1,17 @@
+func test():
+  if true:
+    pass
+  else:
+    pass
+
+func test():
+    if true:
+        pass
+    else:
+        pass
+
+func test():
+	if true:
+		pass
+	else:
+		pass

+ 15 - 0
src/formatter/snapshots/initialization/in.gd

@@ -0,0 +1,15 @@
+var a= 10
+var b :=10
+var c: int = 10
+
+func f(b  :=10):
+	return func(c : =  10):
+		pass
+
+func f(b  : int= 10):
+	return func(c: int=10   ):
+		pass
+
+func f( b = 10 ):
+	return func( c = 10 ):
+		pass

+ 15 - 0
src/formatter/snapshots/initialization/out.gd

@@ -0,0 +1,15 @@
+var a = 10
+var b := 10
+var c: int = 10
+
+func f(b:=10):
+	return func(c:=10):
+		pass
+
+func f(b: int=10):
+	return func(c: int=10):
+		pass
+
+func f(b=10):
+	return func(c=10):
+		pass

+ 5 - 0
src/formatter/snapshots/lambda-functions/in.gd

@@ -0,0 +1,5 @@
+func f():
+	g(func(): return true
+	h(func():
+		return false
+	)

+ 5 - 0
src/formatter/snapshots/lambda-functions/out.gd

@@ -0,0 +1,5 @@
+func f():
+	g(func(): return true
+	h(func():
+		return false
+	)

+ 8 - 0
src/formatter/snapshots/line-breaks/in.gd

@@ -0,0 +1,8 @@
+func f():
+	if a or b\
+	  or c:
+		pass
+
+	if a or \
+	  b or c:
+		pass

+ 8 - 0
src/formatter/snapshots/line-breaks/out.gd

@@ -0,0 +1,8 @@
+func f():
+	if a or b \
+	  or c:
+		pass
+
+	if a or \
+	  b or c:
+		pass

+ 68 - 0
src/formatter/snapshots/nodepaths/in.gd

@@ -0,0 +1,68 @@
+@onready var sprite: Sprite2D = %Sprite
+@onready var sprites = [ %Sprite1,%Sprite2,%Sprite3  ]
+
+@onready var sprite_name = $Sprite
+@onready var sprite_names = [$Sprite1,    $Sprite2,   $Sprite3]
+
+func f():
+	print("$Sprite1", $Sprite1)
+	print("%Sprite1", %Sprite1)
+	var a=val % otherVal
+
+var a = $Child
+var a = $Child/   GrandChild
+var a = $Child/   GrandChild  /   GreatGrandChild
+var a = $"../Sibling"
+var a = $'../Sibling'
+var a = $"../    Sibling    "
+var a = $'    ../Sibling'
+var a = $'..' # parent
+var a = $"../.." # grandparent
+
+var a = get_node('Child')
+var a = get_node("Child/Grand Child")
+var a = get_node("../Sibling")
+
+if has_node('Child') and get_node('Child').has_node('GrandChild'):
+	pass
+
+var a = $%Unique
+var a = $Child/%Unique
+var a = $Child/ GrandChild/ %Unique
+var a = $Child/%Unique/ChildOfUnique
+var a = %Unique
+var a = %Unique/Child
+var a = %Unique/%UniqueChild
+
+var a = $"%Unique"
+var a = get_node("%Unique")
+var a = NodePath("%Unique")
+var a = $'%Unique/Child'
+var a = get_node('%Unique/Child')
+var a = NodePath('%Unique/Child')
+var a = $"%Unique/%UniqueChild"
+var a = get_node("%Unique/%Unique Child")
+var a = NodePath("%Unique/%Unique Child")
+
+if has_node('%Unique') and get_node('%Child').has_node('%GrandChild'):
+	pass
+
+var a = $badlyNamedChild
+var a = $badlyNamedChild/badly_named_grandchild
+
+var a = NodePath("Child")
+var a = NodePath('Child/GrandChild')
+var a = NodePath('../Sibling')
+
+var a = get_node("Child").some_method()
+var a = get_node("Child/GrandChild").some_method()
+var a = get_node("%Child").some_method()
+var a = $Child.some_method()
+var a = $'Child'.some_method()
+var a = $'%Child'.some_method()
+var a = $Child/GrandChild.some_method()
+var a = $"Child/GrandChild".some_method()
+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()

+ 68 - 0
src/formatter/snapshots/nodepaths/out.gd

@@ -0,0 +1,68 @@
+@onready var sprite: Sprite2D = %Sprite
+@onready var sprites = [%Sprite1, %Sprite2, %Sprite3]
+
+@onready var sprite_name = $Sprite
+@onready var sprite_names = [$Sprite1, $Sprite2, $Sprite3]
+
+func f():
+	print("$Sprite1", $Sprite1)
+	print("%Sprite1", %Sprite1)
+	var a = val % otherVal
+
+var a = $Child
+var a = $Child/GrandChild
+var a = $Child/GrandChild/GreatGrandChild
+var a = $"../Sibling"
+var a = $'../Sibling'
+var a = $"../    Sibling    "
+var a = $'    ../Sibling'
+var a = $'..' # parent
+var a = $"../.." # grandparent
+
+var a = get_node('Child')
+var a = get_node("Child/Grand Child")
+var a = get_node("../Sibling")
+
+if has_node('Child') and get_node('Child').has_node('GrandChild'):
+	pass
+
+var a = $%Unique
+var a = $Child/%Unique
+var a = $Child/GrandChild/%Unique
+var a = $Child/%Unique/ChildOfUnique
+var a = %Unique
+var a = %Unique/Child
+var a = %Unique/%UniqueChild
+
+var a = $"%Unique"
+var a = get_node("%Unique")
+var a = NodePath("%Unique")
+var a = $'%Unique/Child'
+var a = get_node('%Unique/Child')
+var a = NodePath('%Unique/Child')
+var a = $"%Unique/%UniqueChild"
+var a = get_node("%Unique/%Unique Child")
+var a = NodePath("%Unique/%Unique Child")
+
+if has_node('%Unique') and get_node('%Child').has_node('%GrandChild'):
+	pass
+
+var a = $badlyNamedChild
+var a = $badlyNamedChild/badly_named_grandchild
+
+var a = NodePath("Child")
+var a = NodePath('Child/GrandChild')
+var a = NodePath('../Sibling')
+
+var a = get_node("Child").some_method()
+var a = get_node("Child/GrandChild").some_method()
+var a = get_node("%Child").some_method()
+var a = $Child.some_method()
+var a = $'Child'.some_method()
+var a = $'%Child'.some_method()
+var a = $Child/GrandChild.some_method()
+var a = $"Child/GrandChild".some_method()
+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()

+ 7 - 0
src/formatter/snapshots/reserved-words/in.gd

@@ -0,0 +1,7 @@
+func f():
+	g(_a_signal_b)
+	g(_a_await_b)
+	g(_a_func_b)
+	g(_a_not_b)
+	g(_a_true_b)
+	g(_a_assert_b)

+ 7 - 0
src/formatter/snapshots/reserved-words/out.gd

@@ -0,0 +1,7 @@
+func f():
+	g(_a_signal_b)
+	g(_a_await_b)
+	g(_a_func_b)
+	g(_a_not_b)
+	g(_a_true_b)
+	g(_a_assert_b)

+ 7 - 0
src/formatter/snapshots/return-expression/in.gd

@@ -0,0 +1,7 @@
+func f():
+	return(some_value)
+
+func g(): return(some_value)
+
+func f():
+	return func(): return false

+ 7 - 0
src/formatter/snapshots/return-expression/out.gd

@@ -0,0 +1,7 @@
+func f():
+	return (some_value)
+
+func g(): return (some_value)
+
+func f():
+	return func(): return false

+ 8 - 0
src/formatter/snapshots/return-type/in.gd

@@ -0,0 +1,8 @@
+func f()->bool:
+	return (some_value)
+
+func g()-> void:
+	pass
+
+func f() ->void:
+	return func() -> bool: return false

+ 8 - 0
src/formatter/snapshots/return-type/out.gd

@@ -0,0 +1,8 @@
+func f() -> bool:
+	return (some_value)
+
+func g() -> void:
+	pass
+
+func f() -> void:
+	return func() -> bool: return false

+ 3 - 0
src/formatter/snapshots/semicolon/in.gd

@@ -0,0 +1,3 @@
+func f():
+	print(1);print(2);  print(3);
+	print(4)

+ 3 - 0
src/formatter/snapshots/semicolon/out.gd

@@ -0,0 +1,3 @@
+func f():
+	print(1); print(2); print(3);
+	print(4)

+ 5 - 0
src/formatter/snapshots/strings/in.gd

@@ -0,0 +1,5 @@
+func f():
+	var strings = [", ",   "",   "  ",   ", , "]
+	print("name: %s" % name)
+	print("%s / %s" % [a, b])
+	print("%s/%s" % [a, b])

+ 5 - 0
src/formatter/snapshots/strings/out.gd

@@ -0,0 +1,5 @@
+func f():
+	var strings = [", ", "", "  ", ", , "]
+	print("name: %s" % name)
+	print("%s / %s" % [a, b])
+	print("%s/%s" % [a, b])

+ 0 - 5
src/formatter/tests/test1.in.gd

@@ -1,5 +0,0 @@
-extends Node
-
-
-func test  ():
-	pass

+ 46 - 14
src/formatter/textmate.ts

@@ -3,7 +3,9 @@ import * as fs from "fs";
 import * as vsctm from "vscode-textmate";
 import * as oniguruma from "vscode-oniguruma";
 import { keywords, symbols } from "./symbols";
-import { get_extension_uri } from "../utils";
+import { get_extension_uri, createLogger } from "../utils";
+
+const log = createLogger("formatter.tm");
 
 // Promisify readFile
 function readFile(path) {
@@ -37,13 +39,18 @@ interface Token {
 	// startIndex: number;
 	// endIndex: number;
 	scopes: string[];
+	original: string;
 	value: string;
 	type?: string;
 	param?: boolean;
+	string?: boolean;
 	skip?: boolean;
 }
 
 function parse_token(token: Token) {
+	if (token.scopes.includes("string.quoted.gdscript")) {
+		token.string = true;
+	}
 	if (token.scopes.includes("meta.function.parameters.gdscript")) {
 		token.param = true;
 	}
@@ -86,14 +93,19 @@ function between(tokens: Token[], current: number) {
 	if (prevToken.skip) return "";
 
 	if (nextToken.param) {
+		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 " ";
 		}
@@ -101,6 +113,12 @@ function between(tokens: Token[], current: number) {
 	}
 	if (prev === "@") return "";
 
+	if (prev === "-") {
+		if (tokens[current - 2]?.value === "(") {
+			return "";
+		}
+	}
+
 	if (prev === ":" && next === "=") return "";
 	if (next === "(") {
 		if (prev === "export") return "";
@@ -110,13 +128,18 @@ function between(tokens: Token[], current: number) {
 
 	if (prev === ")" && nextToken.type === "keyword") return " ";
 
+	if (prev === "[" && nextToken.type === "symbol") return "";
 	if (prev === ":") return " ";
 	if (prev === ";") return " ";
 	if (prev === "#") return " ";
 	if (next === "=") return " ";
 	if (prev === "=") return " ";
+	if (tokens[current - 2]?.value === "=") {
+		if (["+", "-"].includes(prev)) return "";
+	}
 	if (prev === "(") return "";
 	if (next === "{") return " ";
+	if (next === "\\") return " ";
 	if (next === "{}") return " ";
 
 	if (prevToken?.type === "keyword") return " ";
@@ -141,18 +164,24 @@ export function format_document(document: TextDocument): TextEdit[] {
 	const edits: TextEdit[] = [];
 
 	let lineTokens: vsctm.ITokenizeLineResult = null;
+	let onlyEmptyLinesSoFar = true;
 	for (let lineNum = 0; lineNum < document.lineCount; lineNum++) {
 		const line = document.lineAt(lineNum);
 
 		// skip empty lines
 		if (line.isEmptyOrWhitespace) {
-			// delete empty lines
-			if (lineNum === 0 || document.lineAt(lineNum - 1).isEmptyOrWhitespace) {
-				const range = new Range(lineNum, 0, lineNum + 1, 0);
-				edits.push(TextEdit.delete(range));
+			// delete empty lines at the beginning of the file
+			if (onlyEmptyLinesSoFar) {
+				edits.push(TextEdit.delete(line.rangeIncludingLineBreak));
+			}
+			// delete delete the current empty line if the next line is empty too
+			else if (lineNum < document.lineCount - 1 && document.lineAt(lineNum + 1).isEmptyOrWhitespace) {
+				edits.push(TextEdit.delete(line.rangeIncludingLineBreak));
 			}
 			continue;
 		}
+		onlyEmptyLinesSoFar = false;
+
 		// skip comments
 		if (line.text[line.firstNonWhitespaceCharacterIndex] === "#") {
 			continue;
@@ -171,22 +200,25 @@ export function format_document(document: TextDocument): TextEdit[] {
 
 		const tokens: Token[] = [];
 		for (const t of lineTokens.tokens) {
-			const value = line.text.slice(t.startIndex, t.endIndex).trim();
-			// skip whitespace tokens
-			if (value.trim() === "") {
-				continue;
-			}
-
 			const token: Token = {
 				scopes: t.scopes,
-				value: value,
+				original: line.text.slice(t.startIndex, t.endIndex),
+				value: line.text.slice(t.startIndex, t.endIndex).trim(),
 			};
-
 			parse_token(token);
+			// skip whitespace tokens
+			if (!token.string && token.value.trim() === "") {
+				continue;
+			}
 			tokens.push(token);
 		}
 		for (let i = 0; i < tokens.length; i++) {
-			nextLine += between(tokens, i) + tokens[i].value;
+			// log.debug(i, tokens[i].value, tokens[i]);
+			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();
+			}
 		}
 
 		edits.push(TextEdit.replace(line.range, nextLine));

+ 112 - 116
syntaxes/GDScript.tmLanguage.json

@@ -3,49 +3,71 @@
 	"scopeName": "source.gdscript",
 	"name": "GDScript",
 	"patterns": [
-		{ "include": "#nodepath_object" },
-		{ "include": "#base_expression" },
-		{ "include": "#logic_op" },
-		{ "include": "#in_keyword" },
-		{ "include": "#getter_setter_godot4" },
-		{ "include": "#compare_op" },
-		{ "include": "#arithmetic_op" },
-		{ "include": "#assignment_op" },
-		{ "include": "#lambda_declaration" },
-		{ "include": "#control_flow" },
-		{ "include": "#annotations" },
-		{ "include": "#keywords" },
-		{ "include": "#self" },
-		{ "include": "#class_definition" },
-		{ "include": "#variable_definition" },
-		{ "include": "#class_name" },
-		{ "include": "#builtin_func" },
-		{ "include": "#builtin_get_node_shorthand" },
-		{ "include": "#builtin_classes" },
-		{ "include": "#const_vars" },
-		{ "include": "#pascal_case_class" },
-		{ "include": "#class_new" },
-		{ "include": "#class_is" },
-		{ "include": "#class_enum" },
-		{ "include": "#signal_declaration_bare" },
-		{ "include": "#signal_declaration" },
-		{ "include": "#function_declaration" },
-		{ "include": "#function_keyword" },
-		{ "include": "#any_method" },
-		{ "include": "#any_variable" },
-		{ "include": "#any_property" },
-		{ "include": "#extends" }
+		{ "include": "#statement" },
+		{ "include": "#expression" }
 	],
 	"repository": {
+		"statement": {
+			"patterns": [ { "include": "#extends_statement" } ]
+		},
+		"statement_keyword": {
+			"patterns": [
+				{
+					"name": "keyword.control.flow.gdscript",
+					"match": "(?x)\n  \\b(?<!\\.)(\n continue | assert | break | elif | else | if | pass | return | while )\\b\n"
+				},
+				{
+					"name": "storage.type.class.gdscript",
+					"match": "\\b(?<!\\.)(class)\\b"
+				},
+				{
+					"match": "(?x)\n  ^\\s*(\n    case | match\n  )(?=\\s*([-+\\w\\d(\\[{'\":#]|$))\\b\n",
+					"captures": { "1": { "name": "keyword.control.flow.gdscript" } }
+				}
+			]
+		},
+		"extends_statement": {
+			"match": "(extends)\\s+([a-zA-Z_]\\w*\\.[a-zA-Z_]\\w*)?",
+			"captures": {
+				"1": { "name": "keyword.language.gdscript" },
+				"2": { "name": "entity.other.inherited-class.gdscript" }
+			}
+		},
+		"expression": {
+			"patterns": [
+				{ "include": "#base_expression" },
+				{ "include": "#getter_setter_godot4" },
+				{ "include": "#assignment_operator" },
+				{ "include": "#annotations" },
+				{ "include": "#class_name" },
+				{ "include": "#builtin_classes" },
+				{ "include": "#class_new" },
+				{ "include": "#class_is" },
+				{ "include": "#class_enum" },
+				{ "include": "#any_method" },
+				{ "include": "#any_variable" },
+				{ "include": "#any_property" }
+			]
+		},
 		"base_expression": {
 			"patterns": [
 				{ "include": "#builtin_get_node_shorthand" },
 				{ "include": "#nodepath_object" },
 				{ "include": "#nodepath_function" },
 				{ "include": "#strings" },
+				{ "include": "#const_vars" },
 				{ "include": "#keywords" },
-				{ "include": "#logic_op" },
+				{ "include": "#logic_operator" },
+				{ "include": "#compare_operator" },
+				{ "include": "#arithmetic_operator" },
 				{ "include": "#lambda_declaration" },
+				{ "include": "#class_declaration" },
+				{ "include": "#variable_declaration" },
+				{ "include": "#signal_declaration_bare" },
+				{ "include": "#signal_declaration" },
+				{ "include": "#function_declaration" },
+				{ "include": "#statement_keyword" },
+				{ "include": "#assignment_operator" },
 				{ "include": "#in_keyword" },
 				{ "include": "#control_flow" },
 				{ "include": "#round_braces" },
@@ -54,9 +76,7 @@
 				{ "include": "#self" },
 				{ "include": "#letter" },
 				{ "include": "#numbers" },
-				{ "include": "#builtin_func" },
 				{ "include": "#builtin_classes" },
-				{ "include": "#const_vars" },
 				{ "include": "#pascal_case_class" },
 				{ "include": "#line_continuation" }
 			]
@@ -81,7 +101,7 @@
 		},
 		"string_formatting": {
 			"name": "meta.format.percent.gdscript",
-			"match": "(?x)\n  (\n    % (\\([\\w\\s]*\\))?\n      [-+#0 ]*\n      (\\d+|\\*)? (\\.(\\d+|\\*))?\n      ([hlL])?\n      [diouxXeEfFgGcrsab%]\n  )\n",
+			"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" } }
 		},
 		"nodepath_object": {
@@ -93,7 +113,7 @@
 				{
 					"begin": "(\"|')",
 					"end": "\\1",
-					"name": "constant.character.escape.gdscript",
+					"name": "string.quoted.gdscript constant.character.escape.gdscript",
 					"patterns": [
 						{
 							"match": "%",
@@ -115,7 +135,7 @@
 				{
 					"begin": "(\"|')",
 					"end": "\\1",
-					"name": "meta.literal.nodepath.gdscript constant.character.escape",
+					"name": "string.quoted.gdscript meta.literal.nodepath.gdscript constant.character.escape",
 					"patterns": [
 						{
 							"match": "%",
@@ -129,7 +149,7 @@
 			"match": "\\bself\\b",
 			"name": "variable.language.gdscript"
 		},
-		"logic_op": {
+		"logic_operator": {
 			"match": "\\b(and|or|not|!)\\b",
 			"name": "keyword.operator.wordlike.gdscript"
 		},
@@ -155,15 +175,15 @@
 				}
 			]
 		},
-		"compare_op": {
+		"compare_operator": {
 			"match": "<=|>=|==|<|>|!=",
 			"name": "keyword.operator.comparison.gdscript"
 		},
-		"arithmetic_op": {
-			"match": "\\+=|-=|\\*=|/=|%=|&=|\\|=|\\*|/|%|\\+|-|<<|>>|&|\\||\\^|~|!",
+		"arithmetic_operator": {
+			"match": "->|\\+=|-=|\\*=|/=|%=|&=|\\|=|\\*|/|%|\\+|-|<<|>>|&|\\||\\^|~|!",
 			"name": "keyword.operator.arithmetic.gdscript"
 		},
-		"assignment_op": {
+		"assignment_operator": {
 			"match": "=",
 			"name": "keyword.operator.assignment.gdscript"
 		},
@@ -172,7 +192,7 @@
 			"name": "keyword.control.gdscript"
 		},
 		"keywords": {
-			"match": "\\b(?:class|class_name|extends|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|preload|assert|breakpoint|rpc|sync|remote|master|puppet|slave|remotesync|mastersync|puppetsync|trait|namespace)\\b",
 			"name": "keyword.language.gdscript"
 		},
 		"letter": {
@@ -190,32 +210,38 @@
 					"name": "constant.numeric.integer.hexadecimal.gdscript"
 				},
 				{
-					"match": "[-]?([0-9_]+\\.[0-9_]*(e[\\-\\+]?[0-9_]+)?)",
+					"match": "[-]?([0-9][0-9_]+\\.[0-9_]*(e[\\-\\+]?[0-9_]+)?)",
 					"name": "constant.numeric.float.gdscript"
 				},
 				{
-					"match": "[-]?(\\.[0-9_]+(e[\\-\\+]?[0-9_]+)?)",
+					"match": "[-]?(\\.[0-9][0-9_]*(e[\\-\\+]?[0-9_]+)?)",
 					"name": "constant.numeric.float.gdscript"
 				},
 				{
-					"match": "[-]?([0-9_]+e[\\-\\+]?\\[0-9_])",
+					"match": "[-]?([0-9][0-9_]*e[\\-\\+]?\\[0-9_])",
 					"name": "constant.numeric.float.gdscript"
 				},
 				{
-					"match": "[-]?[0-9_]+",
+					"match": "[-]?[0-9][0-9_]*",
 					"name": "constant.numeric.integer.gdscript"
 				}
 			]
 		},
-		"variable_definition": {
-			"begin": "\\b(?:(var)|(const))\\s+([a-zA-Z_]\\w*)\\s*",
-			"end": "$|;",
+		"variable_declaration": {
+			"name": "meta.variable.gdscript",
+			"begin": "\\b(?:(var)|(const))\\s+(?:(\\b[A-Z_][A-Z_0-9]*\\b)|([A-Za-z_]\\w*))\\s*",
 			"beginCaptures": {
 				"1": { "name": "keyword.language.gdscript storage.type.var.gdscript" },
 				"2": { "name": "keyword.language.gdscript storage.type.const.gdscript" },
-				"3": { "name": "variable.other.gdscript" }
+				"3": { "name": "constant.language.gdscript" },
+				"4": { "name": "variable.other.gdscript" }
 			},
+			"end": "$|;",
 			"patterns": [
+				{
+					"match": ":=|=(?!=)",
+					"name": "keyword.operator.assignment.gdscript"
+				},
 				{
 					"match": "(:)\\s*([a-zA-Z_]\\w*)?",
 					"captures": {
@@ -223,10 +249,6 @@
 						"2": { "name": "entity.name.type.class.gdscript" }
 					}
 				},
-				{
-					"match": "=(?!=)",
-					"name": "keyword.operator.assignment.gdscript"
-				},
 				{
 					"match": "(setget)\\s+([a-zA-Z_]\\w*)(?:[,]\\s*([a-zA-Z_]\\w*))?",
 					"captures": {
@@ -235,7 +257,7 @@
 						"3": { "name": "entity.name.function.gdscript" }
 					}
 				},
-				{ "include": "#base_expression" },
+				{ "include": "#expression" },
 				{ "include": "#letter" },
 				{ "include": "#any_variable" },
 				{ "include": "#any_property" },
@@ -255,19 +277,12 @@
 					"beginCaptures": { "1": { "name": "entity.name.function.gdscript" } },
 					"patterns": [
 						{ "include": "#parameters" },
-						{ "include": "#line_continuation" },
-						{
-							"match": "\\s*(\\-\\>)\\s*([a-zA-Z_]\\w*)\\s*\\:",
-							"captures": {
-								"1": { },
-								"2": { "name": "entity.name.type.class.gdscript" }
-							}
-						}
+						{ "include": "#line_continuation" }
 					]
 				}
 			]
 		},
-		"class_definition": {
+		"class_declaration": {
 			"match": "(?<=^class)\\s+([a-zA-Z_]\\w*)\\s*(?=:)",
 			"captures": {
 				"1": { "name": "entity.name.type.class.gdscript" },
@@ -290,26 +305,18 @@
 			}
 		},
 		"class_enum": {
+			"match": "\\b([A-Z][a-zA-Z_0-9]*)\\.([A-Z_0-9]+)",
 			"captures": {
 				"1": { "name": "entity.name.type.class.gdscript" },
 				"2": { "name": "constant.language.gdscript" }
-			},
-			"match": "\\b([A-Z][a-zA-Z_0-9]*)\\.([A-Z_0-9]+)"
+			}
 		},
 		"class_name": {
+			"match": "(?<=class_name)\\s+([a-zA-Z_]\\w*(\\.([a-zA-Z_]\\w*))?)",
 			"captures": {
 				"1": { "name": "entity.name.type.class.gdscript" },
 				"2": { "name": "class.other.gdscript" }
-			},
-			"match": "(?<=class_name)\\s+([a-zA-Z_]\\w*(\\.([a-zA-Z_]\\w*))?)"
-		},
-		"extends": {
-			"match": "(?<=extends)\\s+[a-zA-Z_]\\w*(\\.([a-zA-Z_]\\w*))?",
-			"name": "entity.other.inherited-class.gdscript"
-		},
-		"builtin_func": {
-			"match": "(?<![^.]\\.|:)\\b(abs|absf|absi|acos|asin|assert|atan|atan2|bytes2var|bytes2var_with_objects|ceil|char|clamp|clampf|clampi|Color8|convert|cos|cosh|cubic_interpolate|db2linear|decimals|dectime|deg2rad|dict2inst|ease|error_string|exp|floor|fmod|fposmod|funcref|get_stack|hash|inst2dict|instance_from_id|inverse_lerp|is_equal_approx|is_inf|is_instance_id_valid|is_instance_valid|is_nan|is_zero_approx|len|lerp|lerp_angle|linear2db|load|log|max|maxf|maxi|min|minf|mini|move_toward|nearest_po2|pingpong|posmod|pow|preload|print|printerr|printraw|prints|printt|print_debug|print_stack|print_verbose|push_error|push_warning|rad2deg|randf|randfn|randf_range|randi|randi_range|randomize|rand_from_seed|rand_range|rand_seed|range|range_lerp|range_step_decimals|rid_allocate_id|rid_from_int64|round|seed|sign|signf|signi|sin|sinh|smoothstep|snapped|sqrt|stepify|step_decimals|str|str2var|tan|tanh|typeof|type_exists|var2bytes|var2bytes_with_objects|var2str|weakref|wrapf|wrapi|yield)\\b(?=(\\()([^)]*)(\\)))",
-			"name": "support.function.builtin.gdscript"
+			}
 		},
 		"builtin_get_node_shorthand": {
 			"patterns": [
@@ -318,7 +325,7 @@
 			]
 		},
 		"builtin_get_node_shorthand_quoted": {
-			"name": "meta.literal.nodepath.gdscript constant.character.escape.gdscript",
+			"name": "string.quoted.gdscript meta.literal.nodepath.gdscript constant.character.escape.gdscript",
 			"begin": "(?:(\\$)|(&|\\^|@))(\"|')",
 			"beginCaptures": {
 				"1": { "name": "keyword.control.flow.gdscript" },
@@ -339,10 +346,10 @@
 				"1": { "name": "keyword.control.flow.gdscript" },
 				"2": { "name": "constant.character.escape.gdscript" }
 			},
-			"end": "[^\\w%]",
+			"end": "(?!%?\\s*[a-zA-Z_]\\w*)\\s*/?*",
 			"patterns": [
 				{
-					"match": "(%)?([a-zA-Z_]\\w*/?)",
+					"match": "(%)?\\s*([a-zA-Z_]\\w*)\\s*/?",
 					"captures": {
 						"1": { "name": "keyword.control.flow.gdscript" },
 						"2": { "name": "constant.character.escape.gdscript" }
@@ -370,6 +377,7 @@
 			"name": "entity.name.type.class.gdscript"
 		},
 		"signal_declaration_bare": {
+			"name": "meta.signal.gdscript",
 			"match": "(?x) \\s*\n (signal) \\s+\n ([a-zA-Z_]\\w*)(?=[\\n\\s])",
 			"captures": {
 				"1": { "name": "keyword.language.gdscript storage.type.function.gdscript" },
@@ -386,34 +394,26 @@
 			},
 			"patterns": [
 				{ "include": "#parameters" },
-				{ "include": "#line_continuation" },
-				{
-					"match": "\\s*(\\-\\>)\\s*([a-zA-Z_]\\w*)\\s*\\:",
-					"captures": {
-						"1": { },
-						"2": { "name": "entity.name.type.class.gdscript" }
-					}
-				}
+				{ "include": "#line_continuation" }
 			]
 		},
 		"lambda_declaration": {
 			"name": "meta.function.gdscript",
 			"begin": "(func)\\s?(?=\\()",
-			"end": "(:|(?=[#'\"\\n]))",
 			"beginCaptures": {
 				"1": { "name": "keyword.language.gdscript storage.type.function.gdscript" },
 				"2": { "name": "entity.name.function.gdscript" }
 			},
+			"end": "(:|(?=[#'\"\\n]))",
+			"end2": "(\\s*(\\-\\>)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:)",
+			"endCaptures2": {
+				"1": { "name": "punctuation.separator.annotation.result.gdscript" },
+				"2": { "name": "keyword.language.void.gdscript" },
+				"3": { "name": "entity.name.type.class.gdscript markup.italic" }
+			},
 			"patterns": [
 				{ "include": "#parameters" },
 				{ "include": "#line_continuation" },
-				{
-					"match": "\\s*(?:\\-\\>)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:",
-					"captures": {
-						"1": { "name": "keyword.language.void.gdscript" },
-						"2": { "name": "entity.name.type.class.gdscript" }
-					}
-				},
 				{ "include": "#base_expression" },
 				{ "include": "#any_variable" },
 				{ "include": "#any_property" }
@@ -422,29 +422,23 @@
 		"function_declaration": {
 			"name": "meta.function.gdscript",
 			"begin": "(?x) \\s*\n (func) \\s+\n ([a-zA-Z_]\\w*) \\s*\n (?=\\()",
-			"end": "((:)|(?=[#'\"\\n]))",
 			"beginCaptures": {
 				"1": { "name": "keyword.language.gdscript storage.type.function.gdscript" },
 				"2": { "name": "entity.name.function.gdscript" }
 			},
-			"endCaptures": { "1": { "name": "punctuation.section.function.begin.gdscript" } },
+			"end": "(:|(?=[#'\"\\n]))",
+			"end2": "(\\s*(\\-\\>)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:)",
+			"endCaptures2": {
+				"1": { "name": "punctuation.separator.annotation.result.gdscript" },
+				"2": { "name": "keyword.language.void.gdscript" },
+				"3": { "name": "entity.name.type.class.gdscript markup.italic" }
+			},
 			"patterns": [
 				{ "include": "#parameters" },
 				{ "include": "#line_continuation" },
-				{
-					"match": "\\s*(?:\\-\\>)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:",
-					"captures": {
-						"1": { "name": "keyword.language.void.gdscript" },
-						"2": { "name": "entity.name.type.class.gdscript" }
-					}
-				},
 				{ "include": "#base_expression" }
 			]
 		},
-		"function_keyword": {
-			"match": "func",
-			"name": "keyword.language.gdscript"
-		},
 		"parameters": {
 			"name": "meta.function.parameters.gdscript",
 			"begin": "(\\()",
@@ -472,12 +466,13 @@
 			"patterns": [ { "include": "#base_expression" } ]
 		},
 		"annotated_parameter": {
-			"begin": "(?x)\n  \\b\n  ([a-zA-Z_]\\w*) \\s* (:)\n",
-			"end": "(,)|(?=\\))",
+			"begin": "(?x)\n \\s* ([a-zA-Z_]\\w*) \\s* (:)\\s* ([a-zA-Z_]\\w*)? \n",
 			"beginCaptures": {
 				"1": { "name": "variable.parameter.function.language.gdscript" },
-				"2": { "name": "punctuation.separator.annotation.gdscript" }
+				"2": { "name": "punctuation.separator.annotation.gdscript" },
+				"3": { "name": "entity.name.type.class.builtin.gdscript" }
 			},
+			"end": "(,)|(?=\\))",
 			"endCaptures": { "1": { "name": "punctuation.separator.parameters.gdscript" } },
 			"patterns": [
 				{ "include": "#base_expression" },
@@ -523,16 +518,18 @@
 			"name": "variable.other.gdscript"
 		},
 		"any_property": {
-			"match": "\\b(\\.)\\s*(?<![@\\$#%])([A-Za-z_]\\w*)\\b(?![(])",
+			"match": "\\b(\\.)\\s*(?<![@\\$#%])(?:(\\b[A-Z_][A-Z_0-9]*\\b)|([A-Za-z_]\\w*))\\b(?![(])",
 			"captures": {
 				"1": { "name": "punctuation.accessor.gdscript" },
-				"2": { "name": "variable.other.property.gdscript" }
+				"2": { "name": "constant.language.gdscript" },
+				"3": { "name": "variable.other.property.gdscript" }
 			}
 		},
 		"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" } },
 			"end": "(\\))",
 			"endCaptures": { "1": { "name": "punctuation.definition.arguments.end.gdscript" } },
 			"patterns": [
@@ -542,7 +539,6 @@
 		},
 		"function_name": {
 			"patterns": [
-				{ "include": "#builtin_func" },
 				{ "include": "#builtin_classes" },
 				{
 					"comment": "Some color schemas support meta.function-call.generic scope",

+ 9 - 0
syntaxes/examples/gdscript2.gd

@@ -29,6 +29,15 @@ func remote_function_a():
 func remote_function_b():
 	pass
 
+signal sig_a
+signal sig_b()
+signal sig_c(param1, param2)
+signal sig_d(param1: int, param2: Dictionary)
+signal sig_e(
+		param1: int, # first param
+		param2: Dictionary,
+	)
+
 # ------------------------------------------------------------------------------
 
 func f():