Skip to content

Commit 25dc786

Browse files
authored
Format logic and parenthesized patterns (#1380)
* Change CodeWriter to make the indentation stack explicit. Prior to this change, CodeWriter maintained an implicit stack where each Piece being formatted has its own indentation level which it can mutate. When the Piece is done, whatever amount of relative indentation it set is discarded. This commit replaces that with an explicit stack that Pieces manipulate by pushing and popping indentation levels onto it. Pieces that don't care about indentation don't affect the stack at all. Pieces that need multiple levels of indentation (like constructor initializers) can push and pop multiple times. * Use an explicit stack for "allow newlines". As with the previous commit, instead of each Piece implicitly having it's own context where this is stored, require pieces to explicitly push and pop regions where newlines are allowed or disallowed. With that change, there's almost nothing left in the _Options class, so eliminate that too. * Refactor ChainPiece.format(). I think it's clearer to handle each state separately. * Format logic patterns. In order to get infix patterns (and split chain constant patterns, and others) indenting correctly in if-case statements, I had to introduce a notion of "collapsible" indent. In this PR, it's only used for the indentation after if-case, but it might be useful elsewhere (for example, with `=>` function bodies). * Fix incorrect indent stack popping.
1 parent ed9f78c commit 25dc786

20 files changed

+396
-181
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 92 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ import 'solution_cache.dart';
2121
class CodeWriter {
2222
final int _pageWidth;
2323

24-
/// The number of spaces of leading indentation at the beginning of each line
25-
/// independent of indentation created by pieces being written.
26-
final int _leadingIndent;
27-
2824
/// Previously cached formatted subtrees.
2925
final SolutionCache _cache;
3026

@@ -50,17 +46,23 @@ class CodeWriter {
5046
/// The number of characters in the line currently being written.
5147
int _column = 0;
5248

53-
/// The stack of state for each [Piece] being formatted.
54-
///
55-
/// For each piece being formatted from a call to [format()], we keep track of
56-
/// things like indentation and nesting levels. Pieces recursively format
57-
/// their children. When they do, we push new values onto this stack. When a
58-
/// piece is done (a call to [format()] returns), we pop the corresponding
59-
/// state off the stack.
49+
/// The stack indentation levels.
6050
///
61-
/// This is used to increase the cumulative nesting as we recurse into pieces
62-
/// and then unwind that as child pieces are completed.
63-
final List<_PieceOptions> _options = [];
51+
/// Each entry in the stack is the absolute number of spaces of leading
52+
/// indentation that should be written when beginning a new line to account
53+
/// for block nesting, expression wrapping, constructor initializers, etc.
54+
final List<_Indent> _indentStack = [];
55+
56+
/// The stack of regions created by pairs of calls to [pushAllowNewlines()]
57+
/// and [popAllowNewlines()].
58+
final List<bool> _allowNewlineStack = [true];
59+
60+
/// Whether any newlines have been written during the [_currentPiece] being
61+
/// formatted.
62+
bool _hadNewline = false;
63+
64+
/// The current innermost piece being formatted by a call to [format()].
65+
Piece? _currentPiece;
6466

6567
/// Whether we have already found the first line where whose piece should be
6668
/// used to expand further solutions.
@@ -94,11 +96,15 @@ class CodeWriter {
9496
/// solution if the line ends up overflowing.
9597
final List<Piece> _currentUnsolvedPieces = [];
9698

97-
CodeWriter(
98-
this._pageWidth, this._leadingIndent, this._cache, this._solution) {
99+
/// [leadingIndent] is the number of spaces of leading indentation at the
100+
/// beginning of each line independent of indentation created by pieces being
101+
/// written.
102+
CodeWriter(this._pageWidth, int leadingIndent, this._cache, this._solution) {
103+
_indentStack.add(_Indent(leadingIndent, 0));
104+
99105
// Write the leading indent before the first line.
100-
_buffer.write(' ' * _leadingIndent);
101-
_column = _leadingIndent;
106+
_buffer.write(' ' * leadingIndent);
107+
_column = leadingIndent;
102108
}
103109

104110
/// Returns the final formatted text and the next piece that can be expanded
@@ -135,35 +141,60 @@ class CodeWriter {
135141
}
136142
}
137143

138-
/// Sets the number of spaces of indentation for code written by the current
139-
/// piece to [indent], relative to the indentation of the surrounding piece.
144+
/// Increases the number of spaces of indentation by [indent] relative to the
145+
/// current amount of indentation.
140146
///
141-
/// Replaces any previous indentation set by this piece.
142-
// TODO(tall): Add another API that adds/subtracts existing indentation.
143-
void setIndent(int indent) {
144-
var parentIndent = _leadingIndent;
145-
146-
// If there is a surrounding Piece, then set the indent relative to that
147-
// piece's current indentation.
148-
if (_options.length > 1) {
149-
parentIndent = _options[_options.length - 2].indent;
147+
/// If [canCollapse] is `true`, then the new [indent] spaces of indentation
148+
/// are "collapsible". This means that further calls to [pushIndent()] will
149+
/// merge their indentation with [indent] and not increase the visible
150+
/// indentation until more than [indent] spaces of indentation have been
151+
/// increased.
152+
void pushIndent(int indent, {bool canCollapse = false}) {
153+
var parentIndent = _indentStack.last.indent;
154+
var parentCollapse = _indentStack.last.collapsible;
155+
156+
if (canCollapse) {
157+
// Increase the indent and the collapsible indent.
158+
_indentStack.add(_Indent(parentIndent + indent, parentCollapse + indent));
159+
} else if (parentCollapse > indent) {
160+
// All new indent is collapsed with the existing collapsible indent.
161+
_indentStack.add(_Indent(parentIndent, parentCollapse - indent));
162+
} else {
163+
// Use up the collapsible indent (if any) and then indent by the rest.
164+
indent -= parentCollapse;
165+
_indentStack.add(_Indent(parentIndent + indent, 0));
150166
}
167+
}
168+
169+
/// Discards the indentation change from the last call to [pushIndent()].
170+
void popIndent() {
171+
_indentStack.removeLast();
172+
}
173+
174+
/// Begins a region of formatting where newlines are allowed if [allow] is
175+
/// `true` or prohibited otherwise.
176+
///
177+
/// If a newline is written while the top of the stack is `false`, the entire
178+
/// solution is considered invalid and gets discarded.
179+
///
180+
/// The region is ended by a corresponding call to [popAllowNewlines()].
181+
void pushAllowNewlines(bool allow) {
182+
_allowNewlineStack.add(allow);
183+
}
151184

152-
_options.last.indent = parentIndent + indent;
185+
/// Ends the region begun by the most recent call to [pushAllowNewlines()].
186+
void popAllowNewlines() {
187+
_allowNewlineStack.removeLast();
153188
}
154189

155190
/// Inserts a newline if [condition] is true.
156191
///
157192
/// If [space] is `true` and [condition] is `false`, writes a space.
158193
///
159194
/// If [blank] is `true`, writes an extra newline to produce a blank line.
160-
///
161-
/// If [indent] is given, sets the amount of block-level indentation for this
162-
/// and all subsequent newlines to [indent].
163-
void splitIf(bool condition,
164-
{bool space = true, bool blank = false, int? indent}) {
195+
void splitIf(bool condition, {bool space = true, bool blank = false}) {
165196
if (condition) {
166-
newline(blank: blank, indent: indent);
197+
newline(blank: blank);
167198
} else if (space) {
168199
this.space();
169200
}
@@ -178,15 +209,10 @@ class CodeWriter {
178209
///
179210
/// If [blank] is `true`, writes an extra newline to produce a blank line.
180211
///
181-
/// If [indent] is given, set the indentation of the new line (and all
182-
/// subsequent lines) to that indentation relative to the containing piece.
183-
///
184212
/// If [flushLeft] is `true`, then the new line begins at column 1 and ignores
185213
/// any surrounding indentation. This is used for multi-line block comments
186214
/// and multi-line strings.
187-
void newline({bool blank = false, int? indent, bool flushLeft = false}) {
188-
if (indent != null) setIndent(indent);
189-
215+
void newline({bool blank = false, bool flushLeft = false}) {
190216
whitespace(blank ? Whitespace.blankLine : Whitespace.newline,
191217
flushLeft: flushLeft);
192218
}
@@ -203,18 +229,12 @@ class CodeWriter {
203229
void whitespace(Whitespace whitespace, {bool flushLeft = false}) {
204230
if (whitespace case Whitespace.newline || Whitespace.blankLine) {
205231
_handleNewline();
206-
_pendingIndent = flushLeft ? 0 : _options.last.indent;
232+
_pendingIndent = flushLeft ? 0 : _indentStack.last.indent;
207233
}
208234

209235
_pendingWhitespace = _pendingWhitespace.collapse(whitespace);
210236
}
211237

212-
/// Sets whether newlines are allowed to occur from this point on for the
213-
/// current piece.
214-
void setAllowNewlines(bool allowed) {
215-
_options.last.allowNewlines = allowed;
216-
}
217-
218238
/// Format [piece] and insert the result into the code being written and
219239
/// returned by [finish()].
220240
///
@@ -264,24 +284,30 @@ class CodeWriter {
264284

265285
/// Format [piece] writing directly into this [CodeWriter].
266286
void _formatInline(Piece piece) {
267-
_options.add(_PieceOptions(
268-
piece,
269-
_options.lastOrNull?.indent ?? _leadingIndent,
270-
_options.lastOrNull?.allowNewlines ?? true));
287+
// Begin a new formatting context for this child.
288+
var previousPiece = _currentPiece;
289+
_currentPiece = piece;
290+
291+
var previousHadNewline = _hadNewline;
292+
_hadNewline = false;
271293

272294
var isUnsolved =
273295
!_solution.isBound(piece) && piece.additionalStates.isNotEmpty;
274296
if (isUnsolved) _currentUnsolvedPieces.add(piece);
275297

298+
// Format the child piece.
276299
piece.format(this, _solution.pieceState(piece));
277300

301+
// Restore the surrounding piece's context.
278302
if (isUnsolved) _currentUnsolvedPieces.removeLast();
279303

280-
var childOptions = _options.removeLast();
304+
var childHadNewline = _hadNewline;
305+
_hadNewline = previousHadNewline;
306+
307+
_currentPiece = previousPiece;
281308

282-
// If the child [piece] contains a newline then this one transitively
283-
// does.
284-
if (childOptions.hasNewline && _options.isNotEmpty) _handleNewline();
309+
// If the child contained a newline then the parent transitively does.
310+
if (childHadNewline && _currentPiece != null) _handleNewline();
285311
}
286312

287313
/// Sets [selectionStart] to be [start] code units into the output.
@@ -301,11 +327,11 @@ class CodeWriter {
301327
/// If this occurs in a place where newlines are prohibited, then invalidates
302328
/// the solution.
303329
void _handleNewline() {
304-
if (!_options.last.allowNewlines) _solution.invalidate(_options.last.piece);
330+
if (!_allowNewlineStack.last) _solution.invalidate(_currentPiece!);
305331

306332
// Note that this piece contains a newline so that we can propagate that
307333
// up to containing pieces too.
308-
_options.last.hasNewline = true;
334+
_hadNewline = true;
309335
}
310336

311337
/// Write any pending whitespace.
@@ -387,24 +413,13 @@ enum Whitespace {
387413
};
388414
}
389415

390-
/// The mutable state local to a single piece being formatted.
391-
class _PieceOptions {
392-
/// The piece being formatted with these options.
393-
final Piece piece;
394-
395-
/// The absolute number of spaces of leading indentation coming from
396-
/// block-like structure or explicit extra indentation (aligning constructor
397-
/// initializers, `show` clauses, etc.).
398-
int indent;
399-
400-
/// Whether newlines are allowed to occur.
401-
///
402-
/// If a newline is written while this is `false`, the entire solution is
403-
/// considered invalid and gets discarded.
404-
bool allowNewlines;
416+
/// A level of indentation in the indentation stack.
417+
class _Indent {
418+
/// The total number of spaces of indentation.
419+
final int indent;
405420

406-
/// Whether any newlines have occurred in this piece or any of its children.
407-
bool hasNewline = false;
421+
/// How many spaces of [indent] can be collapsed with further indentation.
422+
final int collapsible;
408423

409-
_PieceOptions(this.piece, this.indent, this.allowNewlines);
424+
_Indent(this.indent, this.collapsible);
410425
}

lib/src/constants.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,24 @@ class Indent {
7979

8080
/// The ":" on a wrapped constructor initialization list.
8181
static const constructorInitializer = 4;
82+
83+
/// A wrapped constructor initializer after the first one when the parameter
84+
/// list does not have optional or named parameters, like:
85+
///
86+
/// Constructor(
87+
/// parameter,
88+
/// ) : first,
89+
/// second;
90+
/// ^^ This indentation.
91+
static const initializer = 2;
92+
93+
/// A wrapped constructor initializer after the first one when the parameter
94+
/// list has optional or named parameters, like:
95+
///
96+
/// Constructor([
97+
/// parameter,
98+
/// ]) : first,
99+
/// second;
100+
/// ^^^ This indentation.
101+
static const initializerWithOptionalParameter = 3;
82102
}

lib/src/front_end/ast_node_visitor.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,12 +1168,26 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
11681168

11691169
@override
11701170
Piece visitLogicalAndPattern(LogicalAndPattern node) {
1171-
throw UnimplementedError();
1171+
return createInfixChain<LogicalAndPattern>(
1172+
node,
1173+
precedence: node.operator.type.precedence,
1174+
(expression) => (
1175+
expression.leftOperand,
1176+
expression.operator,
1177+
expression.rightOperand
1178+
));
11721179
}
11731180

11741181
@override
11751182
Piece visitLogicalOrPattern(LogicalOrPattern node) {
1176-
throw UnimplementedError();
1183+
return createInfixChain<LogicalOrPattern>(
1184+
node,
1185+
precedence: node.operator.type.precedence,
1186+
(expression) => (
1187+
expression.leftOperand,
1188+
expression.operator,
1189+
expression.rightOperand
1190+
));
11771191
}
11781192

11791193
@override
@@ -1316,7 +1330,11 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
13161330

13171331
@override
13181332
Piece visitParenthesizedPattern(ParenthesizedPattern node) {
1319-
throw UnimplementedError();
1333+
return buildPiece((b) {
1334+
b.token(node.leftParenthesis);
1335+
b.visit(node.pattern);
1336+
b.token(node.rightParenthesis);
1337+
});
13201338
}
13211339

13221340
@override

lib/src/piece/adjacent_strings.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ class AdjacentStringsPiece extends Piece {
2020

2121
@override
2222
void format(CodeWriter writer, State state) {
23-
if (_indent) writer.setIndent(Indent.expression);
23+
if (_indent) writer.pushIndent(Indent.expression);
2424

2525
for (var i = 0; i < _strings.length; i++) {
2626
if (i > 0) writer.newline();
2727
writer.format(_strings[i]);
2828
}
29+
30+
if (_indent) writer.popIndent();
2931
}
3032

3133
@override

lib/src/piece/assign.dart

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,28 @@ class AssignPiece extends Piece {
105105
void format(CodeWriter writer, State state) {
106106
// A split in either child piece forces splitting at assignment operator
107107
// unless specifically allowed.
108-
if (!_allowInnerSplit && state == State.unsplit) {
109-
writer.setAllowNewlines(false);
110-
}
108+
writer.pushAllowNewlines(_allowInnerSplit || state != State.unsplit);
111109

112110
// Don't indent a split delimited expression.
113-
if (state != State.unsplit) writer.setIndent(Indent.expression);
111+
if (state != State.unsplit) writer.pushIndent(Indent.expression);
114112

115113
writer.format(target);
116114
writer.splitIf(state == _atOperator);
117115

118116
// We need extra indentation when there's no inner splitting of the value.
119117
if (!_allowInnerSplit && _indentInValue) {
120-
writer.setIndent(Indent.expression * 2);
118+
writer.pushIndent(Indent.expression, canCollapse: true);
121119
}
120+
122121
writer.format(value);
122+
123+
if (!_allowInnerSplit && _indentInValue) {
124+
writer.popIndent();
125+
}
126+
127+
if (state != State.unsplit) writer.popIndent();
128+
129+
writer.popAllowNewlines();
123130
}
124131

125132
@override

0 commit comments

Comments
 (0)