@openrewrite/rewrite 8.69.0-20251209-092419 → 8.69.0-20251209-133839
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/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +2 -18
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/tabs-and-indents-visitor.d.ts +4 -3
- package/dist/javascript/tabs-and-indents-visitor.d.ts.map +1 -1
- package/dist/javascript/tabs-and-indents-visitor.js +176 -184
- package/dist/javascript/tabs-and-indents-visitor.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/javascript/parser.ts +2 -18
- package/src/javascript/tabs-and-indents-visitor.ts +176 -179
|
@@ -34,17 +34,22 @@ import {TabsAndIndentsStyle} from "./style";
|
|
|
34
34
|
import {findMarker} from "../markers";
|
|
35
35
|
|
|
36
36
|
type IndentKind = 'block' | 'continuation' | 'align';
|
|
37
|
+
type IndentContext = [number, IndentKind]; // [indent, kind]
|
|
37
38
|
export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
38
|
-
private readonly
|
|
39
|
+
private readonly indentSize: number;
|
|
40
|
+
private readonly useTabCharacter: boolean;
|
|
39
41
|
|
|
40
42
|
constructor(private readonly tabsAndIndentsStyle: TabsAndIndentsStyle, private stopAfter?: Tree) {
|
|
41
43
|
super();
|
|
44
|
+
this.indentSize = this.tabsAndIndentsStyle.indentSize;
|
|
45
|
+
this.useTabCharacter = this.tabsAndIndentsStyle.useTabCharacter;
|
|
46
|
+
}
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.singleIndent = " ".repeat(this.tabsAndIndentsStyle.indentSize);
|
|
48
|
+
private indentString(indent: number): string {
|
|
49
|
+
if (this.useTabCharacter) {
|
|
50
|
+
return "\t".repeat(Math.floor(indent / this.indentSize));
|
|
47
51
|
}
|
|
52
|
+
return " ".repeat(indent);
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
protected async preVisit(tree: J, _p: P): Promise<J | undefined> {
|
|
@@ -53,56 +58,90 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
private setupCursorMessagesForTree(cursor: Cursor, tree: J): void {
|
|
61
|
+
// Check if this MethodInvocation starts a method chain (select.after has newline)
|
|
62
|
+
if (tree.kind === J.Kind.MethodInvocation) {
|
|
63
|
+
const mi = tree as J.MethodInvocation;
|
|
64
|
+
if (mi.select && mi.select.after.whitespace.includes("\n")) {
|
|
65
|
+
// This MethodInvocation has a chained method call after it
|
|
66
|
+
// Store the ORIGINAL parent indent context in "chainedIndentContext"
|
|
67
|
+
// This will be propagated down and used when we reach the chain's innermost element
|
|
68
|
+
const parentContext = this.getParentIndentContext(cursor);
|
|
69
|
+
cursor.messages.set("chainedIndentContext", parentContext);
|
|
70
|
+
// For children (arguments), use continuation indent
|
|
71
|
+
// But the prefix will be normalized in postVisit using chainedIndentContext
|
|
72
|
+
const [parentIndent, parentIndentKind] = parentContext;
|
|
73
|
+
cursor.messages.set("indentContext", [parentIndent + this.indentSize, parentIndentKind] as IndentContext);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Check if we're at the base of a chain (no select) and parent has chainedIndentContext
|
|
77
|
+
if (!mi.select) {
|
|
78
|
+
const chainedContext = cursor.parent?.messages.get("chainedIndentContext") as IndentContext | undefined;
|
|
79
|
+
if (chainedContext !== undefined) {
|
|
80
|
+
// Consume the chainedIndentContext - this is the base of the chain
|
|
81
|
+
// The base element gets the original indent (no extra continuation)
|
|
82
|
+
// Propagate chainedIndentContext so the `name` child knows not to add indent
|
|
83
|
+
cursor.messages.set("indentContext", chainedContext);
|
|
84
|
+
cursor.messages.set("chainedIndentContext", chainedContext);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if we're the `name` of a MethodInvocation at the base of a chain
|
|
91
|
+
if (tree.kind === J.Kind.Identifier) {
|
|
92
|
+
const parentValue = cursor.parent?.value;
|
|
93
|
+
if (parentValue?.kind === J.Kind.MethodInvocation) {
|
|
94
|
+
const parentMi = parentValue as J.MethodInvocation;
|
|
95
|
+
// Check if parent has chainedIndentContext (meaning it's at the base of a chain)
|
|
96
|
+
// and we are the `name` (not an argument or something else)
|
|
97
|
+
const parentChainedContext = cursor.parent?.messages.get("chainedIndentContext") as IndentContext | undefined;
|
|
98
|
+
if (parentChainedContext !== undefined && !parentMi.select) {
|
|
99
|
+
// We're the name of a chain-base MethodInvocation - use the chained indent directly
|
|
100
|
+
cursor.messages.set("indentContext", parentChainedContext);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
56
106
|
const [parentMyIndent, parentIndentKind] = this.getParentIndentContext(cursor);
|
|
57
107
|
const myIndent = this.computeMyIndent(tree, parentMyIndent, parentIndentKind);
|
|
58
|
-
cursor.messages.set("myIndent", myIndent);
|
|
59
108
|
|
|
60
109
|
// For Binary, behavior depends on whether it's already on a continuation line
|
|
110
|
+
let indentKind: IndentKind;
|
|
61
111
|
if (tree.kind === J.Kind.Binary) {
|
|
62
|
-
const hasNewline = tree.prefix?.whitespace?.includes("\n") ||
|
|
63
|
-
tree.prefix?.comments?.some(c => c.suffix.includes("\n"));
|
|
64
112
|
// If Binary has newline OR parent is in align mode, children align
|
|
65
|
-
const
|
|
66
|
-
|
|
113
|
+
const hasNewline = this.prefixContainsNewline(tree);
|
|
114
|
+
indentKind = (hasNewline || parentIndentKind === 'align') ? 'align' : 'continuation';
|
|
67
115
|
} else {
|
|
68
|
-
|
|
116
|
+
indentKind = this.computeIndentKind(tree);
|
|
69
117
|
}
|
|
70
|
-
}
|
|
71
118
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Container/RightPadded/LeftPadded inherit myIndent but don't have indentKind
|
|
75
|
-
let parentIndent: string | undefined;
|
|
76
|
-
let parentKind: IndentKind | undefined;
|
|
119
|
+
cursor.messages.set("indentContext", [myIndent, indentKind] as IndentContext);
|
|
120
|
+
}
|
|
77
121
|
|
|
122
|
+
private getParentIndentContext(cursor: Cursor): IndentContext {
|
|
123
|
+
// Walk up the cursor chain to find the nearest indent context
|
|
124
|
+
// We need to walk because intermediate nodes like RightPadded may not have context set
|
|
78
125
|
for (let c = cursor.parent; c != null; c = c.parent) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
126
|
+
// chainedIndentContext stores the original context - prefer it
|
|
127
|
+
const chainedContext = c.messages.get("chainedIndentContext") as IndentContext | undefined;
|
|
128
|
+
if (chainedContext !== undefined) {
|
|
129
|
+
return chainedContext;
|
|
84
130
|
}
|
|
85
131
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
parentKind = kind;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Found both, we can stop
|
|
94
|
-
if (parentIndent !== undefined && parentKind !== undefined) {
|
|
95
|
-
break;
|
|
132
|
+
const context = c.messages.get("indentContext") as IndentContext | undefined;
|
|
133
|
+
if (context !== undefined) {
|
|
134
|
+
return context;
|
|
96
135
|
}
|
|
97
136
|
}
|
|
98
137
|
|
|
99
|
-
return [
|
|
138
|
+
return [0, 'continuation'];
|
|
100
139
|
}
|
|
101
140
|
|
|
102
|
-
private computeMyIndent(tree: J, parentMyIndent:
|
|
103
|
-
// CompilationUnit is the root - it always has myIndent=
|
|
141
|
+
private computeMyIndent(tree: J, parentMyIndent: number, parentIndentKind: IndentKind): number {
|
|
142
|
+
// CompilationUnit is the root - it always has myIndent=0 regardless of prefix content
|
|
104
143
|
if (tree.kind === JS.Kind.CompilationUnit) {
|
|
105
|
-
return
|
|
144
|
+
return 0;
|
|
106
145
|
}
|
|
107
146
|
// TemplateExpressionSpan: reset indent context - template literal content determines its own indentation
|
|
108
147
|
// The expression inside ${...} should be indented based on where it appears in the template, not outer code
|
|
@@ -112,32 +151,38 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
112
151
|
const prefix = span.expression?.prefix?.whitespace ?? "";
|
|
113
152
|
const lastNewline = prefix.lastIndexOf("\n");
|
|
114
153
|
if (lastNewline >= 0) {
|
|
115
|
-
return prefix.
|
|
154
|
+
return prefix.length - lastNewline - 1;
|
|
116
155
|
}
|
|
117
|
-
return
|
|
156
|
+
return 0;
|
|
118
157
|
}
|
|
119
158
|
if (tree.kind === J.Kind.IfElse || parentIndentKind === 'align') {
|
|
120
159
|
return parentMyIndent;
|
|
121
160
|
}
|
|
161
|
+
// Certain structures don't add indent for themselves - they stay at parent level
|
|
162
|
+
// - TryCatch: catch clause is part of try statement
|
|
163
|
+
// - TypeLiteral: { members } in type definitions
|
|
164
|
+
// - EnumValueSet: enum members get same indent as the set itself
|
|
165
|
+
if (tree.kind === J.Kind.TryCatch || tree.kind === JS.Kind.TypeLiteral || tree.kind === J.Kind.EnumValueSet) {
|
|
166
|
+
return parentMyIndent;
|
|
167
|
+
}
|
|
122
168
|
// Only add indent if this element starts on a new line
|
|
123
169
|
// Check both the element's prefix and any Spread marker's prefix
|
|
124
170
|
const hasNewline = this.prefixContainsNewline(tree);
|
|
125
171
|
if (!hasNewline) {
|
|
126
172
|
// Special case for JSX: children of JsxTag don't have newlines in their prefix
|
|
127
|
-
// (newlines are in text Literal nodes), but
|
|
128
|
-
if (this.
|
|
129
|
-
return parentMyIndent + this.
|
|
173
|
+
// (newlines are in text Literal nodes), but JSX children should still get block indent
|
|
174
|
+
if (this.isJsxChildElement(tree)) {
|
|
175
|
+
return parentMyIndent + this.indentSize;
|
|
130
176
|
}
|
|
131
177
|
return parentMyIndent;
|
|
132
178
|
}
|
|
133
179
|
// Add indent for block children or continuation
|
|
134
|
-
return parentMyIndent + this.
|
|
180
|
+
return parentMyIndent + this.indentSize;
|
|
135
181
|
}
|
|
136
182
|
|
|
137
183
|
private prefixContainsNewline(tree: J): boolean {
|
|
138
|
-
// Check the element
|
|
139
|
-
if (tree.prefix
|
|
140
|
-
tree.prefix?.comments?.some(c => c.suffix.includes("\n"))) {
|
|
184
|
+
// Check if the element starts on a new line (only the last whitespace matters)
|
|
185
|
+
if (tree.prefix && lastWhitespace(tree.prefix).includes("\n")) {
|
|
141
186
|
return true;
|
|
142
187
|
}
|
|
143
188
|
// For elements with Spread marker, check the Spread marker's prefix
|
|
@@ -148,9 +193,11 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
148
193
|
return false;
|
|
149
194
|
}
|
|
150
195
|
|
|
151
|
-
private
|
|
152
|
-
// Check if this is a
|
|
153
|
-
|
|
196
|
+
private isJsxChildElement(tree: J): boolean {
|
|
197
|
+
// Check if this is a JSX child element whose parent is a JsxTag
|
|
198
|
+
// JSX children (JsxTag, JsxEmbeddedExpression) don't have newlines in their own prefix
|
|
199
|
+
// (newlines are in text Literal nodes), but they should still get block indent
|
|
200
|
+
if (tree.kind !== JS.Kind.JsxTag && tree.kind !== JS.Kind.JsxEmbeddedExpression) {
|
|
154
201
|
return false;
|
|
155
202
|
}
|
|
156
203
|
const parentTree = this.cursor.parentTree();
|
|
@@ -162,6 +209,8 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
162
209
|
case J.Kind.Block:
|
|
163
210
|
case J.Kind.Case:
|
|
164
211
|
case JS.Kind.JsxTag:
|
|
212
|
+
case JS.Kind.TypeLiteral:
|
|
213
|
+
case J.Kind.EnumValueSet:
|
|
165
214
|
return 'block';
|
|
166
215
|
case JS.Kind.CompilationUnit:
|
|
167
216
|
return 'align';
|
|
@@ -175,17 +224,40 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
175
224
|
this.cursor?.root.messages.set("stop", true);
|
|
176
225
|
}
|
|
177
226
|
|
|
178
|
-
|
|
179
|
-
|
|
227
|
+
// If parent has chainedIndentContext but no indentContext yet, we're exiting a chain element
|
|
228
|
+
// Set indentContext on parent = [chainedIndent + indentSize, chainedIndentKind]
|
|
229
|
+
// This only happens once at the innermost element; subsequent parents will already have indentContext
|
|
230
|
+
const parentChainedContext = this.cursor.parent?.messages.get("chainedIndentContext") as IndentContext | undefined;
|
|
231
|
+
const parentHasIndentContext = this.cursor.parent?.messages.has("indentContext");
|
|
232
|
+
if (parentChainedContext !== undefined && !parentHasIndentContext) {
|
|
233
|
+
const [chainedIndent, chainedIndentKind] = parentChainedContext;
|
|
234
|
+
this.cursor.parent!.messages.set("indentContext", [chainedIndent + this.indentSize, chainedIndentKind] as IndentContext);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const indentContext = this.cursor.messages.get("indentContext") as IndentContext | undefined;
|
|
238
|
+
if (indentContext === undefined) {
|
|
180
239
|
return tree;
|
|
181
240
|
}
|
|
241
|
+
let [myIndent] = indentContext;
|
|
242
|
+
|
|
243
|
+
// For chain-start MethodInvocations, the prefix contains whitespace before the chain BASE
|
|
244
|
+
// Use chainedIndentContext (the original indent) for the prefix, not the continuation indent
|
|
245
|
+
const chainedContext = this.cursor.messages.get("chainedIndentContext") as IndentContext | undefined;
|
|
246
|
+
if (chainedContext !== undefined && tree.kind === J.Kind.MethodInvocation) {
|
|
247
|
+
const mi = tree as J.MethodInvocation;
|
|
248
|
+
if (mi.select && mi.select.after.whitespace.includes("\n")) {
|
|
249
|
+
// This is a chain-start - use original indent for prefix normalization
|
|
250
|
+
myIndent = chainedContext[0];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
182
253
|
|
|
183
254
|
let result = tree;
|
|
255
|
+
const indentStr = this.indentString(myIndent);
|
|
184
256
|
|
|
185
257
|
// Check if the element has a Spread marker - if so, normalize its prefix instead
|
|
186
258
|
const spreadMarker = result.markers?.markers?.find(m => m.kind === JS.Markers.Spread) as { prefix: J.Space } | undefined;
|
|
187
259
|
if (spreadMarker && spaceContainsNewline(spreadMarker.prefix)) {
|
|
188
|
-
const normalizedPrefix = normalizeSpaceIndent(spreadMarker.prefix,
|
|
260
|
+
const normalizedPrefix = normalizeSpaceIndent(spreadMarker.prefix, indentStr);
|
|
189
261
|
if (normalizedPrefix !== spreadMarker.prefix) {
|
|
190
262
|
result = produce(result, draft => {
|
|
191
263
|
const spreadIdx = draft.markers.markers.findIndex(m => m.kind === JS.Markers.Spread);
|
|
@@ -196,7 +268,7 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
196
268
|
}
|
|
197
269
|
} else if (result.prefix && spaceContainsNewline(result.prefix)) {
|
|
198
270
|
// Normalize the entire prefix space including comment suffixes
|
|
199
|
-
const normalizedPrefix = normalizeSpaceIndent(result.prefix,
|
|
271
|
+
const normalizedPrefix = normalizeSpaceIndent(result.prefix, indentStr);
|
|
200
272
|
if (normalizedPrefix !== result.prefix) {
|
|
201
273
|
result = produce(result, draft => {
|
|
202
274
|
draft.prefix = normalizedPrefix;
|
|
@@ -218,21 +290,22 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
218
290
|
return parentTree !== undefined && parentTree.value.kind === JS.Kind.JsxTag;
|
|
219
291
|
}
|
|
220
292
|
|
|
221
|
-
private normalizeJsxTextContent(literal: J.Literal, myIndent:
|
|
293
|
+
private normalizeJsxTextContent(literal: J.Literal, myIndent: number): J.Literal {
|
|
222
294
|
if (!literal.valueSource || !literal.valueSource.includes("\n")) {
|
|
223
295
|
return literal;
|
|
224
296
|
}
|
|
225
297
|
|
|
226
298
|
// Check if this literal is the last child of a JsxTag - if so, its trailing whitespace
|
|
227
299
|
// should use the parent tag's indent, not the content indent
|
|
228
|
-
const
|
|
300
|
+
const parentContext = this.cursor.parentTree()!.messages.get("indentContext") as IndentContext | undefined;
|
|
301
|
+
const parentIndent = parentContext?.[0];
|
|
229
302
|
const isLastChild = parentIndent !== undefined && this.isLastChildOfJsxTag(literal);
|
|
230
303
|
|
|
231
304
|
// For JSX text content, the newline is in the value, not the prefix.
|
|
232
305
|
// Since the content IS effectively on a new line, it should get block child indent.
|
|
233
306
|
// myIndent is the parent's indent (because Literal prefix has no newline),
|
|
234
|
-
// so we need to add
|
|
235
|
-
const contentIndent = myIndent + this.
|
|
307
|
+
// so we need to add indentSize for content lines.
|
|
308
|
+
const contentIndent = myIndent + this.indentSize;
|
|
236
309
|
|
|
237
310
|
// Split by newlines and normalize each line's indentation
|
|
238
311
|
const lines = literal.valueSource.split('\n');
|
|
@@ -251,17 +324,17 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
251
324
|
// Line has only whitespace (or is empty)
|
|
252
325
|
if (isLastChild && i === lines.length - 1) {
|
|
253
326
|
// Trailing whitespace of last child - use parent indent for closing tag alignment
|
|
254
|
-
result.push(parentIndent!);
|
|
327
|
+
result.push(this.indentString(parentIndent!));
|
|
255
328
|
} else if (i < lines.length - 1) {
|
|
256
329
|
// Empty line in the middle (followed by more lines) - keep empty
|
|
257
330
|
result.push('');
|
|
258
331
|
} else {
|
|
259
332
|
// Trailing whitespace of non-last-child - add content indent
|
|
260
|
-
result.push(contentIndent);
|
|
333
|
+
result.push(this.indentString(contentIndent));
|
|
261
334
|
}
|
|
262
335
|
} else {
|
|
263
336
|
// Line has content - add proper indent
|
|
264
|
-
result.push(contentIndent + content);
|
|
337
|
+
result.push(this.indentString(contentIndent) + content);
|
|
265
338
|
}
|
|
266
339
|
}
|
|
267
340
|
|
|
@@ -287,13 +360,13 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
287
360
|
return false;
|
|
288
361
|
}
|
|
289
362
|
|
|
290
|
-
private normalizeBlockEnd(block: J.Block, myIndent:
|
|
363
|
+
private normalizeBlockEnd(block: J.Block, myIndent: number): J.Block {
|
|
291
364
|
const effectiveLastWs = lastWhitespace(block.end);
|
|
292
365
|
if (!effectiveLastWs.includes("\n")) {
|
|
293
366
|
return block;
|
|
294
367
|
}
|
|
295
368
|
return produce(block, draft => {
|
|
296
|
-
draft.end = replaceLastWhitespace(draft.end, ws => replaceIndentAfterLastNewline(ws, myIndent));
|
|
369
|
+
draft.end = replaceLastWhitespace(draft.end, ws => replaceIndentAfterLastNewline(ws, this.indentString(myIndent)));
|
|
297
370
|
});
|
|
298
371
|
}
|
|
299
372
|
|
|
@@ -302,7 +375,7 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
302
375
|
this.cursor = new Cursor(container, this.cursor);
|
|
303
376
|
|
|
304
377
|
// Pre-visit hook: set up cursor messages
|
|
305
|
-
this.preVisitContainer(
|
|
378
|
+
this.preVisitContainer();
|
|
306
379
|
|
|
307
380
|
// Visit children (similar to base visitor but without cursor management)
|
|
308
381
|
let ret = (await produceAsync<J.Container<T>>(container, async draft => {
|
|
@@ -320,58 +393,15 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
320
393
|
return ret;
|
|
321
394
|
}
|
|
322
395
|
|
|
323
|
-
private preVisitContainer
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// This ensures arguments inside method chains like `.select(arg)` inherit the chain's indent level
|
|
328
|
-
// BUT stop at scope boundaries:
|
|
329
|
-
// 1. Blocks - nested code inside callbacks should NOT use outer chainedIndent
|
|
330
|
-
// 2. Other Containers - nested function call arguments should NOT use outer chainedIndent
|
|
331
|
-
for (let c = this.cursor.parent; c; c = c.parent) {
|
|
332
|
-
// Stop searching if we hit a Block (function body, arrow function body, etc.)
|
|
333
|
-
// This prevents chainedIndent from leaking into nested scopes
|
|
334
|
-
if (c.value?.kind === J.Kind.Block) {
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
// Stop searching if we hit another Container (arguments of another function call)
|
|
338
|
-
// This prevents chainedIndent from leaking into nested function calls
|
|
339
|
-
if (c.value?.kind === J.Kind.Container) {
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
const chainedIndent = c.messages.get("chainedIndent") as string | undefined;
|
|
343
|
-
if (chainedIndent !== undefined) {
|
|
344
|
-
myIndent = chainedIndent;
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
this.cursor.messages.set("myIndent", myIndent);
|
|
350
|
-
this.cursor.messages.set("indentKind", 'continuation');
|
|
396
|
+
private preVisitContainer(): void {
|
|
397
|
+
const parentContext = this.cursor.parent?.messages.get("indentContext") as IndentContext | undefined;
|
|
398
|
+
const [myIndent] = parentContext ?? [0, 'continuation'];
|
|
399
|
+
this.cursor.messages.set("indentContext", [myIndent, 'continuation'] as IndentContext);
|
|
351
400
|
}
|
|
352
401
|
|
|
353
402
|
private postVisitContainer<T extends J>(container: J.Container<T>): J.Container<T> {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// Check for chainedIndent for closing delimiter alignment in method chains
|
|
357
|
-
// BUT stop at scope boundaries:
|
|
358
|
-
// 1. Blocks - nested code inside callbacks should NOT use outer chainedIndent
|
|
359
|
-
// 2. Other Containers - nested function call arguments should NOT use outer chainedIndent
|
|
360
|
-
for (let c = this.cursor.parent; c; c = c.parent) {
|
|
361
|
-
// Stop searching if we hit a Block (function body, arrow function body, etc.)
|
|
362
|
-
if (c.value?.kind === J.Kind.Block) {
|
|
363
|
-
break;
|
|
364
|
-
}
|
|
365
|
-
// Stop searching if we hit another Container (arguments of another function call)
|
|
366
|
-
if (c.value?.kind === J.Kind.Container) {
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
const chainedIndent = c.messages.get("chainedIndent") as string | undefined;
|
|
370
|
-
if (chainedIndent !== undefined) {
|
|
371
|
-
parentIndent = chainedIndent;
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
403
|
+
const parentContext = this.cursor.parent?.messages.get("indentContext") as IndentContext | undefined;
|
|
404
|
+
const [parentIndent] = parentContext ?? [0, 'continuation'];
|
|
375
405
|
|
|
376
406
|
// Normalize the last element's after whitespace (closing delimiter like `)`)
|
|
377
407
|
// The closing delimiter should align with the parent's indent level
|
|
@@ -380,7 +410,7 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
380
410
|
if (effectiveLastWs.includes("\n")) {
|
|
381
411
|
return produce(container, draft => {
|
|
382
412
|
const lastDraft = draft.elements[draft.elements.length - 1];
|
|
383
|
-
lastDraft.after = replaceLastWhitespace(lastDraft.after, ws => replaceIndentAfterLastNewline(ws, parentIndent));
|
|
413
|
+
lastDraft.after = replaceLastWhitespace(lastDraft.after, ws => replaceIndentAfterLastNewline(ws, this.indentString(parentIndent)));
|
|
384
414
|
});
|
|
385
415
|
}
|
|
386
416
|
}
|
|
@@ -419,24 +449,21 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
419
449
|
}
|
|
420
450
|
|
|
421
451
|
private preVisitLeftPadded<T extends J | J.Space | number | string | boolean>(left: J.LeftPadded<T>): void {
|
|
422
|
-
|
|
423
|
-
const parentIndent =
|
|
452
|
+
const parentContext = this.cursor.parent?.messages.get("indentContext") as IndentContext | undefined;
|
|
453
|
+
const [parentIndent, parentIndentKind] = parentContext ?? [0, 'continuation'];
|
|
424
454
|
const hasNewline = left.before.whitespace.includes("\n");
|
|
425
455
|
|
|
426
456
|
// Check if parent is a Binary in align mode - if so, don't add continuation indent
|
|
427
|
-
// (Binary sets indentKind='align' when it's already on a continuation line)
|
|
428
457
|
const parentValue = this.cursor.parent?.value;
|
|
429
|
-
const parentIndentKind = this.cursor.parent?.messages.get("indentKind");
|
|
430
458
|
const shouldAlign = parentValue?.kind === J.Kind.Binary && parentIndentKind === 'align';
|
|
431
459
|
|
|
432
460
|
// Compute myIndent INCLUDING continuation if applicable
|
|
433
|
-
// This ensures child elements see the correct parent indent
|
|
434
461
|
let myIndent = parentIndent;
|
|
435
462
|
if (hasNewline && !shouldAlign) {
|
|
436
|
-
myIndent = parentIndent + this.
|
|
463
|
+
myIndent = parentIndent + this.indentSize;
|
|
437
464
|
}
|
|
438
465
|
|
|
439
|
-
this.cursor.messages.set("
|
|
466
|
+
this.cursor.messages.set("indentContext", [myIndent, 'continuation'] as IndentContext);
|
|
440
467
|
this.cursor.messages.set("hasNewline", hasNewline);
|
|
441
468
|
}
|
|
442
469
|
|
|
@@ -452,10 +479,11 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
452
479
|
return left;
|
|
453
480
|
}
|
|
454
481
|
|
|
455
|
-
// Use the
|
|
456
|
-
const
|
|
482
|
+
// Use the indent we computed in preVisitLeftPadded (which includes continuation if applicable)
|
|
483
|
+
const context = this.cursor.messages.get("indentContext") as IndentContext | undefined;
|
|
484
|
+
const [targetIndent] = context ?? [0, 'continuation'];
|
|
457
485
|
return produce(left, draft => {
|
|
458
|
-
draft.before.whitespace = replaceIndentAfterLastNewline(draft.before.whitespace, targetIndent);
|
|
486
|
+
draft.before.whitespace = replaceIndentAfterLastNewline(draft.before.whitespace, this.indentString(targetIndent));
|
|
459
487
|
});
|
|
460
488
|
}
|
|
461
489
|
|
|
@@ -488,74 +516,44 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
488
516
|
}
|
|
489
517
|
|
|
490
518
|
private preVisitRightPadded<T extends J | boolean>(right: J.RightPadded<T>): void {
|
|
491
|
-
|
|
492
|
-
const parentIndent =
|
|
493
|
-
const parentIndentKind = this.cursor.parent?.messages.get("indentKind") as string ?? "";
|
|
519
|
+
const parentContext = this.cursor.parent?.messages.get("indentContext") as IndentContext | undefined;
|
|
520
|
+
const [parentIndent, parentIndentKind] = parentContext ?? [0, 'continuation'];
|
|
494
521
|
|
|
495
|
-
// Check if
|
|
496
|
-
|
|
522
|
+
// Check if parent has chainedIndentContext - if so, this is the select of a method chain
|
|
523
|
+
// Propagate chainedIndentContext but do NOT set indentContext
|
|
524
|
+
const parentChainedContext = this.cursor.parent?.messages.get("chainedIndentContext") as IndentContext | undefined;
|
|
525
|
+
if (parentChainedContext !== undefined) {
|
|
526
|
+
this.cursor.messages.set("chainedIndentContext", parentChainedContext);
|
|
527
|
+
// Do NOT set indentContext - child elements will use chainedIndentContext
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
497
530
|
|
|
498
|
-
//
|
|
499
|
-
const
|
|
500
|
-
const
|
|
531
|
+
// Check if Parentheses wraps a Binary expression - if so, let Binary handle its own indent
|
|
532
|
+
const rightPaddedParentKind = this.cursor.parent?.value?.kind;
|
|
533
|
+
const elementKind = (right.element as any)?.kind;
|
|
534
|
+
const isParenthesesWrappingBinary = rightPaddedParentKind === J.Kind.Parentheses &&
|
|
535
|
+
elementKind === J.Kind.Binary;
|
|
501
536
|
|
|
502
537
|
let myIndent = parentIndent;
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
let existingChainedIndent: string | undefined;
|
|
506
|
-
for (let c = this.cursor.parent; c; c = c.parent) {
|
|
507
|
-
existingChainedIndent = c.messages.get("chainedIndent") as string | undefined;
|
|
508
|
-
if (existingChainedIndent !== undefined) {
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
if (existingChainedIndent === undefined) {
|
|
513
|
-
myIndent = parentIndent + this.singleIndent;
|
|
514
|
-
// Set chainedIndent on parent so further chain elements don't stack
|
|
515
|
-
this.cursor.parent?.messages.set("chainedIndent", myIndent);
|
|
516
|
-
} else {
|
|
517
|
-
myIndent = existingChainedIndent;
|
|
518
|
-
}
|
|
519
|
-
} else if (parentIndentKind !== 'align' && isJava(right.element) && this.elementPrefixContainsNewline(right.element as J)) {
|
|
520
|
-
myIndent = parentIndent + this.singleIndent;
|
|
538
|
+
if (!isParenthesesWrappingBinary && parentIndentKind !== 'align' && isJava(right.element) && this.prefixContainsNewline(right.element as J)) {
|
|
539
|
+
myIndent = parentIndent + this.indentSize;
|
|
521
540
|
// For spread elements with newlines, mark continuation as established
|
|
522
|
-
// This allows subsequent elements on the SAME line to inherit the continuation level
|
|
523
541
|
const element = right.element as J;
|
|
524
542
|
if (this.isSpreadElement(element)) {
|
|
525
543
|
this.cursor.parent?.messages.set("continuationIndent", myIndent);
|
|
526
544
|
}
|
|
527
|
-
} else if (isJava(right.element) && !this.
|
|
545
|
+
} else if (isJava(right.element) && !this.prefixContainsNewline(right.element as J)) {
|
|
528
546
|
// Element has no newline - check if a previous sibling established continuation
|
|
529
|
-
const continuationIndent = this.cursor.parent?.messages.get("continuationIndent") as
|
|
547
|
+
const continuationIndent = this.cursor.parent?.messages.get("continuationIndent") as number | undefined;
|
|
530
548
|
if (continuationIndent !== undefined) {
|
|
531
549
|
myIndent = continuationIndent;
|
|
532
550
|
}
|
|
533
551
|
}
|
|
534
552
|
|
|
535
|
-
this.cursor.messages.set("myIndent", myIndent);
|
|
536
553
|
// Set 'align' for most RightPadded elements to prevent double-continuation
|
|
537
|
-
// EXCEPT when Parentheses wraps a Binary expression -
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const elementKind = (right.element as any)?.kind;
|
|
541
|
-
const isParenthesesWrappingBinary = rightPaddedParentKind === J.Kind.Parentheses &&
|
|
542
|
-
elementKind === J.Kind.Binary;
|
|
543
|
-
if (!isParenthesesWrappingBinary) {
|
|
544
|
-
this.cursor.messages.set("indentKind", "align");
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private elementPrefixContainsNewline(element: J): boolean {
|
|
549
|
-
// Check the element's own prefix
|
|
550
|
-
if (lastWhitespace(element.prefix).includes("\n")) {
|
|
551
|
-
return true;
|
|
552
|
-
}
|
|
553
|
-
// Also check for Spread marker's prefix
|
|
554
|
-
const spreadMarker = element.markers?.markers?.find(m => m.kind === JS.Markers.Spread) as { prefix: J.Space } | undefined;
|
|
555
|
-
if (spreadMarker && spaceContainsNewline(spreadMarker.prefix)) {
|
|
556
|
-
return true;
|
|
557
|
-
}
|
|
558
|
-
return false;
|
|
554
|
+
// EXCEPT when Parentheses wraps a Binary expression - use continuation so Binary children align
|
|
555
|
+
const indentKind: IndentKind = isParenthesesWrappingBinary ? 'continuation' : 'align';
|
|
556
|
+
this.cursor.messages.set("indentContext", [myIndent, indentKind] as IndentContext);
|
|
559
557
|
}
|
|
560
558
|
|
|
561
559
|
/**
|
|
@@ -594,7 +592,7 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
594
592
|
private setupAncestorIndents(): void {
|
|
595
593
|
const path: Cursor[] = [];
|
|
596
594
|
let anchorCursor: Cursor | undefined;
|
|
597
|
-
let anchorIndent =
|
|
595
|
+
let anchorIndent = 0;
|
|
598
596
|
|
|
599
597
|
for (let c = this.cursor.parent; c; c = c.parent) {
|
|
600
598
|
path.push(c);
|
|
@@ -605,14 +603,14 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
605
603
|
const idx = ws.lastIndexOf('\n');
|
|
606
604
|
if (idx !== -1) {
|
|
607
605
|
anchorCursor = c;
|
|
608
|
-
anchorIndent = ws.
|
|
606
|
+
anchorIndent = ws.length - idx - 1;
|
|
609
607
|
}
|
|
610
608
|
}
|
|
611
609
|
|
|
612
610
|
if (v.kind === JS.Kind.CompilationUnit) {
|
|
613
611
|
if (!anchorCursor) {
|
|
614
612
|
anchorCursor = c;
|
|
615
|
-
anchorIndent =
|
|
613
|
+
anchorIndent = 0;
|
|
616
614
|
}
|
|
617
615
|
break;
|
|
618
616
|
}
|
|
@@ -628,8 +626,7 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
628
626
|
const savedCursor = this.cursor;
|
|
629
627
|
this.cursor = c;
|
|
630
628
|
if (c === anchorCursor) {
|
|
631
|
-
c.messages.set("
|
|
632
|
-
c.messages.set("indentKind", this.computeIndentKind(v));
|
|
629
|
+
c.messages.set("indentContext", [anchorIndent, this.computeIndentKind(v)] as IndentContext);
|
|
633
630
|
} else {
|
|
634
631
|
this.setupCursorMessagesForTree(c, v);
|
|
635
632
|
}
|