@malloydata/malloy 0.0.386 → 0.0.387
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lang/prettify/block-body.js +18 -5
- package/dist/lang/prettify/formatter.js +5 -0
- package/dist/lang/prettify/import-select.d.ts +3 -0
- package/dist/lang/prettify/import-select.js +88 -0
- package/dist/lang/prettify/index.js +24 -0
- package/dist/lang/prettify/leaf.js +27 -0
- package/dist/lang/prettify/rules.d.ts +1 -1
- package/dist/lang/prettify/rules.js +8 -0
- package/dist/lang/prettify/sections.js +140 -21
- package/dist/lang/prettify/tokens.js +25 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* RULE: BLOCK BODY
|
|
7
7
|
*
|
|
8
8
|
* A `{ … }` body containing statements (extend body, view body, etc.). Walks
|
|
9
|
-
* children; between adjacent statements
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* children; between adjacent statements:
|
|
10
|
+
* - `view:` definitions always get a blank line before them (and after
|
|
11
|
+
* the previous one), regardless of whether the source had a blank or
|
|
12
|
+
* what kind preceded — view definitions read as their own sections.
|
|
13
|
+
* - For other kinds: preserve a single user-supplied blank line only if
|
|
14
|
+
* the kinds differ. Same-kind adjacent statements (consecutive
|
|
15
|
+
* dimensions, measures, etc.) never get a blank.
|
|
12
16
|
*
|
|
13
17
|
* Also: top-level body — forces a blank line before each statement after the
|
|
14
18
|
* first, regardless of source spacing (top-level statements should breathe).
|
|
@@ -61,9 +65,18 @@ function formatBlockBody(f, ctx) {
|
|
|
61
65
|
if (c instanceof antlr4ts_1.ParserRuleContext) {
|
|
62
66
|
if (lastChild !== null) {
|
|
63
67
|
const userHadBlank = c._start.line - lastChildEndLine > 1;
|
|
64
|
-
const
|
|
65
|
-
|
|
68
|
+
const lastKind = statementKind(lastChild);
|
|
69
|
+
const curKind = statementKind(c);
|
|
70
|
+
const sameKind = lastKind === curKind;
|
|
71
|
+
// `view:` definitions always breathe — blank line above each one
|
|
72
|
+
// (and after the previous one) regardless of the user's source
|
|
73
|
+
// spacing or what kind preceded.
|
|
74
|
+
if (curKind === 'view' || lastKind === 'view') {
|
|
66
75
|
f.o.blank();
|
|
76
|
+
}
|
|
77
|
+
else if (userHadBlank && !sameKind) {
|
|
78
|
+
f.o.blank();
|
|
79
|
+
}
|
|
67
80
|
}
|
|
68
81
|
f.format(c);
|
|
69
82
|
lastChild = c;
|
|
@@ -60,6 +60,7 @@ const sections_1 = require("./sections");
|
|
|
60
60
|
const field_properties_1 = require("./field-properties");
|
|
61
61
|
const pick_case_1 = require("./pick-case");
|
|
62
62
|
const binary_chain_1 = require("./binary-chain");
|
|
63
|
+
const import_select_1 = require("./import-select");
|
|
63
64
|
class Formatter {
|
|
64
65
|
constructor(src, tokens) {
|
|
65
66
|
this.src = src;
|
|
@@ -123,6 +124,10 @@ class Formatter {
|
|
|
123
124
|
return (0, sections_1.formatSectionStatement)(this, node, rule);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
// RULE: IMPORT SELECT — `import {a, b} from 'x'` stays compact.
|
|
128
|
+
if (node instanceof parser.ImportSelectContext) {
|
|
129
|
+
return (0, import_select_1.formatImportSelect)(this, node);
|
|
130
|
+
}
|
|
126
131
|
// RULE: PICK / CASE / BINARY CHAIN.
|
|
127
132
|
if (node instanceof parser.PickStatementContext)
|
|
128
133
|
return (0, pick_case_1.formatPickStatement)(this, node);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* RULE: IMPORT SELECT — `import {a, b, c} from 'url'`
|
|
7
|
+
*
|
|
8
|
+
* The selection list is `{ id (IS id)? (, id (IS id)?)* }`. Inline if the
|
|
9
|
+
* whole brace-and-contents fits on the current line. Otherwise wrap with
|
|
10
|
+
* each item on its own line at +1 indent.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.formatImportSelect = formatImportSelect;
|
|
14
|
+
const tokens_1 = require("./tokens");
|
|
15
|
+
const leaf_1 = require("./leaf");
|
|
16
|
+
const inline_renderer_1 = require("./inline-renderer");
|
|
17
|
+
function formatImportSelect(f, ctx) {
|
|
18
|
+
// Flush hidden tokens between the previous emit (IMPORT) and our opener
|
|
19
|
+
// so a comment like `import /* tag */ {a} from 'x'` is preserved.
|
|
20
|
+
(0, leaf_1.flushHiddenBefore)(f, ctx._start.tokenIndex);
|
|
21
|
+
const items = ctx.importItem();
|
|
22
|
+
// Grammar puts FROM as the last token of importSelect: emit it ourselves
|
|
23
|
+
// so the trailing space and lastEmittedIdx land correctly.
|
|
24
|
+
const fromTok = ctx.FROM().symbol;
|
|
25
|
+
const fromIdx = fromTok.tokenIndex;
|
|
26
|
+
if (items.length === 0) {
|
|
27
|
+
f.o.space();
|
|
28
|
+
f.o.text('{} from');
|
|
29
|
+
(0, leaf_1.note)(f, tokens_1.L.FROM, fromIdx, fromTok);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const firstItem = items[0];
|
|
33
|
+
const lastItem = items[items.length - 1];
|
|
34
|
+
// Comments anywhere in the items' span (between items, inside an `as is`
|
|
35
|
+
// form, etc.) get stripped by renderItemInline. Fall back to a comment-
|
|
36
|
+
// safe wrap that emits each item via f.format — the leaf walker handles
|
|
37
|
+
// hidden-channel placement.
|
|
38
|
+
const itemsHaveComments = (0, leaf_1.hasCommentsInRange)(f, firstItem._start.tokenIndex, lastItem._stop.tokenIndex);
|
|
39
|
+
if (!itemsHaveComments) {
|
|
40
|
+
const itemStrs = items.map(it => (0, inline_renderer_1.renderItemInline)(f, it));
|
|
41
|
+
const inlineBody = '{' + itemStrs.join(', ') + '} from';
|
|
42
|
+
if (f.o.lineLengthSoFar() + 1 + inlineBody.length <= tokens_1.LINE_BUDGET) {
|
|
43
|
+
f.o.space();
|
|
44
|
+
f.o.text(inlineBody);
|
|
45
|
+
(0, leaf_1.note)(f, tokens_1.L.FROM, fromIdx, fromTok);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Wrap form (no comments): one item per line at +1 indent. Pre-rendered
|
|
49
|
+
// text is fine because renderItemInline saw no comments to drop.
|
|
50
|
+
f.o.space();
|
|
51
|
+
f.o.text('{');
|
|
52
|
+
f.o.indent++;
|
|
53
|
+
for (let i = 0; i < itemStrs.length; i++) {
|
|
54
|
+
f.o.nl();
|
|
55
|
+
f.o.text(itemStrs[i]);
|
|
56
|
+
if (i < itemStrs.length - 1)
|
|
57
|
+
f.o.text(',');
|
|
58
|
+
}
|
|
59
|
+
f.o.indent--;
|
|
60
|
+
f.o.nl();
|
|
61
|
+
f.o.text('} from');
|
|
62
|
+
(0, leaf_1.note)(f, tokens_1.L.FROM, fromIdx, fromTok);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Comment-safe wrap: each item emits via f.format so flushHiddenBefore
|
|
66
|
+
// can place its leading/inter-item comments correctly. Trailing `,` after
|
|
67
|
+
// each non-last item, leaf walker turns it into a newline at indent.
|
|
68
|
+
f.o.space();
|
|
69
|
+
f.o.text('{');
|
|
70
|
+
f.o.indent++;
|
|
71
|
+
// Advance past the OCURLY we just emitted manually so the first item's
|
|
72
|
+
// flushHiddenBefore doesn't try to re-emit it.
|
|
73
|
+
f.lastEmittedIdx = ctx._start.tokenIndex;
|
|
74
|
+
for (let i = 0; i < items.length; i++) {
|
|
75
|
+
(0, leaf_1.flushHiddenBefore)(f, items[i]._start.tokenIndex);
|
|
76
|
+
f.o.nl();
|
|
77
|
+
f.format(items[i]);
|
|
78
|
+
if (i < items.length - 1)
|
|
79
|
+
f.o.text(',');
|
|
80
|
+
}
|
|
81
|
+
// Catch any tail comments between the last item and the closing `}`.
|
|
82
|
+
(0, leaf_1.flushHiddenBefore)(f, fromIdx);
|
|
83
|
+
f.o.indent--;
|
|
84
|
+
f.o.nl();
|
|
85
|
+
f.o.text('} from');
|
|
86
|
+
(0, leaf_1.note)(f, tokens_1.L.FROM, fromIdx, fromTok);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=import-select.js.map
|
|
@@ -57,6 +57,30 @@
|
|
|
57
57
|
* - Single-arg function calls don't wrap (no point — nowhere useful to break).
|
|
58
58
|
* - `(` hugs only after a known-callable token (CALL_HUG_AFTER); after `is`,
|
|
59
59
|
* `as`, `extend`, `on`, `when`, etc. the `(` is grouping and gets a space.
|
|
60
|
+
* CALL_HUG_AFTER includes the keyword-named built-ins that are commonly
|
|
61
|
+
* used as functions: ALL, EXCLUDE, the timeframe truncation keywords
|
|
62
|
+
* (YEAR/MONTH/DAY/…), and the cast-target type names
|
|
63
|
+
* (TIMESTAMP/DATE/NUMBER/STRING/BOOLEAN/JSON).
|
|
64
|
+
* - `!` is the cast operator (`epoch_ms!timestamp(x)`); it glues to both
|
|
65
|
+
* sides like `.` does.
|
|
66
|
+
* - Empty `{}` collapses inline (`extend {}`, not `extend {\n}`).
|
|
67
|
+
* - `import {a, b, c} from 'x'` formats as a flat list when it fits on the
|
|
68
|
+
* line; otherwise one item per line at +1 indent.
|
|
69
|
+
* - `view:` definitions in a block body always have a blank line before
|
|
70
|
+
* each one (after the first), even when no blank was in the source. The
|
|
71
|
+
* same-kind-no-blank rule still applies to other statement kinds.
|
|
72
|
+
* - `{...}` bodies that contain more than one section statement never
|
|
73
|
+
* collapse onto a single line, even when they would fit. Reading two
|
|
74
|
+
* `group_by:` clauses jammed on one line is hostile.
|
|
75
|
+
* - Single is-item section lists keep the keyword and item on the same
|
|
76
|
+
* line (`nest: name is { … }`), so the body wraps naturally instead of
|
|
77
|
+
* forcing a `nest:\n name is {` opener split.
|
|
78
|
+
* - join_one / join_many / join_cross multi-item lists always wrap one
|
|
79
|
+
* item per line — items use `with`/`on` instead of `is` but are
|
|
80
|
+
* structurally is-like.
|
|
81
|
+
* - Trailing comments between the last item of a section list and the
|
|
82
|
+
* enclosing `}` are emitted at the section's inner indent, so they
|
|
83
|
+
* stay associated with the section the user wrote them in.
|
|
60
84
|
*
|
|
61
85
|
* Adding a new section-statement
|
|
62
86
|
* ------------------------------
|
|
@@ -127,6 +127,18 @@ function hasCommentsInRange(f, fromIdx, toIdx) {
|
|
|
127
127
|
}
|
|
128
128
|
return false;
|
|
129
129
|
}
|
|
130
|
+
// Index of the next non-hidden, non-EOF token strictly after `idx`, or -1.
|
|
131
|
+
function nextVisibleAfter(f, idx) {
|
|
132
|
+
for (let j = idx + 1; j < f.tokens.length; j++) {
|
|
133
|
+
const t = f.tokens[j];
|
|
134
|
+
if (t.channel === antlr4ts_1.Token.HIDDEN_CHANNEL)
|
|
135
|
+
continue;
|
|
136
|
+
if (t.type === antlr4ts_1.Token.EOF)
|
|
137
|
+
return -1;
|
|
138
|
+
return j;
|
|
139
|
+
}
|
|
140
|
+
return -1;
|
|
141
|
+
}
|
|
130
142
|
// Does the paren-pair at [openIdx, closeIdx] have any COMMA at its own depth?
|
|
131
143
|
// (Used to distinguish "function call with multiple args" from "single-arg
|
|
132
144
|
// call" / "empty parens".)
|
|
@@ -190,6 +202,21 @@ function emitVisibleToken(f, t, idx) {
|
|
|
190
202
|
if (t.type === tokens_1.L.OCURLY) {
|
|
191
203
|
f.o.space();
|
|
192
204
|
f.o.text('{');
|
|
205
|
+
// Empty `{}`: peek the next visible token. If it's the matching close
|
|
206
|
+
// AND nothing hidden sits between them (no comments to preserve), emit
|
|
207
|
+
// inline so we get `extend {}` not `extend {\n}`. With a comment in the
|
|
208
|
+
// gap (`extend { /* keep */ }`), fall through to the wrapping form so
|
|
209
|
+
// the leaf walker's comment placement runs.
|
|
210
|
+
const nextVisible = nextVisibleAfter(f, idx);
|
|
211
|
+
if (nextVisible !== -1 &&
|
|
212
|
+
f.tokens[nextVisible].type === tokens_1.L.CCURLY &&
|
|
213
|
+
!hasCommentsInRange(f, idx + 1, nextVisible - 1)) {
|
|
214
|
+
f.o.text('}');
|
|
215
|
+
if (f.o.indent === 0)
|
|
216
|
+
f.needBlank = true;
|
|
217
|
+
note(f, tokens_1.L.CCURLY, nextVisible, f.tokens[nextVisible]);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
193
220
|
f.o.indent++;
|
|
194
221
|
f.o.nl();
|
|
195
222
|
note(f, t.type, idx, t);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ParserRuleContext } from 'antlr4ts';
|
|
2
|
-
export type ItemKind = 'fieldEntry' | 'nestEntry' | 'fieldDef' | 'fieldName' | 'collectionMember' | 'orderBySpec' | 'fieldExpr';
|
|
2
|
+
export type ItemKind = 'fieldEntry' | 'nestEntry' | 'fieldDef' | 'fieldName' | 'collectionMember' | 'orderBySpec' | 'fieldExpr' | 'joinDef' | 'includeField' | 'indexElement';
|
|
3
3
|
export interface SectionRule {
|
|
4
4
|
ctxClass: new (...args: never[]) => ParserRuleContext;
|
|
5
5
|
keywordTypes: number[];
|
|
@@ -79,6 +79,14 @@ exports.SECTION_STATEMENT_RULES = [
|
|
|
79
79
|
rule(parser.OrderByStatementContext, [tokens_1.L.ORDER_BY], c => c.ordering(), 'orderBySpec'),
|
|
80
80
|
rule(parser.WhereStatementContext, [tokens_1.L.WHERE], c => c.filterClauseList(), 'fieldExpr'),
|
|
81
81
|
rule(parser.HavingStatementContext, [tokens_1.L.HAVING], c => c.filterClauseList(), 'fieldExpr'),
|
|
82
|
+
rule(parser.DefJoinOneContext, [tokens_1.L.JOIN_ONE], c => c.joinList(), 'joinDef'),
|
|
83
|
+
rule(parser.DefJoinManyContext, [tokens_1.L.JOIN_MANY], c => c.joinList(), 'joinDef'),
|
|
84
|
+
rule(parser.DefJoinCrossContext, [tokens_1.L.JOIN_CROSS], c => c.joinList(), 'joinDef'),
|
|
85
|
+
// include block items: `public: a, b`, `internal: x, y, z`. The keyword
|
|
86
|
+
// (PUBLIC/PRIVATE/INTERNAL) lives one level down inside accessLabelProp;
|
|
87
|
+
// findKeyword in ./sections handles that nested case.
|
|
88
|
+
rule(parser.IncludeItemContext, [tokens_1.L.PUBLIC, tokens_1.L.PRIVATE, tokens_1.L.INTERNAL], c => c.includeList(), 'includeField'),
|
|
89
|
+
rule(parser.IndexStatementContext, [tokens_1.L.INDEX], c => c.indexFields(), 'indexElement'),
|
|
82
90
|
];
|
|
83
91
|
// Coarse statement-kind labels for the same-kind-no-blank rule in block
|
|
84
92
|
// bodies. Different kinds preserve a single user-supplied blank line.
|
|
@@ -13,11 +13,21 @@
|
|
|
13
13
|
*
|
|
14
14
|
* formatSectionList rule (locked in with the user):
|
|
15
15
|
* - All bare items + total fits ≤ LINE_BUDGET → inline `kw: a, b, c`.
|
|
16
|
-
* - Single item that fits (even if it has `is`) → inline.
|
|
16
|
+
* - Single item that fits (even if it has `is`) → inline. Items containing
|
|
17
|
+
* a `{...}` body with more than one section statement are excluded —
|
|
18
|
+
* view bodies don't read on one line regardless of length.
|
|
19
|
+
* - Single is-item that doesn't fit inline → keep keyword and item on the
|
|
20
|
+
* same line (`nest: name is { …wrapped body… }`); the body's `{...}`
|
|
21
|
+
* wraps internally. Annotated items still take the keyword-on-own-line
|
|
22
|
+
* form so the annotation lands above its item.
|
|
17
23
|
* - Otherwise → wrapped: keyword on its own line; items at +1 indent;
|
|
18
24
|
* bare items flow-fill ≤ LINE_BUDGET, comma-separated intra-line,
|
|
19
25
|
* no trailing commas; `is` items each on own line; annotated items
|
|
20
26
|
* each on own line, annotation on the line above.
|
|
27
|
+
*
|
|
28
|
+
* After the last item, trailing comments that sit between the section and
|
|
29
|
+
* the enclosing `}` are also emitted at the inner indent — they belong to
|
|
30
|
+
* the section the user just wrote, not the parent block.
|
|
21
31
|
*/
|
|
22
32
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
33
|
if (k2 === undefined) k2 = k;
|
|
@@ -70,7 +80,9 @@ function formatSectionStatement(f, stmt, rule) {
|
|
|
70
80
|
}
|
|
71
81
|
for (let i = 0; i < stmt.childCount; i++) {
|
|
72
82
|
const c = stmt.getChild(i);
|
|
73
|
-
|
|
83
|
+
const isKeywordChild = (c instanceof tree_1.TerminalNode && c.symbol === keywordTok) ||
|
|
84
|
+
(c instanceof antlr4ts_1.ParserRuleContext && childContainsToken(c, keywordTok));
|
|
85
|
+
if (isKeywordChild) {
|
|
74
86
|
(0, leaf_1.flushHiddenBefore)(f, keywordTok.tokenIndex);
|
|
75
87
|
if (f.o.indent > 0)
|
|
76
88
|
f.o.nl();
|
|
@@ -83,18 +95,39 @@ function formatSectionStatement(f, stmt, rule) {
|
|
|
83
95
|
if (c === listCtx) {
|
|
84
96
|
const items = listItems(listCtx, rule.itemKind);
|
|
85
97
|
if (items.length > 0)
|
|
86
|
-
formatSectionList(f, items);
|
|
98
|
+
formatSectionList(f, items, rule.itemKind);
|
|
87
99
|
continue;
|
|
88
100
|
}
|
|
89
101
|
f.format(c);
|
|
90
102
|
}
|
|
91
103
|
}
|
|
104
|
+
function childContainsToken(node, tok) {
|
|
105
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
106
|
+
const c = node.getChild(i);
|
|
107
|
+
if (c instanceof tree_1.TerminalNode && c.symbol === tok)
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
92
112
|
function findKeyword(node, types) {
|
|
113
|
+
// Direct terminal children — fast path, the common case.
|
|
93
114
|
for (let i = 0; i < node.childCount; i++) {
|
|
94
115
|
const c = node.getChild(i);
|
|
95
116
|
if (c instanceof tree_1.TerminalNode && types.includes(c.symbol.type))
|
|
96
117
|
return c.symbol;
|
|
97
118
|
}
|
|
119
|
+
// Fallback: descend one level. Some rules wrap the keyword in a small
|
|
120
|
+
// nested rule (e.g. includeItem → accessLabelProp → INTERNAL).
|
|
121
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
122
|
+
const c = node.getChild(i);
|
|
123
|
+
if (c instanceof antlr4ts_1.ParserRuleContext) {
|
|
124
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
125
|
+
const g = c.getChild(j);
|
|
126
|
+
if (g instanceof tree_1.TerminalNode && types.includes(g.symbol.type))
|
|
127
|
+
return g.symbol;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
98
131
|
return undefined;
|
|
99
132
|
}
|
|
100
133
|
function listItems(listCtx, itemKind) {
|
|
@@ -117,6 +150,12 @@ function listItems(listCtx, itemKind) {
|
|
|
117
150
|
return c instanceof parser.OrderBySpecContext;
|
|
118
151
|
case 'fieldExpr':
|
|
119
152
|
return c instanceof parser.FieldExprContext;
|
|
153
|
+
case 'joinDef':
|
|
154
|
+
return c instanceof parser.JoinDefContext;
|
|
155
|
+
case 'includeField':
|
|
156
|
+
return c instanceof parser.IncludeFieldContext;
|
|
157
|
+
case 'indexElement':
|
|
158
|
+
return c instanceof parser.IndexElementContext;
|
|
120
159
|
}
|
|
121
160
|
};
|
|
122
161
|
const out = [];
|
|
@@ -127,8 +166,10 @@ function listItems(listCtx, itemKind) {
|
|
|
127
166
|
}
|
|
128
167
|
return out;
|
|
129
168
|
}
|
|
130
|
-
function classifyItem(f, ctx) {
|
|
131
|
-
|
|
169
|
+
function classifyItem(f, ctx, itemKind) {
|
|
170
|
+
// joinDef items always wrap onto their own line. They use `with` / `on`
|
|
171
|
+
// instead of `is`, but they're structurally one-per-line like is-items.
|
|
172
|
+
let hasIs = itemKind === 'joinDef';
|
|
132
173
|
let hasAnnotation = false;
|
|
133
174
|
for (let i = ctx._start.tokenIndex; i <= ctx._stop.tokenIndex; i++) {
|
|
134
175
|
const t = f.tokens[i];
|
|
@@ -142,17 +183,23 @@ function classifyItem(f, ctx) {
|
|
|
142
183
|
}
|
|
143
184
|
return { ctx, hasIs, hasAnnotation };
|
|
144
185
|
}
|
|
145
|
-
function formatSectionList(f, items) {
|
|
146
|
-
const itemInfos = items.map(it => classifyItem(f, it));
|
|
186
|
+
function formatSectionList(f, items, itemKind) {
|
|
187
|
+
const itemInfos = items.map(it => classifyItem(f, it, itemKind));
|
|
147
188
|
const noAnnotations = itemInfos.every(info => !info.hasAnnotation);
|
|
148
189
|
const allBare = itemInfos.every(info => !info.hasIs && !info.hasAnnotation);
|
|
149
190
|
const firstItem = items[0];
|
|
150
191
|
const lastItem = items[items.length - 1];
|
|
151
192
|
// Inline candidate: no annotations, no hidden-channel comments anywhere in
|
|
152
193
|
// the items' span (renderItemInline drops them), AND either all bare or
|
|
153
|
-
// exactly one item.
|
|
194
|
+
// exactly one item. Items containing a `{...}` body with multiple inner
|
|
195
|
+
// statements are also excluded — collapsing a view body onto one line is
|
|
196
|
+
// hostile to read regardless of length.
|
|
154
197
|
const itemsHaveComments = (0, leaf_1.hasCommentsInRange)(f, firstItem._start.tokenIndex, lastItem._stop.tokenIndex);
|
|
155
|
-
const
|
|
198
|
+
const itemsHaveMultiStatementBody = itemInfos.some(info => hasMultiStatementCurlyBody(f, info.ctx));
|
|
199
|
+
const inlineEligible = noAnnotations &&
|
|
200
|
+
!itemsHaveComments &&
|
|
201
|
+
!itemsHaveMultiStatementBody &&
|
|
202
|
+
(allBare || items.length === 1);
|
|
156
203
|
if (inlineEligible) {
|
|
157
204
|
const renderedItems = itemInfos.map(info => (0, inline_renderer_1.renderItemInline)(f, info.ctx));
|
|
158
205
|
const inlineBody = renderedItems.join(', ');
|
|
@@ -164,6 +211,20 @@ function formatSectionList(f, items) {
|
|
|
164
211
|
return;
|
|
165
212
|
}
|
|
166
213
|
}
|
|
214
|
+
// Single is-item that doesn't fit inline: keep the keyword on the same
|
|
215
|
+
// line as the item (`nest: name is { …wrapped body… }`) instead of
|
|
216
|
+
// breaking before the name. The body's `{...}` will wrap on its own.
|
|
217
|
+
// Annotated items still need the keyword-on-own-line form so the
|
|
218
|
+
// annotation can land between them.
|
|
219
|
+
if (items.length === 1 &&
|
|
220
|
+
itemInfos[0].hasIs &&
|
|
221
|
+
!itemInfos[0].hasAnnotation &&
|
|
222
|
+
!itemsHaveComments) {
|
|
223
|
+
f.o.text(' ');
|
|
224
|
+
f.format(itemInfos[0].ctx);
|
|
225
|
+
f.lastEmittedType = lastItem._stop.type;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
167
228
|
// Wrapped form. Two paths:
|
|
168
229
|
// - No comments anywhere: original flow-fill — bare items pack into lines
|
|
169
230
|
// at LINE_BUDGET, `is`/annotated items each get their own line.
|
|
@@ -230,14 +291,56 @@ function formatSectionList(f, items) {
|
|
|
230
291
|
// lastEmittedIdx so tail comments aren't re-emitted by the parent.
|
|
231
292
|
f.lastEmittedType = lastItem._stop.type;
|
|
232
293
|
}
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
294
|
+
// Does any `{...}` block inside this item contain more than one section
|
|
295
|
+
// keyword (group_by:, aggregate:, where:, …) at its top level? Used to gate
|
|
296
|
+
// the section-list inline form: a view body with multiple statements never
|
|
297
|
+
// reads well on a single line, even when it fits. Single-statement bodies
|
|
298
|
+
// like `{ where: x = 1 }` may still inline.
|
|
299
|
+
function hasMultiStatementCurlyBody(f, ctx) {
|
|
300
|
+
const fromIdx = ctx._start.tokenIndex;
|
|
301
|
+
const toIdx = ctx._stop.tokenIndex;
|
|
302
|
+
for (let i = fromIdx; i <= toIdx; i++) {
|
|
303
|
+
if (f.tokens[i].type !== tokens_1.L.OCURLY)
|
|
304
|
+
continue;
|
|
305
|
+
const close = (0, tokens_1.findMatching)(f.tokens, i, tokens_1.L.OCURLY, tokens_1.L.CCURLY);
|
|
306
|
+
let count = 0;
|
|
307
|
+
let depth = 0;
|
|
308
|
+
for (let j = i + 1; j < close; j++) {
|
|
309
|
+
const t = f.tokens[j];
|
|
310
|
+
if (t.type === tokens_1.L.OCURLY || t.type === tokens_1.L.OPAREN || t.type === tokens_1.L.OBRACK) {
|
|
311
|
+
depth++;
|
|
312
|
+
}
|
|
313
|
+
else if (t.type === tokens_1.L.CCURLY ||
|
|
314
|
+
t.type === tokens_1.L.CPAREN ||
|
|
315
|
+
t.type === tokens_1.L.CBRACK) {
|
|
316
|
+
depth--;
|
|
317
|
+
}
|
|
318
|
+
else if (depth === 0 && tokens_1.SECTION_TOKENS.has(t.type)) {
|
|
319
|
+
count++;
|
|
320
|
+
if (count > 1)
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
i = close;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
// After the last item of a wrapped section list, flush trailing comments
|
|
329
|
+
// belonging to the section. There are two cases:
|
|
330
|
+
//
|
|
331
|
+
// 1. Same-line tail: a comment on the SAME source line as the last item.
|
|
332
|
+
// Always belongs to the last item; emit at the section's inner indent.
|
|
333
|
+
//
|
|
334
|
+
// 2. Different-line trailing comments that sit between the last item and
|
|
335
|
+
// the closing `}` of the enclosing block (no other statement follows).
|
|
336
|
+
// These visually belong to the section the user just wrote, not the
|
|
337
|
+
// block. Emit them at the inner indent so they stay associated with
|
|
338
|
+
// the section. If a real statement follows the comments, leave them
|
|
339
|
+
// for the parent — they're leading comments for that statement.
|
|
238
340
|
function flushSameLineTail(f, lastTok) {
|
|
239
341
|
const lastEndLine = (0, tokens_1.endLineOf)(lastTok);
|
|
240
342
|
let j = lastTok.tokenIndex + 1;
|
|
343
|
+
// Phase 1: same-line tail comments.
|
|
241
344
|
while (j < f.tokens.length) {
|
|
242
345
|
const t = f.tokens[j];
|
|
243
346
|
if (t.channel !== antlr4ts_1.Token.HIDDEN_CHANNEL)
|
|
@@ -247,15 +350,31 @@ function flushSameLineTail(f, lastTok) {
|
|
|
247
350
|
j++;
|
|
248
351
|
}
|
|
249
352
|
if (j > lastTok.tokenIndex + 1) {
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// newline back for EOL comments). Without this, the comment lands on a
|
|
255
|
-
// new line, and a re-parse sees it as a different-line comment, breaking
|
|
256
|
-
// idempotence.
|
|
353
|
+
// Same-line comments: drop the wrapping loop's per-item newline so
|
|
354
|
+
// emitHiddenToken's same-line branch reattaches the comment correctly
|
|
355
|
+
// (and adds a trailing newline back for EOL comments). Otherwise a
|
|
356
|
+
// re-parse sees a different-line comment, breaking idempotence.
|
|
257
357
|
f.o.trimTrailingNewlines();
|
|
258
358
|
(0, leaf_1.flushHiddenBefore)(f, j);
|
|
259
359
|
}
|
|
360
|
+
// Phase 2: own-line comments before the next visible token. If the next
|
|
361
|
+
// visible token is a closing `}`, the comments visually belong to this
|
|
362
|
+
// section — emit them at the inner indent. Otherwise leave them for the
|
|
363
|
+
// parent (they're leading comments for whatever follows).
|
|
364
|
+
let k = j;
|
|
365
|
+
let trailingHidden = 0;
|
|
366
|
+
while (k < f.tokens.length) {
|
|
367
|
+
const t = f.tokens[k];
|
|
368
|
+
if (t.channel !== antlr4ts_1.Token.HIDDEN_CHANNEL)
|
|
369
|
+
break;
|
|
370
|
+
trailingHidden++;
|
|
371
|
+
k++;
|
|
372
|
+
}
|
|
373
|
+
if (trailingHidden > 0 && k < f.tokens.length) {
|
|
374
|
+
const next = f.tokens[k];
|
|
375
|
+
if (next.type === tokens_1.L.CCURLY) {
|
|
376
|
+
(0, leaf_1.flushHiddenBefore)(f, k);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
260
379
|
}
|
|
261
380
|
//# sourceMappingURL=sections.js.map
|
|
@@ -87,6 +87,26 @@ exports.CALL_HUG_AFTER = new Set([
|
|
|
87
87
|
exports.L.CAST,
|
|
88
88
|
exports.L.NOW,
|
|
89
89
|
exports.L.LAST,
|
|
90
|
+
// Ungrouped / level-modifier function-style calls.
|
|
91
|
+
exports.L.ALL,
|
|
92
|
+
exports.L.EXCLUDE,
|
|
93
|
+
// Timeframe truncation keywords used as functions: year(x), month(x), …
|
|
94
|
+
exports.L.YEAR,
|
|
95
|
+
exports.L.QUARTER,
|
|
96
|
+
exports.L.MONTH,
|
|
97
|
+
exports.L.WEEK,
|
|
98
|
+
exports.L.DAY,
|
|
99
|
+
exports.L.HOUR,
|
|
100
|
+
exports.L.MINUTE,
|
|
101
|
+
exports.L.SECOND,
|
|
102
|
+
// Type-cast keyword names: timestamp(x), date(x), number(x), string(x), …
|
|
103
|
+
exports.L.TIMESTAMP,
|
|
104
|
+
exports.L.TIMESTAMPTZ,
|
|
105
|
+
exports.L.DATE,
|
|
106
|
+
exports.L.NUMBER,
|
|
107
|
+
exports.L.STRING,
|
|
108
|
+
exports.L.BOOLEAN,
|
|
109
|
+
exports.L.JSON,
|
|
90
110
|
]);
|
|
91
111
|
// Binary operators that get spaces on both sides at the leaf level.
|
|
92
112
|
// (Chain wrapping for and/or/??/+/- happens at parse-tree level — see
|
|
@@ -119,10 +139,15 @@ function leadingAction(prevType, nextType) {
|
|
|
119
139
|
nextType === exports.L.SEMI ||
|
|
120
140
|
nextType === exports.L.COLON ||
|
|
121
141
|
nextType === exports.L.TRIPLECOLON ||
|
|
142
|
+
nextType === exports.L.EXCLAM ||
|
|
122
143
|
nextType === exports.L.CPAREN ||
|
|
123
144
|
nextType === exports.L.CBRACK) {
|
|
124
145
|
return 'glue';
|
|
125
146
|
}
|
|
147
|
+
// After the `!` cast operator (`epoch_ms!timestamp(x)`), the next token
|
|
148
|
+
// glues to it like the `.` operator does.
|
|
149
|
+
if (prevType === exports.L.EXCLAM)
|
|
150
|
+
return 'glue';
|
|
126
151
|
if ((nextType === exports.L.OPAREN || nextType === exports.L.OBRACK) &&
|
|
127
152
|
prevType !== null &&
|
|
128
153
|
exports.CALL_HUG_AFTER.has(prevType)) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MALLOY_VERSION = "0.0.
|
|
1
|
+
export declare const MALLOY_VERSION = "0.0.387";
|
package/dist/version.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MALLOY_VERSION = void 0;
|
|
4
4
|
// generated with 'generate-version-file' script; do not edit manually
|
|
5
|
-
exports.MALLOY_VERSION = '0.0.
|
|
5
|
+
exports.MALLOY_VERSION = '0.0.387';
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/malloy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.387",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@malloydata/malloy-filter": "0.0.
|
|
55
|
-
"@malloydata/malloy-interfaces": "0.0.
|
|
56
|
-
"@malloydata/malloy-tag": "0.0.
|
|
54
|
+
"@malloydata/malloy-filter": "0.0.387",
|
|
55
|
+
"@malloydata/malloy-interfaces": "0.0.387",
|
|
56
|
+
"@malloydata/malloy-tag": "0.0.387",
|
|
57
57
|
"@noble/hashes": "^1.8.0",
|
|
58
58
|
"antlr4ts": "^0.5.0-alpha.4",
|
|
59
59
|
"assert": "^2.0.0",
|