diff --git a/.vscode/test_files.code-snippets b/.vscode/test_files.code-snippets new file mode 100644 index 0000000..9b73cff --- /dev/null +++ b/.vscode/test_files.code-snippets @@ -0,0 +1,48 @@ +{ + "# --- IN ---": { + "scope": "gdscript", + "prefix": "#IN", + "body": [ + "# --- IN ---" + ], + "description": "Snapshot Test #IN block" + }, + "# --- OUT ---": { + "scope": "gdscript", + "prefix": "#OUT", + "body": [ + "# --- OUT ---" + ], + "description": "Snapshot Test #OUT block" + }, + "# --- END ---": { + "scope": "gdscript", + "prefix": "#END", + "body": [ + "# --- END ---" + ], + "description": "Snapshot Test #END block" + }, + "# --- CONFIG ---": { + "scope": "gdscript", + "prefix": [ + "#CO", + "#CONFIG" + ], + "body": [ + "# --- CONFIG ---" + ], + "description": "Snapshot Test #CONFIG block" + }, + "# --- CONFIG ALL ---": { + "scope": "gdscript", + "prefix": [ + "#CA", + "#CONFIG ALL" + ], + "body": [ + "# --- CONFIG ALL ---" + ], + "description": "Snapshot Test #CONFIG ALL block" + }, +} \ No newline at end of file diff --git a/src/formatter/formatter.test.ts b/src/formatter/formatter.test.ts index 342b70a..12087c6 100644 --- a/src/formatter/formatter.test.ts +++ b/src/formatter/formatter.test.ts @@ -1,61 +1,195 @@ -import * as vscode from "vscode"; -import * as path from "node:path"; import * as fs from "node:fs"; +import * as path from "node:path"; +import * as vscode from "vscode"; import { format_document, type FormatterOptions } from "./textmate"; -import * as chai from "chai"; -const expect = chai.expect; +import { expect } from "chai"; const dots = ["..", "..", ".."]; const basePath = path.join(__filename, ...dots); +const snapshotsFolderPath = path.join(basePath, "src/formatter/snapshots"); -function get_options(testFolderPath: string) { - const options: FormatterOptions = { - maxEmptyLines: 2, - denseFunctionParameters: false, - }; - const optionsPath = path.join(testFolderPath, "config.json"); +function normalizeLineEndings(str: string) { + return str.replace(/\r?\n/g, "\n"); +} + +const defaultOptions: FormatterOptions = { + maxEmptyLines: 2, + denseFunctionParameters: false, +}; + +function get_options(folder: fs.Dirent) { + const optionsPath = path.join(folder.path, folder.name, "config.json"); if (fs.existsSync(optionsPath)) { const file = fs.readFileSync(optionsPath).toString(); const config = JSON.parse(file); - return { ...options, ...config } as FormatterOptions; + return { ...defaultOptions, ...config } as FormatterOptions; } - return options; + return defaultOptions; +} + +function set_content(content: string) { + return vscode.workspace + .openTextDocument() + .then((doc) => vscode.window.showTextDocument(doc)) + .then((editor) => { + const editBuilder = (textEdit) => { + textEdit.insert(new vscode.Position(0, 0), String(content)); + }; + + return editor + .edit(editBuilder, { + undoStopBefore: true, + undoStopAfter: false, + }) + .then(() => editor); + }); +} + +function build_config(lines: string[]) { + try { + return JSON.parse(lines.join("\n")); + } catch (e) { + return {}; + } +} + +class TestLines { + config: string[] = []; + in: string[] = []; + out: string[] = []; + + parse(_config) { + const config = { ...defaultOptions, ..._config, ...build_config(this.config) }; + + const test: Test = { + in: this.in.join("\n"), + out: this.out.join("\n"), + config: config, + }; + + if (test.out === "") { + test.out = this.in.join("\n"); + } + + if (!config.strictTrailingNewlines) { + test.in = test.in.trimEnd(); + test.out = test.out.trimEnd(); + } + return test; + } +} + +interface Test { + config?: FormatterOptions; + in: string; + out: string; +} + +const CONFIG_ALL = "# --- CONFIG ALL ---"; +const CONFIG = "# --- CONFIG ---"; +const IN = "# --- IN ---"; +const OUT = "# --- OUT ---"; +const END = "# --- END ---"; + +const MODES = [CONFIG_ALL, CONFIG, IN, OUT, END]; + +function parse_test_file(content: string): Test[] { + let defaultConfig = null; + let defaultConfigString: string[] = []; + + const tests: Test[] = []; + let mode = null; + let test = new TestLines(); + + for (const _line of content.split("\n")) { + const line = _line.trim(); + + if (MODES.includes(line)) { + if (line === CONFIG || line === IN) { + if (test.in.length !== 0) { + tests.push(test.parse(defaultConfig)); + test = new TestLines(); + } + } + + if (defaultConfigString.length !== 0) { + defaultConfig = build_config(defaultConfigString); + defaultConfigString = []; + } + mode = line; + continue; + } + + if (mode === CONFIG_ALL) defaultConfigString.push(line); + if (mode === CONFIG) test.config.push(line); + if (mode === IN) test.in.push(line); + if (mode === OUT) test.out.push(line); + } + + if (test.in.length !== 0) { + tests.push(test.parse(defaultConfig)); + } + + return tests; } suite("GDScript Formatter Tests", () => { - // Search for all folders in the snapshots folder and run a test for each - // comparing the output of the formatter with the expected output. - // To add a new test, create a new folder in the snapshots folder - // and add two files, `in.gd` and `out.gd` for the input and expected output. - const snapshotsFolderPath = path.join(basePath, "src/formatter/snapshots"); - const testFolders = fs.readdirSync(snapshotsFolderPath); + const testFiles = fs.readdirSync(snapshotsFolderPath, { withFileTypes: true, recursive: true }); - // biome-ignore lint/complexity/noForEach: - 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")); + for (const file of testFiles.filter((f) => f.isFile())) { + if (["in.gd", "out.gd"].includes(file.name) || !file.name.endsWith(".gd")) { + continue; + } + test(`Snapshot Test: ${file.name}`, async () => { + const uri = vscode.Uri.file(path.join(snapshotsFolderPath, file.name)); + const inDoc = await vscode.workspace.openTextDocument(uri); + const text = inDoc.getText(); - const documentIn = await vscode.workspace.openTextDocument(uriIn); - const documentOut = await vscode.workspace.openTextDocument(uriOut); + for (const test of parse_test_file(text)) { + const editor = await set_content(test.in); + const document = editor.document; - const options = get_options(testFolderPath); - const edits = format_document(documentIn, options); + const edits = format_document(document, test.config); // Apply the formatting edits const workspaceEdit = new vscode.WorkspaceEdit(); - workspaceEdit.set(uriIn, edits); + workspaceEdit.set(document.uri, edits); await vscode.workspace.applyEdit(workspaceEdit); - // Compare the result with the expected output - expect(documentIn.getText().replace("\r\n", "\n")).to.equal( - documentOut.getText().replace("\r\n", "\n"), - ); - }); + const actual = normalizeLineEndings(document.getText()); + const expected = normalizeLineEndings(test.out); + expect(actual).to.equal(expected); + } + }); + } + + for (const folder of testFiles.filter((f) => f.isDirectory())) { + const pathIn = path.join(folder.path, folder.name, "in.gd"); + const pathOut = path.join(folder.path, folder.name, "out.gd"); + if (!(fs.existsSync(pathIn) && fs.existsSync(pathOut))) { + continue; } - }); + test(`Snapshot Pair Test: ${folder.name}`, async () => { + const uriIn = vscode.Uri.file(path.join(folder.path, folder.name, "in.gd")); + const uriOut = vscode.Uri.file(path.join(folder.path, folder.name, "out.gd")); + + const documentIn = await vscode.workspace.openTextDocument(uriIn); + const documentOut = await vscode.workspace.openTextDocument(uriOut); + + const options = get_options(folder); + const edits = format_document(documentIn, options); + + // Apply the formatting edits + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(uriIn, edits); + await vscode.workspace.applyEdit(workspaceEdit); + + // Compare the result with the expected output + const actual = normalizeLineEndings(documentIn.getText()); + const expected = normalizeLineEndings(documentOut.getText()); + expect(actual).to.equal(expected); + }); + } }); diff --git a/src/formatter/snapshots/README.md b/src/formatter/snapshots/README.md new file mode 100644 index 0000000..afcb913 --- /dev/null +++ b/src/formatter/snapshots/README.md @@ -0,0 +1,101 @@ + +## An `IN` block is fed into the formatter and the output is compared to the `OUT` block + +``` +# --- IN --- +var a = 10 +# --- OUT --- +var a = 10 +``` + +## Trailing newlines in `IN` and `OUT` blocks is automatically removed + +``` +# --- IN --- +var a = 10 +# --- OUT --- +var a = 10 + +# --- IN --- +var b = 'ten' +# --- OUT --- +var b = 'ten' +``` + +## An `IN` block by itself will be reused at the `OUT` target + +Many test cases can simply be expressed as "do not change this": + +``` +# --- IN --- +var a = """ { + level_file: '%s', + md5_hash: %s, +} +""" +``` + +## Formatter and test harness options can be controlled with `CONFIG` blocks + +This test will fail because `strictTrailingNewlines: true` disables trailing newline removal. + +``` +# --- CONFIG --- +{"strictTrailingNewlines": true} +# --- IN --- +var a = 10 +# --- OUT --- +var a = 10 + +``` + +## `CONFIG ALL` set the default options moving forward, and `END` blocks allow additional layout flexibility + +``` +# --- CONFIG ALL --- +{"strictTrailingNewlines": true} + +# --- IN --- +var a = 10 +# --- OUT --- +var a = 10 +# --- END --- + +# anything I want goes here + +# --- IN --- +var b = 'ten' +# --- OUT --- +var b = 'ten' +``` + +## `CONFIG` blocks override `CONFIG ALL`, and the configs are merged for a given test + +This test will pass, because the second test has a `CONFIG` that overrides the `CONFIG ALL` at the top. + +``` +# --- CONFIG ALL --- +{"strictTrailingNewlines": true} + +# --- IN --- +var a = 10 +# --- OUT --- +var a = 10 +# --- END --- + +# anything I want goes here + +# --- CONFIG --- +{"strictTrailingNewlines": false} +# --- IN --- +var b = 'ten' +# --- OUT --- +var b = 'ten' + + + +# --- IN --- +var c = true +# --- OUT --- +var c = true +``` \ No newline at end of file diff --git a/src/formatter/snapshots/assignment-operators/out.gd b/src/formatter/snapshots/assignment-operators/out.gd deleted file mode 100644 index b237476..0000000 --- a/src/formatter/snapshots/assignment-operators/out.gd +++ /dev/null @@ -1,15 +0,0 @@ -func f(): - # arithmetic - x += 1 - x -= 1 - x *= 1 - x /= 1 - x %= 1 - - # bitwise - x |= 1 - x &= 1 - x ~= 1 - x /= 1 - x >>= 1 - x <<= 1 diff --git a/src/formatter/snapshots/bitwise-operators/in.gd b/src/formatter/snapshots/bitwise-operators/in.gd deleted file mode 100644 index 9d53e57..0000000 --- a/src/formatter/snapshots/bitwise-operators/in.gd +++ /dev/null @@ -1,5 +0,0 @@ -func f(): - collision_mask = 1 << 1 | 1 << 3 - collision_mask = 1 << 1 & 1 << 3 - collision_mask = ~1 - collision_mask = 1 ^ ~ 1 diff --git a/src/formatter/snapshots/bitwise-operators/out.gd b/src/formatter/snapshots/bitwise-operators/out.gd deleted file mode 100644 index 401cf8a..0000000 --- a/src/formatter/snapshots/bitwise-operators/out.gd +++ /dev/null @@ -1,5 +0,0 @@ -func f(): - collision_mask = 1 << 1 | 1 << 3 - collision_mask = 1 << 1 & 1 << 3 - collision_mask = ~1 - collision_mask = 1 ^ ~1 diff --git a/src/formatter/snapshots/double_dot_notation.gd b/src/formatter/snapshots/double_dot_notation.gd new file mode 100644 index 0000000..0a22fa5 --- /dev/null +++ b/src/formatter/snapshots/double_dot_notation.gd @@ -0,0 +1,5 @@ +# --- IN --- +func handleDeath() -> void: + var signalConnections: Array[Dictionary] = self.get_incoming_connections() + for connection in signalConnections: + connection.signal.disconnect(connection.callable) \ No newline at end of file diff --git a/src/formatter/snapshots/lambda_multiline_spacing.gd b/src/formatter/snapshots/lambda_multiline_spacing.gd new file mode 100644 index 0000000..5734480 --- /dev/null +++ b/src/formatter/snapshots/lambda_multiline_spacing.gd @@ -0,0 +1,4 @@ +# --- IN --- +poll_animation.on_complete(func() -> void: + highlight.visible = false +) \ No newline at end of file diff --git a/src/formatter/snapshots/leave_strings_alone.gd b/src/formatter/snapshots/leave_strings_alone.gd new file mode 100644 index 0000000..44b1f6b --- /dev/null +++ b/src/formatter/snapshots/leave_strings_alone.gd @@ -0,0 +1,16 @@ +# --- IN --- +func dump() -> String: + return """ + { + level_file: '%s', + md5_hash: %s, + text: '%s', + level_size: %s, + world_pos: %s, + preview_size: %s, + preview_pos: %s, + preview_texture: %s, + explorer_layer: %s, + connections: %s, + } + """ diff --git a/src/formatter/snapshots/max-empty-lines-1/config.json b/src/formatter/snapshots/max-empty-lines-1/config.json deleted file mode 100644 index e8b343a..0000000 --- a/src/formatter/snapshots/max-empty-lines-1/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "maxEmptyLines": 1 -} \ No newline at end of file diff --git a/src/formatter/snapshots/max-empty-lines-1/in.gd b/src/formatter/snapshots/max-empty-lines-1/in.gd deleted file mode 100644 index d1440c3..0000000 --- a/src/formatter/snapshots/max-empty-lines-1/in.gd +++ /dev/null @@ -1,27 +0,0 @@ - - - -class Test: - - - - - func _ready(): - - - pass - - - -func test(): - - - pass - - - - -# comments -func with_comments(): - - pass diff --git a/src/formatter/snapshots/max-empty-lines-1/out.gd b/src/formatter/snapshots/max-empty-lines-1/out.gd deleted file mode 100644 index cb6fb2b..0000000 --- a/src/formatter/snapshots/max-empty-lines-1/out.gd +++ /dev/null @@ -1,14 +0,0 @@ -class Test: - - func _ready(): - - pass - -func test(): - - pass - -# comments -func with_comments(): - - pass diff --git a/src/formatter/snapshots/max-empty-lines-2/in.gd b/src/formatter/snapshots/max-empty-lines-2/in.gd deleted file mode 100644 index 5937cc2..0000000 --- a/src/formatter/snapshots/max-empty-lines-2/in.gd +++ /dev/null @@ -1,28 +0,0 @@ - - - -class Test: - - - - - func _ready(): - - - pass - - - -func test(): - - - pass - - - - -# comments -func with_comments(): - - - pass diff --git a/src/formatter/snapshots/max-empty-lines-2/out.gd b/src/formatter/snapshots/max-empty-lines-2/out.gd deleted file mode 100644 index 9c8e646..0000000 --- a/src/formatter/snapshots/max-empty-lines-2/out.gd +++ /dev/null @@ -1,20 +0,0 @@ -class Test: - - - func _ready(): - - - pass - - -func test(): - - - pass - - -# comments -func with_comments(): - - - pass diff --git a/src/formatter/snapshots/max_empty_lines.gd b/src/formatter/snapshots/max_empty_lines.gd new file mode 100644 index 0000000..96fa7d4 --- /dev/null +++ b/src/formatter/snapshots/max_empty_lines.gd @@ -0,0 +1,72 @@ +# --- IN --- +func test(): + + pass +# --- OUT --- +func test(): + pass + +# --- IN --- +class Test: + + func _ready(): + + pass +# --- OUT --- +class Test: + func _ready(): + pass + +# --- IN --- +func test(): # with comment + + pass +# --- OUT --- +func test(): # with comment + pass + +# --- IN --- +class Test: # with comment + + func _ready(): # with comment + + pass +# --- OUT --- +class Test: # with comment + func _ready(): # with comment + pass + +# --- CONFIG --- +{"maxEmptyLines": 1} +# --- IN --- +func a(): + pass + + + +func b(): + pass +# --- OUT --- +func a(): + pass + +func b(): + pass + +# --- CONFIG --- +{"maxEmptyLines": 2} +# --- IN --- +func a(): + pass + + + +func b(): + pass +# --- OUT --- +func a(): + pass + + +func b(): + pass diff --git a/src/formatter/snapshots/multiline_leading_minus.gd b/src/formatter/snapshots/multiline_leading_minus.gd new file mode 100644 index 0000000..29f517d --- /dev/null +++ b/src/formatter/snapshots/multiline_leading_minus.gd @@ -0,0 +1,23 @@ +# --- IN --- +var test1 := deg_to_rad( + -90 +) + +# --- IN --- +var test2 := Vector2( + -0.0, + 1.0 +) + +# --- IN --- +var test3 := Vector3( + 0.0, + -0.0, + 0.0 +) + +# --- IN --- +func get_audio_compensation() -> float: + return AudioServer.get_time_since_last_mix() \ + - AudioServer.get_output_latency() \ + + (1 / Engine.get_frames_per_second()) * 2 \ No newline at end of file diff --git a/src/formatter/snapshots/nodepaths/in.gd b/src/formatter/snapshots/nodepaths/in.gd index fa7a6c4..30740d6 100644 --- a/src/formatter/snapshots/nodepaths/in.gd +++ b/src/formatter/snapshots/nodepaths/in.gd @@ -44,6 +44,9 @@ var a = $Child/%Unique/ChildOfUnique var a = %Unique var a = %Unique/Child var a = %Unique/%UniqueChild +var a = %"Unique" +var a = %'Unique/Child' +var a = %'Unique/%UniqueChild' var a = $"%Unique" var a = get_node("%Unique") diff --git a/src/formatter/snapshots/nodepaths/out.gd b/src/formatter/snapshots/nodepaths/out.gd index 076253b..5190b50 100644 --- a/src/formatter/snapshots/nodepaths/out.gd +++ b/src/formatter/snapshots/nodepaths/out.gd @@ -44,6 +44,9 @@ var a = $Child/%Unique/ChildOfUnique var a = %Unique var a = %Unique/Child var a = %Unique/%UniqueChild +var a = %"Unique" +var a = %'Unique/Child' +var a = %'Unique/%UniqueChild' var a = $"%Unique" var a = get_node("%Unique") diff --git a/src/formatter/snapshots/assignment-operators/in.gd b/src/formatter/snapshots/operators.gd similarity index 55% rename from src/formatter/snapshots/assignment-operators/in.gd rename to src/formatter/snapshots/operators.gd index b237476..159354b 100644 --- a/src/formatter/snapshots/assignment-operators/in.gd +++ b/src/formatter/snapshots/operators.gd @@ -1,3 +1,4 @@ +# --- IN --- func f(): # arithmetic x += 1 @@ -5,11 +6,19 @@ func f(): x *= 1 x /= 1 x %= 1 + x = 2 ** 2 + x = 2 * -1 # bitwise x |= 1 x &= 1 + x ^= 1 x ~= 1 + x = ~1 x /= 1 x >>= 1 x <<= 1 + + x = 1 << 1 | 1 >> 3 + x = 1 << 1 & 1 >> 3 + x = 1 ^ ~1 \ No newline at end of file diff --git a/src/formatter/snapshots/scientific_notation.gd b/src/formatter/snapshots/scientific_notation.gd new file mode 100644 index 0000000..f46111f --- /dev/null +++ b/src/formatter/snapshots/scientific_notation.gd @@ -0,0 +1,6 @@ +# --- IN --- +var a = 1e-6 +var b = 4e-09 +var c = 58.1e-10 +var d = 58.1e+10 +var e = 9.732e-06 diff --git a/src/formatter/snapshots/test.gd b/src/formatter/snapshots/test.gd new file mode 100644 index 0000000..96ae0ad --- /dev/null +++ b/src/formatter/snapshots/test.gd @@ -0,0 +1,14 @@ +# --- CONFIG --- +{"strictTrailingNewlines": true} +# --- IN --- +var a = 10 +# --- OUT --- +var a = 10 +# --- END --- + +# anything I want goes here + +# --- IN --- +var b = 'ten' +# --- OUT --- +var b = 'ten' \ No newline at end of file diff --git a/src/formatter/textmate.ts b/src/formatter/textmate.ts index d8b6a2c..0534efe 100644 --- a/src/formatter/textmate.ts +++ b/src/formatter/textmate.ts @@ -4,7 +4,7 @@ import * as fs from "node:fs"; import * as vsctm from "vscode-textmate"; import * as oniguruma from "vscode-oniguruma"; import { keywords, symbols } from "./symbols"; -import { get_configuration, get_extension_uri, createLogger } from "../utils"; +import { get_configuration, get_extension_uri, createLogger, is_debug_mode } from "../utils"; const log = createLogger("formatter.tm"); @@ -50,10 +50,11 @@ interface Token { param?: boolean; string?: boolean; skip?: boolean; + identifier?: boolean; } export interface FormatterOptions { - maxEmptyLines: 1 | 2; + maxEmptyLines: 0 | 1 | 2; denseFunctionParameters: boolean; } @@ -73,6 +74,9 @@ function parse_token(token: Token) { if (token.scopes.includes("meta.function.parameters.gdscript")) { token.param = true; } + if (token.value.match(/[A-Za-z_]\w+/)) { + token.identifier = true; + } if (token.scopes.includes("meta.literal.nodepath.gdscript")) { token.skip = true; token.type = "nodepath"; @@ -111,6 +115,10 @@ function parse_token(token: Token) { token.type = "variable"; return; } + if (token.scopes.includes("comment.line.number-sign.gdscript")) { + token.type = "comment"; + return; + } } function between(tokens: Token[], current: number, options: FormatterOptions) { @@ -128,12 +136,17 @@ function between(tokens: Token[], current: number, options: FormatterOptions) { if (prevToken.skip && nextToken.skip) return ""; if (prev === "(") return ""; + if (prev === ".") { + if (nextToken?.type === "symbol") return " "; + return ""; + } + if (next === ".") return ""; if (nextToken.param) { if (options.denseFunctionParameters) { if (prev === "-") { if (tokens[current - 2]?.value === "=") return ""; - if (["keyword", "symbol"].includes(tokens[current - 2].type)) { + if (["keyword", "symbol"].includes(tokens[current - 2]?.type)) { return ""; } if ([",", "("].includes(tokens[current - 2]?.value)) { @@ -169,7 +182,9 @@ function between(tokens: Token[], current: number, options: FormatterOptions) { if (prev === "@") return ""; if (prev === "-") { - if (["keyword", "symbol"].includes(tokens[current - 2].type)) { + if (nextToken.identifier) return " "; + if (current === 1) return ""; + if (["keyword", "symbol"].includes(tokens[current - 2]?.type)) { return ""; } if ([",", "(", "["].includes(tokens[current - 2]?.value)) { @@ -232,6 +247,7 @@ export function format_document(document: TextDocument, _options?: FormatterOpti const options = _options ?? get_formatter_options(); + let lastToken = null; let lineTokens: vsctm.ITokenizeLineResult = null; let onlyEmptyLinesSoFar = true; let emptyLineCount = 0; @@ -259,7 +275,11 @@ export function format_document(document: TextDocument, _options?: FormatterOpti // delete consecutive empty lines if (emptyLineCount) { - for (let i = emptyLineCount - options.maxEmptyLines; i > 0; i--) { + let maxEmptyLines = options.maxEmptyLines; + if (lastToken === ":") { + maxEmptyLines = 0; + } + for (let i = emptyLineCount - maxEmptyLines; i > 0; i--) { edits.push(TextEdit.delete(document.lineAt(lineNum - i).rangeIncludingLineBreak)); } emptyLineCount = 0; @@ -296,12 +316,19 @@ export function format_document(document: TextDocument, _options?: FormatterOpti tokens.push(token); } for (let i = 0; i < tokens.length; i++) { - log.debug(i, tokens[i].value, tokens[i]); - if (i > 0 && tokens[i - 1].string === true && tokens[i].string === true) { + if (is_debug_mode()) log.debug(i, tokens[i].value, tokens[i]); + + if (i === 0 && tokens[i].string) { + // leading whitespace is already accounted for + nextLine += tokens[i].original.trimStart(); + } else if (i > 0 && tokens[i - 1].string && tokens[i].string) { nextLine += tokens[i].original; } else { nextLine += between(tokens, i, options) + tokens[i].value.trim(); } + if (tokens[i].type !== "comment") { + lastToken = tokens[i].value; + } } edits.push(TextEdit.replace(line.range, nextLine)); diff --git a/syntaxes/GDScript.tmLanguage.json b/syntaxes/GDScript.tmLanguage.json index d48ce89..a830ce2 100644 --- a/syntaxes/GDScript.tmLanguage.json +++ b/syntaxes/GDScript.tmLanguage.json @@ -274,6 +274,11 @@ "match": "[-]?([0-9][0-9_]*e[\\-\\+]?\\[0-9_])", "name": "constant.numeric.float.gdscript" }, + { + "name": "constant.numeric.float.gdscript", + "match": "(?x)\n (?)\\s*(void\\w*)|([a-zA-Z_]\\w*)\\s*\\:)", - "endCaptures2": { - "1": { "name": "punctuation.separator.annotation.result.gdscript" }, - "2": { "name": "keyword.language.void.gdscript" }, - "3": { "name": "entity.name.type.class.gdscript markup.italic" } - }, + "end": "(:)", + "endCaptures": { "1": { "name": "punctuation.section.function.begin.gdscript" } }, "patterns": [ { "include": "#parameters" }, { "include": "#line_continuation" },