Skip to content

Commit 9ad80ca

Browse files
Talon formatter now supports line width argument
1 parent 7e6e58c commit 9ad80ca

8 files changed

Lines changed: 87 additions & 45 deletions

File tree

eslint.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default defineConfig(
2424
"warn",
2525
{
2626
argsIgnorePattern: "^_",
27+
varsIgnorePattern: "^_",
2728
caughtErrorsIgnorePattern: "^_",
2829
},
2930
],

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"build": "tsc -p . && tsx ./src/build.ts",
3636
"clean": "rm -rf out",
3737
"lint": "npm run lint:ts &&npm run lint:fmt",
38-
"lint:ts": "eslint src",
38+
"lint:ts": "tsc -p . --noEmit && eslint src",
3939
"lint:fmt": "prettier --check .",
4040
"fix": "npm run fix:ts && npm run fix:fmt",
4141
"fix:ts": "eslint src --fix",

src/cli/talonFormatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ void main({
99
binName: "talon-fmt",
1010
fileEndings: ["talon", "talon-list"],
1111
supportedFlagArgs: ["--indent-tabs"],
12-
supportedValueArgs: ["--indent-width", "--column-width"],
12+
supportedValueArgs: ["--indent-width", "--line-width", "--column-width"],
1313

1414
format: async (text, options, fileName) => {
1515
if (isListFile(text, fileName)) {

src/lib/talonFormatter.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import type { Node } from "web-tree-sitter";
22
import { getColumnWidth } from "../util/getColumnWidth.js";
33
import { getIndentation } from "../util/getIndentation.js";
4+
import { DEFAULT_LINE_WIDTH } from "../util/constants.js";
45

56
interface Options {
67
readonly indentTabs?: boolean;
78
readonly indentWidth?: number;
9+
readonly lineWidth?: number;
810
readonly columnWidth?: number;
911
}
1012

1113
export function talonFormatter(node: Node, options: Options = {}): string {
1214
const columnWidth = getColumnWidth(node.text) ?? options.columnWidth;
1315
const indentation = getIndentation(options.indentTabs, options.indentWidth);
14-
const formatter = new TalonFormatter(indentation, columnWidth);
16+
const formatter = new TalonFormatter(
17+
indentation,
18+
options.lineWidth ?? DEFAULT_LINE_WIDTH,
19+
columnWidth,
20+
);
1521
return formatter.getText(node);
1622
}
1723

@@ -20,6 +26,7 @@ class TalonFormatter {
2026

2127
constructor(
2228
private indent: string,
29+
private lineWidth: number,
2330
private columnWidth: number | undefined,
2431
) {}
2532

@@ -28,26 +35,26 @@ class TalonFormatter {
2835
}
2936

3037
private getLeftRightText(node: Node): string {
31-
const { children } = node;
32-
const isMultiline =
33-
children[2].startPosition.row > children[1].endPosition.row;
34-
const left = this.getNodeText(children[0]);
35-
const leftWithColon = `${left}:`;
36-
const leftWithPadding = (() => {
37-
if (isMultiline) {
38-
return leftWithColon;
39-
}
40-
if (this.columnWidth == null) {
41-
return `${leftWithColon} `;
38+
const [leftNode, _colonNode, ...rightNodes] = node.children;
39+
const left = this.getNodeText(leftNode);
40+
41+
if (rightNodes.length === 1) {
42+
if (isLeftRightSingleLine(leftNode, rightNodes)) {
43+
const right = this.getNodeText(rightNodes[0]);
44+
const leftWithPadding =
45+
this.columnWidth != null
46+
? `${left}: `.padEnd(this.columnWidth)
47+
: `${left}: `;
48+
if (leftWithPadding.length + right.length <= this.lineWidth) {
49+
return leftWithPadding + right;
50+
}
4251
}
43-
return `${leftWithColon} `.padEnd(this.columnWidth);
44-
})();
45-
const nl = isMultiline ? "\n" : "";
46-
const right = children
47-
.slice(2)
48-
.map((n) => this.getNodeText(n, isMultiline))
52+
}
53+
54+
const right = rightNodes
55+
.map((n) => this.getNodeText(n, true))
4956
.join("\n");
50-
return `${leftWithPadding}${nl}${right}`;
57+
return `${left}:\n${right}`;
5158
}
5259

5360
private getNodeText(node: Node, isIndented = false): string {
@@ -185,3 +192,7 @@ class TalonFormatter {
185192
}
186193
}
187194
}
195+
196+
function isLeftRightSingleLine(left: Node, rights: Node[]): boolean {
197+
return left.endPosition.row === rights[rights.length - 1].startPosition.row;
198+
}

src/test/cli.test.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { parseArgs } from "../util/parseArgs.js";
1111
import { printHelp } from "../util/printHelp.js";
1212

1313
suite("CLI", () => {
14-
test("formats a file in place", async () => {
14+
test("Formats a file in place", async () => {
1515
const fileName = await createTempFile(
1616
"talonfmt-",
1717
"example.txt",
@@ -30,7 +30,7 @@ suite("CLI", () => {
3030
}
3131
});
3232

33-
test("reports changes without writing in check mode", async () => {
33+
test("Reports changes without writing in check mode", async () => {
3434
const fileName = await createTempFile(
3535
"talonfmt-",
3636
"example.txt",
@@ -49,7 +49,7 @@ suite("CLI", () => {
4949
}
5050
});
5151

52-
test("counts only changed files", async () => {
52+
test("Counts only changed files", async () => {
5353
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "talonfmt-"));
5454
const unchangedFileName = path.join(directory, "unchanged.txt");
5555
const changedFileName = path.join(directory, "changed.txt");
@@ -81,7 +81,7 @@ suite("CLI", () => {
8181
}
8282
});
8383

84-
test("ignores missing files", async () => {
84+
test("Ignores missing files", async () => {
8585
const fileName = path.join(os.tmpdir(), "talonfmt-missing.txt");
8686
const cli = createCLI((text) => `${text} updated`);
8787

@@ -90,7 +90,7 @@ suite("CLI", () => {
9090
assert.equal(didChange, false);
9191
});
9292

93-
test("wraps formatter errors", async () => {
93+
test("Wraps formatter errors", async () => {
9494
const fileName = await createTempFile(
9595
"talonfmt-",
9696
"example.txt",
@@ -110,7 +110,7 @@ suite("CLI", () => {
110110
}
111111
});
112112

113-
test("writes formatted stdin to stdout", async () => {
113+
test("Writes formatted stdin to stdout", async () => {
114114
const cli = createCLI((text) => `${text} updated`);
115115
const output = await captureStreamWrite(process.stdout, async () =>
116116
readAndFormatStdin(cli, "content"),
@@ -120,7 +120,7 @@ suite("CLI", () => {
120120
assert.equal(output.text, "content updated");
121121
});
122122

123-
test("reports stdin formatting issues to stderr in check mode", async () => {
123+
test("Reports stdin formatting issues to stderr in check mode", async () => {
124124
const cli = createCLI((text) => `${text} updated`);
125125
const output = await captureStreamWrite(process.stderr, async () =>
126126
readAndFormatStdin(cli, "content", true),
@@ -130,7 +130,7 @@ suite("CLI", () => {
130130
assert.equal(output.text, "[warn] Code style issues found in stdin.");
131131
});
132132

133-
test("returns success for unchanged stdin in check mode", async () => {
133+
test("Returns success for unchanged stdin in check mode", async () => {
134134
const cli = createCLI((text) => text);
135135
const stderr = await captureStreamWrite(process.stderr, async () =>
136136
readAndFormatStdin(cli, "content", true),
@@ -145,7 +145,7 @@ suite("CLI", () => {
145145
assert.equal(stdout.text, "");
146146
});
147147

148-
test("passes options and file name to file formatter", async () => {
148+
test("Passes options and file name to file formatter", async () => {
149149
const fileName = await createTempFile(
150150
"talonfmt-",
151151
"example.txt",
@@ -183,7 +183,7 @@ suite("CLI", () => {
183183
}
184184
});
185185

186-
test("passes options and stdin file name to stdin formatter", async () => {
186+
test("Passes options and stdin file name to stdin formatter", async () => {
187187
const options = {
188188
indentTabs: true,
189189
indentWidth: 2,
@@ -212,7 +212,7 @@ suite("CLI", () => {
212212
assert.equal(actualFileName, "stdin");
213213
});
214214

215-
test("parses check mode", () => {
215+
test("Parses check mode", () => {
216216
const expected = getArguments({
217217
filePatterns: ["a.txt", "b.txt"],
218218
check: true,
@@ -225,7 +225,7 @@ suite("CLI", () => {
225225
assert.deepEqual(actual, expected);
226226
});
227227

228-
test("parses check mode and end-of-options marker", () => {
228+
test("Parses check mode and end-of-options marker", () => {
229229
const expected = getArguments({
230230
filePatterns: ["--check"],
231231
check: true,
@@ -238,15 +238,15 @@ suite("CLI", () => {
238238
assert.deepEqual(actual, expected);
239239
});
240240

241-
test("parses tabs and width arguments", () => {
241+
test("Parses tabs and width arguments", () => {
242242
const expected = getArguments({
243243
filePatterns: ["a.txt"],
244244
help: false,
245245
version: false,
246246
check: false,
247247
indentTabs: true,
248248
indentWidth: 2,
249-
lineWidth: undefined,
249+
lineWidth: 40,
250250
columnWidth: 24,
251251
});
252252
const actual = parseArgs(
@@ -255,6 +255,8 @@ suite("CLI", () => {
255255
"--indent-tabs",
256256
"--indent-width",
257257
"2",
258+
"--line-width",
259+
"40",
258260
"--column-width",
259261
"24",
260262
"a.txt",
@@ -264,7 +266,7 @@ suite("CLI", () => {
264266
assert.deepEqual(actual, expected);
265267
});
266268

267-
test("rejects unsupported formatter arguments", () => {
269+
test("Rejects unsupported formatter arguments", () => {
268270
const snippetCli: CLI = {
269271
...createCLI(() => ""),
270272
binName: "snippet-fmt",
@@ -278,7 +280,7 @@ suite("CLI", () => {
278280
);
279281
});
280282

281-
test("rejects unsupported formatter flags", () => {
283+
test("Rejects unsupported formatter flags", () => {
282284
const snippetCli: CLI = {
283285
...createCLI(() => ""),
284286
binName: "snippet-fmt",
@@ -291,7 +293,7 @@ suite("CLI", () => {
291293
);
292294
});
293295

294-
test("parses only supported arguments for current cli", () => {
296+
test("Parses only supported arguments for current cli", () => {
295297
const expected = getArguments({
296298
filePatterns: ["a.txt"],
297299
indentTabs: true,
@@ -313,7 +315,7 @@ suite("CLI", () => {
313315
assert.deepEqual(actual, expected);
314316
});
315317

316-
test("prints help only for supported arguments", async () => {
318+
test("Prints help only for supported arguments", async () => {
317319
const cli: CLI = {
318320
binName: "tree-sitter-fmt",
319321
fileEndings: ["scm"],
@@ -345,7 +347,7 @@ suite("CLI", () => {
345347
);
346348
});
347349

348-
test("rejects unknown arguments", () => {
350+
test("Rejects unknown arguments", () => {
349351
assert.throws(
350352
() =>
351353
parseArgs(
@@ -356,7 +358,7 @@ suite("CLI", () => {
356358
);
357359
});
358360

359-
test("rejects missing width values", () => {
361+
test("Rejects missing width values", () => {
360362
assert.throws(
361363
() =>
362364
parseArgs(
@@ -367,7 +369,7 @@ suite("CLI", () => {
367369
);
368370
});
369371

370-
test("rejects invalid width values", () => {
372+
test("Rejects invalid width values", () => {
371373
assert.throws(
372374
() =>
373375
parseArgs(
@@ -399,7 +401,11 @@ function createCLI(format: (text: string) => string | Promise<string>): CLI {
399401
binName: "talon-fmt" as const,
400402
fileEndings: ["txt"],
401403
supportedFlagArgs: ["--indent-tabs"],
402-
supportedValueArgs: ["--indent-width", "--column-width"],
404+
supportedValueArgs: [
405+
"--indent-width",
406+
"--line-width",
407+
"--column-width",
408+
],
403409
format: (text: string) => Promise.resolve(format(text)),
404410
};
405411
}

src/test/talonFormatter.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ suite("Talon formatter", () => {
186186
});
187187
}
188188

189-
test("uses tabs for indented command blocks", async () => {
189+
test("Uses tabs for indented command blocks", async () => {
190190
const rootNode = await parseText(
191191
"foo:\n edit.left()",
192192
"tree-sitter-talon",
@@ -198,6 +198,25 @@ suite("Talon formatter", () => {
198198

199199
assert.equal(actual, "foo:\n\tedit.left()\n");
200200
});
201+
202+
test("Breaks long inline declarations when column width is unset", async () => {
203+
const rootNode = await parseText("aaa: bbb", "tree-sitter-talon");
204+
205+
const actual = talonFormatter(rootNode, {
206+
lineWidth: 7,
207+
});
208+
209+
assert.equal(actual, "aaa:\n bbb\n");
210+
});
211+
212+
test("Breaks long inline declarations at the default line width", async () => {
213+
const right = `"${"a".repeat(76)}"`;
214+
const rootNode = await parseText(`foo: ${right}`, "tree-sitter-talon");
215+
216+
const actual = talonFormatter(rootNode);
217+
218+
assert.equal(actual, `foo:\n ${right}\n`);
219+
});
201220
});
202221

203222
function getContentString(content: Content): string {

src/util/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ export const EXIT_OK = 0;
44
export const EXIT_FAIL = 1;
55
// Exit code 2: Unexpected error
66
export const EXIT_ERROR = 2;
7+
8+
export const DEFAULT_INDENT_WIDTH = 4;
9+
export const DEFAULT_LINE_WIDTH = 80;

src/util/getIndentation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { DEFAULT_INDENT_WIDTH } from "./constants.js";
2+
13
export function getIndentation(
24
indentTabs: boolean | undefined,
35
indentWidth: number | undefined,
46
): string {
5-
return indentTabs ? "\t" : " ".repeat(indentWidth ?? 4);
7+
return indentTabs ? "\t" : " ".repeat(indentWidth ?? DEFAULT_INDENT_WIDTH);
68
}

0 commit comments

Comments
 (0)