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.
diff --git a/lib/src/back_end/code_writer.dart b/lib/src/back_end/code_writer.dart
index d26a52f..4be9b1a 100644
--- a/lib/src/back_end/code_writer.dart
+++ b/lib/src/back_end/code_writer.dart
@@ -21,10 +21,6 @@
 class CodeWriter {
   final int _pageWidth;
 
-  /// The number of spaces of leading indentation at the beginning of each line
-  /// independent of indentation created by pieces being written.
-  final int _leadingIndent;
-
   /// Previously cached formatted subtrees.
   final SolutionCache _cache;
 
@@ -50,17 +46,23 @@
   /// The number of characters in the line currently being written.
   int _column = 0;
 
-  /// The stack of state for each [Piece] being formatted.
+  /// The stack indentation levels.
   ///
-  /// For each piece being formatted from a call to [format()], we keep track of
-  /// things like indentation and nesting levels. Pieces recursively format
-  /// their children. When they do, we push new values onto this stack. When a
-  /// piece is done (a call to [format()] returns), we pop the corresponding
-  /// state off the stack.
-  ///
-  /// This is used to increase the cumulative nesting as we recurse into pieces
-  /// and then unwind that as child pieces are completed.
-  final List<_PieceOptions> _options = [];
+  /// Each entry in the stack is the absolute number of spaces of leading
+  /// indentation that should be written when beginning a new line to account
+  /// for block nesting, expression wrapping, constructor initializers, etc.
+  final List<_Indent> _indentStack = [];
+
+  /// The stack of regions created by pairs of calls to [pushAllowNewlines()]
+  /// and [popAllowNewlines()].
+  final List<bool> _allowNewlineStack = [true];
+
+  /// Whether any newlines have been written during the [_currentPiece] being
+  /// formatted.
+  bool _hadNewline = false;
+
+  /// The current innermost piece being formatted by a call to [format()].
+  Piece? _currentPiece;
 
   /// Whether we have already found the first line where whose piece should be
   /// used to expand further solutions.
@@ -94,11 +96,15 @@
   /// solution if the line ends up overflowing.
   final List<Piece> _currentUnsolvedPieces = [];
 
-  CodeWriter(
-      this._pageWidth, this._leadingIndent, this._cache, this._solution) {
+  /// [leadingIndent] is the number of spaces of leading indentation at the
+  /// beginning of each line independent of indentation created by pieces being
+  /// written.
+  CodeWriter(this._pageWidth, int leadingIndent, this._cache, this._solution) {
+    _indentStack.add(_Indent(leadingIndent, 0));
+
     // Write the leading indent before the first line.
-    _buffer.write(' ' * _leadingIndent);
-    _column = _leadingIndent;
+    _buffer.write(' ' * leadingIndent);
+    _column = leadingIndent;
   }
 
   /// Returns the final formatted text and the next piece that can be expanded
@@ -135,21 +141,50 @@
     }
   }
 
-  /// Sets the number of spaces of indentation for code written by the current
-  /// piece to [indent], relative to the indentation of the surrounding piece.
+  /// Increases the number of spaces of indentation by [indent] relative to the
+  /// current amount of indentation.
   ///
