Skip to content

Commit 1185265

Browse files
authored
fix: calculates remaining space for options to avoid line breaking (#64)
partially addresses elastic/kibana#257200 ## Summary This PR changes how the remaining space on the line of a command is calculated to decide when to break options into a new line or not. ## Details **Before the fix:** The decision to break command options (e.g. STATS’s BY ip) to a new line used this condition: `options.length > opts.wrap - inp.remaining - cmd.length - 1 - args.txt.length`. For every command, inp.remaining was set to the same value as opts.wrap (e.g. 500), so `opts.wrap - inp.remaining` was always 0. The “remaining width” for options became negative (e.g. 0 − 6 − 1 − 85 = −92), and so `options.length > remaining` was always true (e.g. 5 > −92). As a result, options were always broken to a new line, regardless of wrap. Example: with `{ wrap: 120 and multiline: true }`, a STATS line like `| STATS count = COUNT(*), avg = AVG(bytes), p95 = PERCENTILE(bytes, 95), ext = VALUES(tags.keyword) BY ip` would still be split so that `BY ip` appeared on the next line, even though it would have fit on the same line. **After the fix:** The code now computes how much of the line is already used by the prefix (indent + pipe tab + "| " + command name + space + args) and treats the rest as the width available for options. So `linePrefixLength = indent.length + pipeTab.length + 2 + cmd.length + 1 + args.txt.length`, and `remainingForOptions = opts.wrap - linePrefixLength`. Options are broken only when `options.length > remainingForOptions`. Example: with `{ wrap: 120 }`, a prefix of length 96 leaves 24 characters for options; "BY ip" has length 5, so 5 > 24 is false and BY stays on the same line as STATS. With `{ wrap: 80 }`, the same prefix might leave no room (or negative room), so 5 > remainingForOptions becomes true and BY is correctly broken to a new line. So the new structure represents “space left on the current line for options” and only breaks when the options text would not fit in that space. ### Checklist - [x] Unit tests have been added or updated. - [x] The PR title has the correct [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) label in the title, this is crucial for the correct versioning of this package. Check the following cheat shit. | Title Label | Release | |---|---| | `breaking: true (!)` | `major` | | `feat` | `minor` | | `fix` | `patch` | | `refactor` | `patch` | | `perf` | `patch` | | `build` | `patch` | | `docs` | `patch` | | `chore` | `patch` | | `revert` | `patch` |
1 parent d6b90c7 commit 1185265

3 files changed

Lines changed: 46 additions & 20 deletions

File tree

src/pretty_print/__tests__/wrapping_pretty_printer.comments.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,15 +1024,13 @@ FROM index`;
10241024
/* before subquery */ (/* inside start */ FROM index2 /* after source */
10251025
| WHERE a > 10 /* after where */
10261026
| EVAL b = a * 2
1027-
| STATS cnt = COUNT(*)
1028-
BY c
1027+
| STATS cnt = COUNT(*) BY c
10291028
| SORT cnt DESC
10301029
| LIMIT 10) /* after first subquery */,
10311030
index3,
10321031
(FROM index4 | STATS COUNT(*)) /* after second */
10331032
| WHERE d > 10
1034-
| STATS max = MAX(*)
1035-
BY e
1033+
| STATS max = MAX(*) BY e
10361034
| SORT max DESC`;
10371035

10381036
assertReprint(query, expected);

src/pretty_print/__tests__/wrapping_pretty_printer.test.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
* 2.0.
66
*/
77

8-
import { parse } from '../../parser';
8+
import { Parser } from '../../parser';
99
import type { ESQLMap } from '../../types';
1010
import { Walker } from '../../ast/walker';
1111
import type { WrappingPrettyPrinterOptions } from '../wrapping_pretty_printer';
1212
import { WrappingPrettyPrinter } from '../wrapping_pretty_printer';
1313

1414
const reprint = (src: string, opts?: WrappingPrettyPrinterOptions) => {
15-
const { root } = parse(src);
15+
const { root } = Parser.parse(src);
1616
const text = WrappingPrettyPrinter.print(root, opts);
1717

1818
// console.log(JSON.stringify(root.commands, null, 2));
@@ -230,8 +230,7 @@ FROM index
230230

231231
expect(text).toBe(
232232
`FROM k8s
233-
| STATS count = COUNT()
234-
BY @timestamp = BUCKET(@timestamp, 1 MINUTE)
233+
| STATS count = COUNT() BY @timestamp = BUCKET(@timestamp, 1 MINUTE)
235234
| CHANGE_POINT count
236235
ON @timestamp
237236
AS type, pvalue
@@ -329,8 +328,7 @@ FROM index
329328
expect(text).toBe(`FROM index
330329
| FORK
331330
(
332-
STATS count = COUNT()
333-
BY category
331+
STATS count = COUNT() BY category
334332
| WHERE count > 10
335333
| SORT count DESC
336334
)
@@ -782,6 +780,29 @@ FROM index
782780
- | LIMIT 10`);
783781
});
784782

783+
test('STATS with multiple agg fields and BY: with default wrap, breaks at BY when line exceeds width', () => {
784+
const query = `FROM kibana_sample_data_logs
785+
| STATS count = COUNT(*), avg = AVG(bytes), p95 = PERCENTILE(bytes, 95), ext = VALUES(tags.keyword) BY ip
786+
| EVAL newField = CASE(count < 100, "groupA", count > 100 AND count < 500, "groupB", "Other")
787+
| KEEP newField`;
788+
const text = reprint(query).text;
789+
// With default wrap (80), STATS args overflow so options (BY) are broken to a new line
790+
expect(text).toMatch(/\|\s*STATS[\s\S]*?\n\s*BY ip/m);
791+
});
792+
793+
test('STATS with multiple agg fields and BY: with large wrap, query fits on one line so BY stays on same line as STATS', () => {
794+
const query = `FROM kibana_sample_data_logs
795+
| STATS count = COUNT(*), avg = AVG(bytes), p95 = PERCENTILE(bytes, 95), ext = VALUES(tags.keyword) BY ip
796+
| EVAL newField = CASE(count < 100, "groupA", count > 100 AND count < 500, "groupB", "Other")
797+
| KEEP newField`;
798+
const text = reprint(query, { wrap: 120, multiline: true }).text;
799+
800+
// With multiline: true and wrap 120, STATS line fits so BY is not broken to a new line
801+
const statsLine = text.split('\n').find((l) => l.includes('STATS') && l.includes('BY ip'));
802+
expect(statsLine).toBeDefined();
803+
expect(statsLine!.trim()).toMatch(/^\| STATS .+ BY ip$/);
804+
});
805+
785806
test('wraps function list', () => {
786807
const query = `
787808
FROM index
@@ -940,7 +961,7 @@ FROM index
940961
describe('map expression', () => {
941962
test('empty map', () => {
942963
const src = `ROW F(0, {"a": 0})`;
943-
const { root } = parse(src);
964+
const { root } = Parser.parse(src);
944965
const map = Walker.match(root, { type: 'map' }) as ESQLMap;
945966

946967
map.entries = [];
@@ -956,7 +977,7 @@ FROM index
956977

957978
test('empty map (multiline)', () => {
958979
const src = `ROW F(0, {"a": 0}) | LIMIT 1`;
959-
const { root } = parse(src);
980+
const { root } = Parser.parse(src);
960981
const map = Walker.match(root, { type: 'map' }) as ESQLMap;
961982

962983
map.entries = [];
@@ -1503,15 +1524,13 @@ describe('subqueries (parens)', () => {
15031524
(FROM index2
15041525
| WHERE a > 10
15051526
| EVAL b = a * 2
1506-
| STATS cnt = COUNT(*)
1507-
BY c
1527+
| STATS cnt = COUNT(*) BY c
15081528
| SORT cnt DESC
15091529
| LIMIT 10),
15101530
index3,
15111531
(FROM index4 | STATS COUNT(*))
15121532
| WHERE d > 10
1513-
| STATS max = MAX(*)
1514-
BY e
1533+
| STATS max = MAX(*) BY e
15151534
| SORT max DESC`
15161535
);
15171536
});

src/pretty_print/wrapping_pretty_printer.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -915,11 +915,20 @@ export class WrappingPrettyPrinter {
915915
options += (options ? ' ' : '') + out.txt;
916916
}
917917

918+
// Remaining width for options on the current line: wrap minus (indent + "| " + cmd + " " + args)
919+
const pipeTab = opts.pipeTab ?? ' ';
920+
const pipeAndSpaceLength = '| '.length;
921+
const spaceBetweenCmdAndArgsLength = 1;
922+
const linePrefixLength =
923+
inp.indent.length +
924+
pipeTab.length +
925+
pipeAndSpaceLength +
926+
cmd.length +
927+
spaceBetweenCmdAndArgsLength +
928+
args.txt.length;
929+
const remainingForOptions = opts.wrap - linePrefixLength;
918930
breakOptions =
919-
breakOptions ||
920-
args.lines > 1 ||
921-
optionsLines > 1 ||
922-
options.length > opts.wrap - inp.remaining - cmd.length - 1 - args.txt.length;
931+
breakOptions || args.lines > 1 || optionsLines > 1 || options.length > remainingForOptions;
923932

924933
if (breakOptions) {
925934
options = optionsTxt.join('\n' + optionIndent);

0 commit comments

Comments
 (0)