|
@@ -1,16 +1,16 @@
|
|
|
-import { Range, TextDocument, TextEdit } from "vscode";
|
|
|
-import * as fs from "fs";
|
|
|
+import { Range, type TextDocument, TextEdit, 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 +22,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 +51,16 @@ interface Token {
|
|
|
skip?: boolean;
|
|
|
}
|
|
|
|
|
|
+class FormatterOptions {
|
|
|
+ emptyLinesBeforeFunctions: "one" | "two";
|
|
|
+ denseFunctionDeclarations: boolean;
|
|
|
+
|
|
|
+ constructor() {
|
|
|
+ this.emptyLinesBeforeFunctions = get_configuration("formatter.emptyLinesBeforeFunctions");
|
|
|
+ this.denseFunctionDeclarations = get_configuration("formatter.denseFunctionDeclarations");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function parse_token(token: Token) {
|
|
|
if (token.scopes.includes("string.quoted.gdscript")) {
|
|
|
token.string = true;
|
|
@@ -65,6 +79,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 +97,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 +107,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.denseFunctionDeclarations) {
|
|
|
+ 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 +153,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 "";
|
|
|
}
|
|
|
}
|
|
@@ -134,6 +173,7 @@ function between(tokens: Token[], current: number) {
|
|
|
if (prev === "[" && nextToken.type === "symbol") return "";
|
|
|
if (prev === ":") return " ";
|
|
|
if (prev === ";") return " ";
|
|
|
+ if (prev === "##") return " ";
|
|
|
if (prev === "#") return " ";
|
|
|
if (next === "=") return " ";
|
|
|
if (prev === "=") return " ";
|
|
@@ -157,7 +197,13 @@ 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;
|
|
|
+});
|
|
|
+
|
|
|
+function is_comment(line: TextLine): boolean {
|
|
|
+ return line.text[line.firstNonWhitespaceCharacterIndex] === "#";
|
|
|
+}
|
|
|
|
|
|
export function format_document(document: TextDocument): TextEdit[] {
|
|
|
// quit early if grammar is not loaded
|
|
@@ -166,35 +212,67 @@ export function format_document(document: TextDocument): TextEdit[] {
|
|
|
}
|
|
|
const edits: TextEdit[] = [];
|
|
|
|
|
|
+ const options = new FormatterOptions();
|
|
|
+
|
|
|
let lineTokens: vsctm.ITokenizeLineResult = null;
|
|
|
let onlyEmptyLinesSoFar = true;
|
|
|
+ let emptyLineCount = 0;
|
|
|
+ let firstEmptyLine = 0;
|
|
|
for (let lineNum = 0; lineNum < document.lineCount; lineNum++) {
|
|
|
const line = document.lineAt(lineNum);
|
|
|
|
|
|
// skip empty lines
|
|
|
- if (line.isEmptyOrWhitespace) {
|
|
|
+ if (line.isEmptyOrWhitespace || is_comment(line)) {
|
|
|
// delete empty lines at the beginning of the file
|
|
|
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));
|
|
|
- }
|
|
|
+ if (emptyLineCount === 0) {
|
|
|
+ firstEmptyLine = lineNum;
|
|
|
+ }
|
|
|
+ if (!is_comment(line)) {
|
|
|
+ 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) {
|
|
|
+ let maxEmptyLines = 1;
|
|
|
+
|
|
|
+ const start = line.text.trimStart();
|
|
|
+ if (options.emptyLinesBeforeFunctions === "two") {
|
|
|
+ if (start.startsWith("func") || start.startsWith("static func")) {
|
|
|
+ maxEmptyLines++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (start.startsWith("class")) {
|
|
|
+ maxEmptyLines++;
|
|
|
+ }
|
|
|
+ let i = 0;
|
|
|
+ let deletedLines = 0;
|
|
|
+ const linesToDelete = emptyLineCount - maxEmptyLines;
|
|
|
+ while (i < lineNum && deletedLines < linesToDelete) {
|
|
|
+ const candidate = document.lineAt(firstEmptyLine + i++);
|
|
|
+ if (candidate.isEmptyOrWhitespace) {
|
|
|
+ edits.push(TextEdit.delete(candidate.rangeIncludingLineBreak));
|
|
|
+ deletedLines++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ emptyLineCount = 0;
|
|
|
+ }
|
|
|
+
|
|
|
// skip comments
|
|
|
- if (line.text[line.firstNonWhitespaceCharacterIndex] === "#") {
|
|
|
+ if (is_comment(line)) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
@@ -228,7 +306,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();
|
|
|
}
|
|
|
}
|
|
|
|