-  /// Replaces any previous indentation set by this piece.
-  // TODO(tall): Add another API that adds/subtracts existing indentation.
-  void setIndent(int indent) {
-    var parentIndent = _leadingIndent;
+  /// If [canCollapse] is `true`, then the new [indent] spaces of indentation
+  /// are "collapsible". This means that further calls to [pushIndent()] will
+  /// merge their indentation with [indent] and not increase the visible
+  /// indentation until more than [indent] spaces of indentation have been
+  /// increased.
+  void pushIndent(int indent, {bool canCollapse = false}) {
+    var parentIndent = _indentStack.last.indent;
+    var parentCollapse = _indentStack.last.collapsible;
 
-    // If there is a surrounding Piece, then set the indent relative to that
-    // piece's current indentation.
-    if (_options.length > 1) {
-      parentIndent = _options[_options.length - 2].indent;
+    if (canCollapse) {
+      // Increase the indent and the collapsible indent.
+      _indentStack.add(_Indent(parentIndent + indent, parentCollapse + indent));
+    } else if (parentCollapse > indent) {
+      // All new indent is collapsed with the existing collapsible indent.
+      _indentStack.add(_Indent(parentIndent, parentCollapse - indent));
+    } else {
+      // Use up the collapsible indent (if any) and then indent by the rest.
+      indent -= parentCollapse;
+      _indentStack.add(_Indent(parentIndent + indent, 0));
     }
+  }
 
-    _options.last.indent = parentIndent + indent;
+  /// Discards the indentation change from the last call to [pushIndent()].
+  void popIndent() {
+    _indentStack.removeLast();
+  }
+
+  /// Begins a region of formatting where newlines are allowed if [allow] is
+  /// `true` or prohibited otherwise.
+  ///
+  /// If a newline is written while the top of the stack is `false`, the entire
+  /// solution is considered invalid and gets discarded.
+  ///
+  /// The region is ended by a corresponding call to [popAllowNewlines()].
+  void pushAllowNewlines(bool allow) {
+    _allowNewlineStack.add(allow);
+  }
+
+  /// Ends the region begun by the most recent call to [pushAllowNewlines()].
+  void popAllowNewlines() {
+    _allowNewlineStack.removeLast();
   }
 
   /// Inserts a newline if [condition] is true.
@@ -157,13 +192,9 @@
   /// If [space] is `true` and [condition] is `false`, writes a space.
   ///
   /// If [blank] is `true`, writes an extra newline to produce a blank line.
-  ///
-  /// If [indent] is given, sets the amount of block-level indentation for this
-  /// and all subsequent newlines to [indent].
-  void splitIf(bool condition,
-      {bool space = true, bool blank = false, int? indent}) {
+  void splitIf(bool condition, {bool space = true, bool blank = false}) {
     if (condition) {
-      newline(blank: blank, indent: indent);
+      newline(blank: blank);
     } else if (space) {
       this.space();
     }
@@ -178,15 +209,10 @@
   ///
   /// If [blank] is `true`, writes an extra newline to produce a blank line.
   ///
-  /// If [indent] is given, set the indentation of the new line (and all
-  /// subsequent lines) to that indentation relative to the containing piece.
-  ///
   /// If [flushLeft] is `true`, then the new line begins at column 1 and ignores
   /// any surrounding indentation. This is used for multi-line block comments
   /// and multi-line strings.
-  void newline({bool blank = false, int? indent, bool flushLeft = false}) {
-    if (indent != null) setIndent(indent);
-
+  void newline({bool blank = false, bool flushLeft = false}) {
     whitespace(blank ? Whitespace.blankLine : Whitespace.newline,
         flushLeft: flushLeft);
   }
@@ -203,18 +229,12 @@
   void whitespace(Whitespace whitespace, {bool flushLeft = false}) {
     if (whitespace case Whitespace.newline || Whitespace.blankLine) {
       _handleNewline();
-      _pendingIndent = flushLeft ? 0 : _options.last.indent;
+      _pendingIndent = flushLeft ? 0 : _indentStack.last.indent;
     }
 
     _pendingWhitespace = _pendingWhitespace.collapse(whitespace);
   }
 
-  /// Sets whether newlines are allowed to occur from this point on for the
-  /// current piece.
-  void setAllowNewlines(bool allowed) {
-    _options.last.allowNewlines = allowed;
-  }
-
   /// Format [piece] and insert the result into the code being written and
   /// returned by [finish()].
   ///
@@ -264,24 +284,30 @@
 
   /// Format [piece] writing directly into this [CodeWriter].
   void _formatInline(Piece piece) {
-    _options.add(_PieceOptions(
-        piece,
-        _options.lastOrNull?.indent ?? _leadingIndent,
-        _options.lastOrNull?.allowNewlines ?? true));
+    // Begin a new formatting context for this child.
+    var previousPiece = _currentPiece;
+    _currentPiece = piece;
+
+    var previousHadNewline = _hadNewline;
+    _hadNewline = false;
 
     var isUnsolved =
         !_solution.isBound(piece) && piece.additionalStates.isNotEmpty;
     if (isUnsolved) _currentUnsolvedPieces.add(piece);
 
+    // Format the child piece.
     piece.format(this, _solution.pieceState(piece));
 
+    // Restore the surrounding piece's context.
     if (isUnsolved) _currentUnsolvedPieces.removeLast();
 
-    var childOptions = _options.removeLast();
+    var childHadNewline = _hadNewline;
+    _hadNewline = previousHadNewline;
 
-    // If the child [piece] contains a newline then this one transitively
-    // does.
-    if (childOptions.hasNewline && _options.isNotEmpty) _handleNewline();
+    _currentPiece = previousPiece;
+
+    // If the child contained a newline then the parent transitively does.
+    if (childHadNewline && _currentPiece != null) _handleNewline();
   }
 
   /// Sets [selectionStart] to be [start] code units into the output.
@@ -301,11 +327,11 @@
   /// If this occurs in a place where newlines are prohibited, then invalidates
   /// the solution.
   void _handleNewline() {
-    if (!_options.last.allowNewlines) _solution.invalidate(_options.last.piece);
+    if (!_allowNewlineStack.last) _solution.invalidate(_currentPiece!);
 
     // Note that this piece contains a newline so that we can propagate that
     // up to containing pieces too.
-    _options.last.hasNewline = true;
+    _hadNewline = true;
   }
 
   /// Write any pending whitespace.
@@ -387,24 +413,13 @@
       };
 }
 
-/// The mutable state local to a single piece being formatted.
-class _PieceOptions {
-  /// The piece being formatted with these options.
-  final Piece piece;
+/// A level of indentation in the indentation stack.
+class _Indent {
+  /// The total number of spaces of indentation.
+  final int indent;
 
-  /// The absolute number of spaces of leading indentation coming from
-  /// block-like structure or explicit extra indentation (aligning constructor
-  /// initializers, `show` clauses, etc.).
-  int indent;
+  /// How many spaces of [indent] can be collapsed with further indentation.
+  final int collapsible;
 
-  /// Whether newlines are allowed to occur.
-  ///
-  /// If a newline is written while this is `false`, the entire solution is
-  /// considered invalid and gets discarded.
-  bool allowNewlines;
-
-  /// Whether any newlines have occurred in this piece or any of its children.
-  bool hasNewline = false;
-
-  _PieceOptions(this.piece, this.indent, this.allowNewlines);
+  _Indent(this.indent, this.collapsible);
 }
diff --git a/lib/src/constants.dart b/lib/src/constants.dart
index be0b6f9..a830adc 100644
--- a/lib/src/constants.dart
+++ b/lib/src/constants.dart
@@ -79,4 +79,24 @@
 
   /// The ":" on a wrapped constructor initialization list.
   static const constructorInitializer = 4;
+
+  /// A wrapped constructor initializer after the first one when the parameter
+  /// list does not have optional or named parameters, like:
+  ///
+  ///     Constructor(
+  ///       parameter,
+  ///     ) : first,
+  ///         second;
+  ///       ^^ This indentation.
+  static const initializer = 2;
+
+  /// A wrapped constructor initializer after the first one when the parameter
+  /// list has optional or named parameters, like:
+  ///
+  ///     Constructor([
+  ///       parameter,
+  ///     ]) : first,
+  ///          second;
+  ///       ^^^ This indentation.
+  static const initializerWithOptionalParameter = 3;
 }
diff --git a/lib/src/front_end/ast_node_visitor.dart b/lib/src/front_end/ast_node_visitor.dart
index 5600b5b..74b454c 100644
--- a/lib/src/front_end/ast_node_visitor.dart
+++ b/lib/src/front_end/ast_node_visitor.dart
@@ -1168,12 +1168,26 @@
 
   @override
   Piece visitLogicalAndPattern(LogicalAndPattern node) {
-    throw UnimplementedError();
+    return createInfixChain<LogicalAndPattern>(
+        node,
+        precedence: node.operator.type.precedence,
+        (expression) => (
+              expression.leftOperand,
+              expression.operator,
+              expression.rightOperand
+            ));
   }
 
   @override
   Piece visitLogicalOrPattern(LogicalOrPattern node) {
-    throw UnimplementedError();
+    return createInfixChain<LogicalOrPattern>(
+        node,
+        precedence: node.operator.type.precedence,
+        (expression) => (
+              expression.leftOperand,
+              expression.operator,
+              expression.rightOperand
+            ));
   }
 
   @override
@@ -1316,7 +1330,11 @@
 
   @override
   Piece visitParenthesizedPattern(ParenthesizedPattern node) {
-    throw UnimplementedError();
+    return buildPiece((b) {
+      b.token(node.leftParenthesis);
+      b.visit(node.pattern);
+      b.token(node.rightParenthesis);
+    });
   }
 
   @override
diff --git a/lib/src/piece/adjacent_strings.dart b/lib/src/piece/adjacent_strings.dart
index 0015c29..ce25158 100644
--- a/lib/src/piece/adjacent_strings.dart
+++ b/lib/src/piece/adjacent_strings.dart
@@ -20,12 +20,14 @@
 
   @override
   void format(CodeWriter writer, State state) {
-    if (_indent) writer.setIndent(Indent.expression);
+    if (_indent) writer.pushIndent(Indent.expression);
 
     for (var i = 0; i < _strings.length; i++) {
       if (i > 0) writer.newline();
       writer.format(_strings[i]);
     }
+
+    if (_indent) writer.popIndent();
   }
 
   @override
diff --git a/lib/src/piece/assign.dart b/lib/src/piece/assign.dart
index 358a581..db3075a 100644
--- a/lib/src/piece/assign.dart
+++ b/lib/src/piece/assign.dart
@@ -105,21 +105,28 @@
   void format(CodeWriter writer, State state) {
     // A split in either child piece forces splitting at assignment operator
     // unless specifically allowed.
-    if (!_allowInnerSplit && state == State.unsplit) {
-      writer.setAllowNewlines(false);
-    }
+    writer.pushAllowNewlines(_allowInnerSplit || state != State.unsplit);
 
     // Don't indent a split delimited expression.
-    if (state != State.unsplit) writer.setIndent(Indent.expression);
+    if (state != State.unsplit) writer.pushIndent(Indent.expression);
 
     writer.format(target);
     writer.splitIf(state == _atOperator);
 
     // We need extra indentation when there's no inner splitting of the value.
     if (!_allowInnerSplit && _indentInValue) {
-      writer.setIndent(Indent.expression * 2);
+      writer.pushIndent(Indent.expression, canCollapse: true);
     }
+
     writer.format(value);
+
+    if (!_allowInnerSplit && _indentInValue) {
+      writer.popIndent();
+    }
+
+    if (state != State.unsplit) writer.popIndent();
+
+    writer.popAllowNewlines();
   }
 
   @override
diff --git a/lib/src/piece/chain.dart b/lib/src/piece/chain.dart
index caf152c..bb5f71c 100644
--- a/lib/src/piece/chain.dart
+++ b/lib/src/piece/chain.dart
@@ -138,53 +138,71 @@
 
   @override
   void format(CodeWriter writer, State state) {
-    // If we split at the ".", then indent all of the calls, like:
-    //
-    //     target
-    //         .call(
-    //           arg,
-    //         );
     switch (state) {
       case State.unsplit:
-        writer.setAllowNewlines(_allowSplitInTarget);
+        _formatTarget(writer);
+
+        writer.pushAllowNewlines(false);
+        for (var i = 0; i < _calls.length; i++) {
+          _formatCall(writer, state, i);
+        }
+        writer.popAllowNewlines();
+
       case _splitAfterProperties:
-        writer.setIndent(_indent);
-        writer.setAllowNewlines(_allowSplitInTarget);
-      case _blockFormatTrailingCall:
-        writer.setAllowNewlines(_allowSplitInTarget);
-      case State.split:
-        writer.setIndent(_indent);
-    }
+        writer.pushIndent(_indent);
+        _formatTarget(writer);
 
-    writer.format(_target);
-
-    for (var i = 0; i < _calls.length; i++) {
-      switch (state) {
-        case State.unsplit:
-          writer.setAllowNewlines(false);
-        case _splitAfterProperties:
-          writer.setAllowNewlines(i >= _leadingProperties);
+        for (var i = 0; i < _calls.length; i++) {
+          writer.pushAllowNewlines(i >= _leadingProperties);
           writer.splitIf(i >= _leadingProperties, space: false);
-        case _blockFormatTrailingCall:
-          writer.setAllowNewlines(i == _blockCallIndex);
-        case State.split:
-          writer.setAllowNewlines(true);
-          writer.newline();
-      }
+          _formatCall(writer, state, i);
+          writer.popAllowNewlines();
+        }
 
-      // If the chain is fully split, then every call except for the last will
-      // be on its own line. If the chain is split after properties, then
-      // every non-property call except the last will be on its own line.
-      var separate = switch (state) {
-        _splitAfterProperties =>
-          i >= _leadingProperties && i < _calls.length - 1,
-        State.split => i < _calls.length - 1,
-        _ => false,
-      };
-      writer.format(_calls[i]._call, separate: separate);
+        writer.popIndent();
+
+      case _blockFormatTrailingCall:
+        _formatTarget(writer);
+
+        for (var i = 0; i < _calls.length; i++) {
+          writer.pushAllowNewlines(i == _blockCallIndex);
+          _formatCall(writer, state, i);
+          writer.popAllowNewlines();
+        }
+
+      case State.split:
+        writer.pushIndent(_indent);
+        writer.format(_target);
+
+        writer.pushAllowNewlines(true);
+        for (var i = 0; i < _calls.length; i++) {
+          writer.newline();
+          _formatCall(writer, state, i);
+        }
+        writer.popAllowNewlines();
+        writer.popIndent();
     }
   }
 
+  void _formatTarget(CodeWriter writer) {
+    writer.pushAllowNewlines(_allowSplitInTarget);
+    writer.format(_target);
+    writer.popAllowNewlines();
+  }
+
+  void _formatCall(CodeWriter writer, State state, int i) {
+    // If the chain is fully split, then every call except for the last will
+    // be on its own line. If the chain is split after properties, then
+    // every non-property call except the last will be on its own line.
+    var separate = switch (state) {
+      _splitAfterProperties => i >= _leadingProperties && i < _calls.length - 1,
+      State.split => i < _calls.length - 1,
+      _ => false,
+    };
+
+    writer.format(_calls[i]._call, separate: separate);
+  }
+
   @override
   void forEachChild(void Function(Piece piece) callback) {
     callback(_target);
diff --git a/lib/src/piece/clause.dart b/lib/src/piece/clause.dart
index 0145f8c..290daf3 100644
--- a/lib/src/piece/clause.dart
+++ b/lib/src/piece/clause.dart
@@ -89,22 +89,28 @@
 
   @override
   void format(CodeWriter writer, State state) {
+    writer.pushIndent(Indent.expression);
+
     for (var clause in _clauses) {
       if (_allowLeadingClause && clause == _clauses.first) {
         // Before the leading clause, only split when in the fully split state.
         // A split inside the first clause forces a split before the keyword.
-        writer.splitIf(state == State.split, indent: Indent.expression);
-        writer.setAllowNewlines(state == State.split);
+        writer.splitIf(state == State.split);
+        writer.pushAllowNewlines(state == State.split);
       } else {
         // For the other clauses (or if there is no leading one), split in the
         // fully split state and any split inside and clause forces all of them
         // to split.
-        writer.setAllowNewlines(state != State.unsplit);
-        writer.splitIf(state != State.unsplit, indent: Indent.expression);
+        writer.pushAllowNewlines(state != State.unsplit);
+        writer.splitIf(state != State.unsplit);
       }
 
       writer.format(clause);
+
+      writer.popAllowNewlines();
     }
+
+    writer.popIndent();
   }
 
   @override
@@ -129,13 +135,17 @@
   @override
   void format(CodeWriter writer, State state) {
     // If any of the parts inside the clause split, split the list.
-    writer.setAllowNewlines(state != State.unsplit);
+    writer.pushAllowNewlines(state != State.unsplit);
+    writer.pushIndent(Indent.expression);
 
     writer.format(_keyword);
     for (var part in _parts) {
-      writer.splitIf(state == State.split, indent: Indent.expression);
+      writer.splitIf(state == State.split);
       writer.format(part);
     }
+
+    writer.popIndent();
+    writer.popAllowNewlines();
   }
 
   @override
diff --git a/lib/src/piece/constructor.dart b/lib/src/piece/constructor.dart
index fbec5d3..51f704d 100644
--- a/lib/src/piece/constructor.dart
+++ b/lib/src/piece/constructor.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 import '../back_end/code_writer.dart';
+import '../constants.dart';
 import 'piece.dart';
 
 /// A constructor declaration.
@@ -129,9 +130,7 @@
   void format(CodeWriter writer, State state) {
     // If there's a newline in the header or parameters (like a line comment
     // after the `)`), then don't allow the initializers to remain unsplit.
-    if (_initializers != null && state == State.unsplit) {
-      writer.setAllowNewlines(false);
-    }
+    writer.pushAllowNewlines(_initializers == null || state != State.unsplit);
 
     writer.format(_header);
     writer.format(_parameters);
@@ -142,26 +141,25 @@
     }
 
     if (_initializers case var initializers?) {
-      writer.setAllowNewlines(state != State.unsplit);
-      writer.splitIf(state == _splitBeforeInitializers, indent: 2);
+      writer.pushIndent(Indent.block);
+      writer.splitIf(state == _splitBeforeInitializers);
 
       writer.format(_initializerSeparator!);
       writer.space();
 
       // Indent subsequent initializers past the `:`.
       if (_hasOptionalParameter && state == _splitBetweenInitializers) {
-        // If the parameter list ends in `]) : init...` then we need to indent
-        // +5 to line up subsequent initializers.
-        writer.setIndent(5);
+        writer.pushIndent(Indent.initializerWithOptionalParameter);
       } else {
-        writer.setIndent(4);
+        writer.pushIndent(Indent.initializer);
       }
 
       writer.format(initializers);
+      writer.popIndent();
+      writer.popIndent();
     }
 
-    writer.setIndent(0);
-    writer.setAllowNewlines(true);
+    writer.popAllowNewlines();
     writer.format(_body);
   }
 
diff --git a/lib/src/piece/for.dart b/lib/src/piece/for.dart
index 3b4aa30..1b60a9a 100644
--- a/lib/src/piece/for.dart
+++ b/lib/src/piece/for.dart
@@ -34,9 +34,7 @@
 
   @override
   void format(CodeWriter writer, State state) {
-    if (!_hasBlockBody && state == State.unsplit) {
-      writer.setAllowNewlines(false);
-    }
+    writer.pushAllowNewlines(_hasBlockBody || state != State.unsplit);
 
     writer.format(_forKeyword);
     writer.space();
@@ -45,10 +43,14 @@
     if (_hasBlockBody) {
       writer.space();
     } else {
-      writer.splitIf(state == State.split, indent: Indent.block);
+      writer.pushIndent(Indent.block);
+      writer.splitIf(state == State.split);
     }
 
     writer.format(_body);
+    if (!_hasBlockBody) writer.popIndent();
+
+    writer.popAllowNewlines();
   }
 
   @override
diff --git a/lib/src/piece/function.dart b/lib/src/piece/function.dart
index 43a5dbc..8f96563 100644
--- a/lib/src/piece/function.dart
+++ b/lib/src/piece/function.dart
@@ -57,14 +57,15 @@
   void format(CodeWriter writer, State state) {
     if (_returnType case var returnType?) {
       // A split inside the return type forces splitting after the return type.
-      writer.setAllowNewlines(state == State.split);
-
+      writer.pushAllowNewlines(state == State.split);
       writer.format(returnType);
+      writer.popAllowNewlines();
 
       // A split in the type parameters or parameters does not force splitting
       // after the return type.
-      writer.setAllowNewlines(true);
+      writer.pushAllowNewlines(true);
       writer.splitIf(state == State.split);
+      writer.popAllowNewlines();
     }
 
     writer.format(_signature);
diff --git a/lib/src/piece/if.dart b/lib/src/piece/if.dart
index df1eb1e..1471a58 100644
--- a/lib/src/piece/if.dart
+++ b/lib/src/piece/if.dart
@@ -53,21 +53,25 @@
       var section = _sections[i];
 
       // A split in the condition forces the branches to split.
-      writer.setAllowNewlines(state == State.split);
+      writer.pushAllowNewlines(state == State.split);
       writer.format(section.header);
 
       if (!section.isBlock) {
-        writer.splitIf(state == State.split, indent: Indent.block);
+        writer.pushIndent(Indent.block);
+        writer.splitIf(state == State.split);
       }
 
       // TODO(perf): Investigate whether it's worth using `separate:` here.
       writer.format(section.statement);
 
       // Reset the indentation for the subsequent `else` or `} else` line.
+      if (!section.isBlock) writer.popIndent();
+
       if (i < _sections.length - 1) {
-        writer.splitIf(state == State.split && !section.isBlock,
-            indent: Indent.none);
+        writer.splitIf(state == State.split && !section.isBlock);
       }
+
+      writer.popAllowNewlines();
     }
   }
 }
diff --git a/lib/src/piece/infix.dart b/lib/src/piece/infix.dart
index 9b07193..a951f20 100644
--- a/lib/src/piece/infix.dart
+++ b/lib/src/piece/infix.dart
@@ -25,9 +25,9 @@
   @override
   void format(CodeWriter writer, State state) {
     if (state == State.unsplit) {
-      writer.setAllowNewlines(false);
+      writer.pushAllowNewlines(false);
     } else {
-      writer.setIndent(Indent.expression);
+      writer.pushIndent(Indent.expression);
     }
 
     for (var i = 0; i < _operands.length; i++) {
@@ -39,6 +39,12 @@
       writer.format(_operands[i], separate: separate);
       if (i < _operands.length - 1) writer.splitIf(state == State.split);
     }
+
+    if (state == State.unsplit) {
+      writer.popAllowNewlines();
+    } else {
+      writer.popIndent();
+    }
   }
 
   @override
diff --git a/lib/src/piece/list.dart b/lib/src/piece/list.dart
index c02ed7d..b82a4c7 100644
--- a/lib/src/piece/list.dart
+++ b/lib/src/piece/list.dart
@@ -110,17 +110,20 @@
   void format(CodeWriter writer, State state) {
     // Format the opening bracket, if there is one.
     if (_before case var before?) {
-      if (_style.splitListIfBeforeSplits && state == State.unsplit) {
-        writer.setAllowNewlines(false);
-      }
+      writer.pushAllowNewlines(
+          !_style.splitListIfBeforeSplits || state != State.unsplit);
 
       writer.format(before);
 
-      if (state == State.unsplit) writer.setAllowNewlines(false);
+      writer.popAllowNewlines();
+      writer.pushAllowNewlines(state != State.unsplit);
+
+      if (state != State.unsplit) {
+        writer.pushIndent(Indent.block);
+      }
 
       // Whitespace after the opening bracket.
       writer.splitIf(state != State.unsplit,
-          indent: Indent.block,
           space: _style.spaceWhenUnsplit && _elements.isNotEmpty);
     }
 
@@ -130,13 +133,14 @@
 
       // Only some elements (usually a single block element) allow newlines
       // when the list itself isn't split.
-      writer.setAllowNewlines(
+      if (_before != null || i > 0) writer.popAllowNewlines();
+      writer.pushAllowNewlines(
           element.allowNewlinesWhenUnsplit || state == State.split);
 
       // If this element allows newlines when the list isn't split, add
       // indentation if it requires it.
       if (state == State.unsplit && element.indentWhenBlockFormatted) {
-        writer.setIndent(Indent.expression);
+        writer.pushIndent(Indent.expression);
       }
 
       // We can format each list item separately if the item is on its own line.
@@ -149,7 +153,7 @@
       writer.format(element, separate: separate);
 
       if (state == State.unsplit && element.indentWhenBlockFormatted) {
-        writer.setIndent(Indent.none);
+        writer.popIndent();
       }
 
       // Write a space or newline between elements.
@@ -163,14 +167,18 @@
 
     // Format the closing bracket, if any.
     if (_after case var after?) {
+      if (state != State.unsplit) writer.popIndent();
+
       // Whitespace before the closing bracket.
       writer.splitIf(state != State.unsplit,
-          indent: Indent.none,
           space: _style.spaceWhenUnsplit && _elements.isNotEmpty);
 
-      writer.setAllowNewlines(true);
+      if (_before != null || _elements.isNotEmpty) writer.popAllowNewlines();
+      writer.pushAllowNewlines(true);
       writer.format(after);
     }
+
+    writer.popAllowNewlines();
   }
 
   @override
diff --git a/lib/src/piece/postfix.dart b/lib/src/piece/postfix.dart
index f0f85f6..bcd2e9a 100644
--- a/lib/src/piece/postfix.dart
+++ b/lib/src/piece/postfix.dart
@@ -28,16 +28,17 @@
 
   @override
   void format(CodeWriter writer, State state) {
-    // If any of the operands split, then force the postfix sequence to split
-    // too.
-    // TODO(tall): This will need to be revisited when we use PostfixPiece for
-    // actual postfix operators where this isn't always desired.
-    if (state == State.unsplit) writer.setAllowNewlines(false);
+    // If any operand splits, then force the postfix sequence to split too.
+    writer.pushAllowNewlines(state == State.split);
+    writer.pushIndent(Indent.expression);
 
     for (var piece in pieces) {
-      writer.splitIf(state == State.split, indent: Indent.expression);
+      writer.splitIf(state == State.split);
       writer.format(piece);
     }
+
+    writer.popIndent();
+    writer.popAllowNewlines();
   }
 
   @override
diff --git a/lib/src/piece/sequence.dart b/lib/src/piece/sequence.dart
index 515e48f..5b5c072 100644
--- a/lib/src/piece/sequence.dart
+++ b/lib/src/piece/sequence.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import '../back_end/code_writer.dart';
-import '../constants.dart';
 import 'piece.dart';
 
 /// A piece for a series of statements or members inside a block or declaration
@@ -29,12 +28,12 @@
 
   @override
   void format(CodeWriter writer, State state) {
-    writer.setAllowNewlines(state == State.split);
+    writer.pushAllowNewlines(state == State.split);
 
     if (_leftBracket case var leftBracket?) {
       writer.format(leftBracket);
-      writer.splitIf(state == State.split,
-          space: false, indent: _elements.firstOrNull?._indent ?? 0);
+      writer.pushIndent(_elements.firstOrNull?._indent ?? 0);
+      writer.splitIf(state == State.split, space: false);
     }
 
     for (var i = 0; i < _elements.length; i++) {
@@ -50,15 +49,20 @@
       writer.format(element, separate: separate);
 
       if (i < _elements.length - 1) {
-        writer.newline(
-            blank: element.blankAfter, indent: _elements[i + 1]._indent);
+        if (_leftBracket != null || i > 0) writer.popIndent();
+        writer.pushIndent(_elements[i + 1]._indent);
+        writer.newline(blank: element.blankAfter);
       }
     }
 
+    if (_leftBracket != null || _elements.length > 1) writer.popIndent();
+
     if (_rightBracket case var rightBracket?) {
-      writer.splitIf(state == State.split, space: false, indent: Indent.none);
+      writer.splitIf(state == State.split, space: false);
       writer.format(rightBracket);
     }
+
+    writer.popAllowNewlines();
   }
 
   @override
diff --git a/lib/src/piece/variable.dart b/lib/src/piece/variable.dart
index 7a6f8ee..8f5c026 100644
--- a/lib/src/piece/variable.dart
+++ b/lib/src/piece/variable.dart
@@ -64,12 +64,10 @@
 
     // If we split at the variables (but not the type), then indent the
     // variables and their initializers.
-    if (state == _betweenVariables) writer.setIndent(Indent.expression);
+    if (state == _betweenVariables) writer.pushIndent(Indent.expression);
 
-    // Force variables to split if an initializer does.
-    if (_variables.length > 1 && state == State.unsplit) {
-      writer.setAllowNewlines(false);
-    }
+    // Force multiple variables to split if an initializer does.
+    writer.pushAllowNewlines(_variables.length == 1 || state != State.unsplit);
 
     // Split after the type annotation.
     writer.splitIf(state == _afterType);
@@ -81,6 +79,10 @@
       // TODO(perf): Investigate whether it's worth using `separate:` here.
       writer.format(_variables[i]);
     }
+
+    if (state == _betweenVariables) writer.popIndent();
+
+    writer.popAllowNewlines();
   }
 
   @override
diff --git a/test/pattern/constant.stmt b/test/pattern/constant.stmt
new file mode 100644
index 0000000..9b12dc2
--- /dev/null
+++ b/test/pattern/constant.stmt
@@ -0,0 +1,18 @@
+40 columns                              |
+>>> Split in qualified name.
+if (object case veryLongPrefix.longIdentifierName) {;}
+<<<
+if (object
+    case veryLongPrefix
+        .longIdentifierName) {
+  ;
+}
+>>> Split in property chain.
+if (object case longPrefix.longType.longIdentifierName) {;}
+<<<
+if (object
+    case longPrefix
+        .longType
+        .longIdentifierName) {
+  ;
+}
\ No newline at end of file
diff --git a/test/pattern/list.stmt b/test/pattern/list.stmt
index 354eb0a..810e4e1 100644
--- a/test/pattern/list.stmt
+++ b/test/pattern/list.stmt
@@ -1,5 +1,5 @@
 40 columns                              |
->>> Basic list patterns. 
+>>> Basic list patterns.
 switch (obj) {
 case  [  ]  :
 case  <  int  >  [  ]  :
@@ -89,3 +89,22 @@
 ]) {
   ;
 }
+>>> Split inside element forces list to split.
+if (obj case [first,secondLongPattern ||thirdVeryLongPattern]) {;}
+<<<
+if (obj case [
+  first,
+  secondLongPattern ||
+      thirdVeryLongPattern,
+]) {
+  ;
+}
+>>> Split in rest element does not split after "...".
+if (obj case [...firstPattern || secondVeryLongPattern]) {;}
+<<<
+if (obj case [
+  ...firstPattern ||
+      secondVeryLongPattern,
+]) {
+  ;
+}
diff --git a/test/pattern/logic.stmt b/test/pattern/logic.stmt
new file mode 100644
index 0000000..1a932bb
--- /dev/null
+++ b/test/pattern/logic.stmt
@@ -0,0 +1,51 @@
+40 columns                              |
+>>> Unsplit.
+if (o case 1   ||  2   &&  3  ) {}
+<<<
+if (o case 1 || 2 && 3) {}
+>>> Nested as subpattern.
+if (o case 1   &&  (  2   ||  3   )  ) {}
+<<<
+if (o case 1 && (2 || 3)) {}
+>>> Chain of same logic operator all split together.
+if (object case first || second || third || fourth) {;}
+<<<
+if (object
+    case first ||
+        second ||
+        third ||
+        fourth) {
+  ;
+}
+>>> Chains of different logic operators split separately.
+if (object case first && second || third && fourth && fifth && sixth) {;}
+<<<
+if (object
+    case first && second ||
+        third &&
+            fourth &&
+            fifth &&
+            sixth) {
+  ;
+}
+>>> Chains of different logic operators split separately.
+if (object case first && second && third && fourth || fifth && sixth) {;}
+<<<
+if (object
+    case first &&
+            second &&
+            third &&
+            fourth ||
+        fifth && sixth) {
+  ;
+}
+>>> Multiple split variables as logic operands.
+if (object case SomeVeryLongTypeName anAlsoLongVariableName || AnotherLongTypeName anotherLongVariableName) {;}
+<<<
+if (object
+    case SomeVeryLongTypeName
+        anAlsoLongVariableName ||
+        AnotherLongTypeName
+        anotherLongVariableName) {
+  ;
+}
\ No newline at end of file
diff --git a/test/pattern/map.stmt b/test/pattern/map.stmt
index 2892cb0..4c2cfb2 100644
--- a/test/pattern/map.stmt
+++ b/test/pattern/map.stmt
@@ -56,3 +56,14 @@
   a: longPattern1,
   b: veryLongPattern2,
 }) {}
+>>> Split inside value forces map to split.
+if (obj case {firstKey: first, secondKey: secondLongPattern ||thirdLongPattern}) {;}
+<<<
+if (obj case {
+  firstKey: first,
+  secondKey:
+      secondLongPattern ||
+          thirdLongPattern,
+}) {
+  ;
+}