diff --git a/lib/src/back_end/solution.dart b/lib/src/back_end/solution.dart index c2e7c306..092e9332 100644 --- a/lib/src/back_end/solution.dart +++ b/lib/src/back_end/solution.dart @@ -26,7 +26,9 @@ class PieceStateSet { PieceStateSet._(this._pieces, this._pieceStates); /// The state this solution selects for [piece]. - State pieceState(Piece piece) => _pieceStates[piece] ?? State.initial; + /// + /// If no state has been selected, defaults to the first state. + State pieceState(Piece piece) => _pieceStates[piece] ?? piece.states.first; /// Gets the first piece that doesn't have a state selected yet, or `null` if /// all pieces have selected states. @@ -86,9 +88,6 @@ class Solution implements Comparable { if (piece == null) return const []; return [ - // All pieces support a default state. - Solution(root, pageWidth, _state.cloneWith(piece, State.initial)), - for (var state in piece.states) Solution(root, pageWidth, _state.cloneWith(piece, state)) ]; diff --git a/lib/src/back_end/solver.dart b/lib/src/back_end/solver.dart index 7acc6d48..26effa93 100644 --- a/lib/src/back_end/solver.dart +++ b/lib/src/back_end/solver.dart @@ -14,10 +14,9 @@ import 'solution.dart'; /// possible states, so it isn't feasible to brute force. There are a few /// techniques we use to avoid that: /// -/// - All pieces default to being in [State.initial]. Every piece is -/// implemented such that that state has no line splits (or only mandatory -/// ones) and zero cost. Thus, it tries solutions with a minimum number of -/// line splits first. +/// - The initial state for each piece has no line splits or only mandatory +/// ones. Thus, it tries solutions with a minimum number of line splits +/// first. /// /// - Solutions are explored in priority order. We explore solutions with the /// the lowest cost first. This way, as soon as we find a solution with no diff --git a/lib/src/piece/assign.dart b/lib/src/piece/assign.dart index 032219fa..53960216 100644 --- a/lib/src/piece/assign.dart +++ b/lib/src/piece/assign.dart @@ -14,7 +14,7 @@ import 'piece.dart'; /// /// These constructs can be formatted three ways: /// -/// [State.initial] No split at all: +/// [State.unsplit] No split at all: /// /// ``` /// var x = 123; @@ -60,13 +60,14 @@ class AssignPiece extends Piece { : _isValueDelimited = isValueDelimited; @override - List get states => [if (_isValueDelimited) _insideValue, _atEquals]; + List get additionalStates => + [if (_isValueDelimited) _insideValue, _atEquals]; @override void format(CodeWriter writer, State state) { // A split in either child piece forces splitting after the "=" unless it's // a delimited expression. - if (state == State.initial) writer.setAllowNewlines(false); + if (state == State.unsplit) writer.setAllowNewlines(false); // Don't indent a split delimited expression. if (state != _insideValue) writer.setIndent(Indent.expression); diff --git a/lib/src/piece/block.dart b/lib/src/piece/block.dart index e6c0aee0..e11ec930 100644 --- a/lib/src/piece/block.dart +++ b/lib/src/piece/block.dart @@ -19,24 +19,22 @@ class BlockPiece extends Piece { /// The closing delimiter. final Piece rightBracket; - /// Whether the block should always split its contents. - /// - /// True for most blocks, but false for enums and blocks containing only - /// inline block comments. - final bool _alwaysSplit; - + /// If [alwaysSplit] is true, then the block should always split its contents. + /// This is true for most blocks, but false for enums and blocks containing + /// only inline block comments. BlockPiece(this.leftBracket, this.contents, this.rightBracket, - {bool alwaysSplit = true}) - : _alwaysSplit = alwaysSplit; + {bool alwaysSplit = true}) { + if (alwaysSplit) pin(State.split); + } @override - List get states => _alwaysSplit ? const [] : const [State.split]; + List get additionalStates => const [State.split]; @override void format(CodeWriter writer, State state) { writer.format(leftBracket); - if (_alwaysSplit || state == State.split) { + if (state == State.split) { if (contents.isNotEmpty) { writer.newline(indent: Indent.block); writer.format(contents); diff --git a/lib/src/piece/do_while.dart b/lib/src/piece/do_while.dart index 6ac28a4f..10a9ce16 100644 --- a/lib/src/piece/do_while.dart +++ b/lib/src/piece/do_while.dart @@ -12,9 +12,6 @@ class DoWhilePiece extends Piece { DoWhilePiece(this._body, this._condition); - @override - List get states => const []; - @override void format(CodeWriter writer, State state) { writer.setIndent(Indent.none); diff --git a/lib/src/piece/function.dart b/lib/src/piece/function.dart index e6a652d4..e8201211 100644 --- a/lib/src/piece/function.dart +++ b/lib/src/piece/function.dart @@ -18,7 +18,7 @@ class FunctionTypePiece extends Piece { FunctionTypePiece(this._returnType, this._signature); @override - List get states => const [State.split]; + List get additionalStates => const [State.split]; @override void format(CodeWriter writer, State state) { diff --git a/lib/src/piece/if.dart b/lib/src/piece/if.dart index 4b041982..32d84abe 100644 --- a/lib/src/piece/if.dart +++ b/lib/src/piece/if.dart @@ -30,14 +30,14 @@ class IfPiece extends Piece { /// If there is at least one else or else-if clause, then it always splits. @override - List get states => _isUnbracedIfThen ? const [State.split] : const []; + List get additionalStates => [if (_isUnbracedIfThen) State.split]; @override void format(CodeWriter writer, State state) { if (_isUnbracedIfThen) { // A split in the condition or statement forces moving the entire // statement to the next line. - writer.setAllowNewlines(state != State.initial); + writer.setAllowNewlines(state != State.unsplit); var section = _sections.single; writer.format(section.header); diff --git a/lib/src/piece/import.dart b/lib/src/piece/import.dart index e3d6be40..9314dcb1 100644 --- a/lib/src/piece/import.dart +++ b/lib/src/piece/import.dart @@ -13,7 +13,7 @@ import 'piece.dart'; /// /// Combinators can be split like so: /// -/// [State.initial] All on one line: +/// [State.unsplit] All on one line: /// /// ``` /// import 'animals.dart' show Ant, Bat hide Cat, Dog; @@ -109,7 +109,7 @@ class ImportPiece extends Piece { } @override - List get states => [ + List get additionalStates => [ _beforeCombinators, if (_combinators.length > 1) ...[ _firstCombinator, @@ -126,13 +126,13 @@ class ImportPiece extends Piece { if (_combinators.isNotEmpty) { _combinators[0].format(writer, - splitKeyword: state != State.initial, + splitKeyword: state != State.unsplit, splitNames: state == _firstCombinator || state == State.split); } if (_combinators.length > 1) { _combinators[1].format(writer, - splitKeyword: state != State.initial, + splitKeyword: state != State.unsplit, splitNames: state == _secondCombinator || state == State.split); } } diff --git a/lib/src/piece/infix.dart b/lib/src/piece/infix.dart index b6227243..5cee4b97 100644 --- a/lib/src/piece/infix.dart +++ b/lib/src/piece/infix.dart @@ -22,25 +22,19 @@ class InfixPiece extends Piece { InfixPiece(this.operands); @override - List get states => const [State.split]; + List get additionalStates => const [State.split]; @override void format(CodeWriter writer, State state) { - switch (state) { - case State.initial: - writer.setAllowNewlines(false); - for (var i = 0; i < operands.length; i++) { - writer.format(operands[i]); - - if (i < operands.length - 1) writer.space(); - } + if (state == State.unsplit) { + writer.setAllowNewlines(false); + } else { + writer.setNesting(Indent.expression); + } - case State.split: - writer.setNesting(Indent.expression); - for (var i = 0; i < operands.length; i++) { - writer.format(operands[i]); - if (i < operands.length - 1) writer.newline(); - } + for (var i = 0; i < operands.length; i++) { + writer.format(operands[i]); + if (i < operands.length - 1) writer.splitIf(state == State.split); } } diff --git a/lib/src/piece/list.dart b/lib/src/piece/list.dart index cdb54a40..dcf54d91 100644 --- a/lib/src/piece/list.dart +++ b/lib/src/piece/list.dart @@ -55,15 +55,12 @@ class ListPiece extends Piece { this._isTypeList); @override - List get states { - // Don't split between an empty pair of brackets. - if (_arguments.isEmpty) return const []; - - // Type lists are more expensive to split. - if (_isTypeList) return const [_splitTypes]; - - return const [State.split]; - } + List get additionalStates => [ + if (_isTypeList) + _splitTypes // Type lists are more expensive to split. + else if (_arguments.isNotEmpty) + State.split // Don't split between an empty pair of brackets. + ]; @override void format(CodeWriter writer, State state) { @@ -78,7 +75,7 @@ class ListPiece extends Piece { // }); // ``` switch (state) { - case State.initial: + case State.unsplit: // All arguments on one line with no trailing comma. writer.setAllowNewlines(false); for (var i = 0; i < _arguments.length; i++) { diff --git a/lib/src/piece/piece.dart b/lib/src/piece/piece.dart index 82493447..dd402646 100644 --- a/lib/src/piece/piece.dart +++ b/lib/src/piece/piece.dart @@ -12,15 +12,41 @@ import '../back_end/code_writer.dart'; /// formatting and line splitting. The final output is then determined by /// deciding which pieces split and how. abstract class Piece { - /// The ordered list of indexes identifying each way this piece can split. + /// The ordered list of ways this piece may split. /// - /// Each piece determines what each value in the list represents. The list - /// returned by this function should be sorted so that earlier states in the - /// list compare less than later states. + /// If the piece is aready pinned, then this is just the one pinned state. + /// Otherwise, it's [State.unsplit], which all pieces support, followed by + /// any other [additionalStates]. + List get states { + if (_pinnedState case var state?) { + return [state]; + } else { + return [State.unsplit, ...additionalStates]; + } + } + + /// The ordered list of all possible ways this piece could split. + /// + /// Piece subclasses should override this if they support being split in + /// multiple different ways. + /// + /// Each piece determines what each [State] in the list represents, including + /// the automatically included [State.unsplit]. The list returned by this + /// function should be sorted so that earlier states in the list compare less + /// than later states. + List get additionalStates => const []; + + /// If this piece has been pinned to a specific state, that state. /// - /// In addition to the values returned here, each piece should implicitly - /// support a [State.initial] which is the least split form the piece allows. - List get states; + /// This is used when a piece which otherwise supports multiple ways of + /// splitting should be eagerly constrained to a specific splitting choice + /// because of the context where it appears. For example, if conditional + /// expressions are nested, then all of them are forced to split because it's + /// too hard to read nested conditionals all on one line. We can express that + /// by pinning the Piece used for a conditional expression to its split state + /// when surrounded by or containing other conditionals. + State? get pinnedState => _pinnedState; + State? _pinnedState; /// Given that this piece is in [state], use [writer] to produce its formatted /// output. @@ -28,6 +54,11 @@ abstract class Piece { /// Invokes [callback] on each piece contained in this piece. void forEachChild(void Function(Piece piece) callback); + + /// Forces this piece to always use [state]. + void pin(State state) { + _pinnedState = state; + } } /// A simple atomic piece of code. @@ -49,9 +80,6 @@ class TextPiece extends Piece { /// multiline strings, etc. bool _containsNewline = false; - @override - List get states => const []; - /// Whether the last line of this piece's text ends with [text]. bool endsWith(String text) => _lines.isNotEmpty && _lines.last.endsWith(text); @@ -96,7 +124,7 @@ class TextPiece extends Piece { /// Each state identifies one way that a piece can be split into multiple lines. /// Each piece determines how its states are interpreted. class State implements Comparable { - static const initial = State(0, cost: 0); + static const unsplit = State(0, cost: 0); /// The maximally split state a piece can be in. /// diff --git a/lib/src/piece/postfix.dart b/lib/src/piece/postfix.dart index eb933687..432ea012 100644 --- a/lib/src/piece/postfix.dart +++ b/lib/src/piece/postfix.dart @@ -26,7 +26,7 @@ class PostfixPiece extends Piece { PostfixPiece(this.pieces); @override - List get states => const [State.split]; + List get additionalStates => const [State.split]; @override void format(CodeWriter writer, State state) { @@ -34,7 +34,7 @@ class PostfixPiece extends Piece { // 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.initial) writer.setAllowNewlines(false); + if (state == State.unsplit) writer.setAllowNewlines(false); for (var piece in pieces) { writer.splitIf(state == State.split, indent: Indent.expression); diff --git a/lib/src/piece/sequence.dart b/lib/src/piece/sequence.dart index 91642c34..08b3db35 100644 --- a/lib/src/piece/sequence.dart +++ b/lib/src/piece/sequence.dart @@ -22,9 +22,6 @@ class SequencePiece extends Piece { /// Whether this sequence has any contents. bool get isNotEmpty => _contents.isNotEmpty; - @override - List get states => const []; - @override void format(CodeWriter writer, State state) { for (var i = 0; i < _contents.length; i++) { diff --git a/lib/src/piece/variable.dart b/lib/src/piece/variable.dart index b4b5ba78..3eb88881 100644 --- a/lib/src/piece/variable.dart +++ b/lib/src/piece/variable.dart @@ -59,7 +59,7 @@ class VariablePiece extends Piece { : _hasType = hasType; @override - List get states => [ + List get additionalStates => [ if (_variables.length > 1) _betweenVariables, if (_hasType) _afterType, ]; @@ -73,7 +73,7 @@ class VariablePiece extends Piece { if (state == _betweenVariables) writer.setIndent(Indent.expression); // Force variables to split if an initializer does. - if (_variables.length > 1 && state == State.initial) { + if (_variables.length > 1 && state == State.unsplit) { writer.setAllowNewlines(false); } @@ -82,7 +82,7 @@ class VariablePiece extends Piece { for (var i = 0; i < _variables.length; i++) { // Split between variables. - if (i > 0) writer.splitIf(state != State.initial); + if (i > 0) writer.splitIf(state != State.unsplit); writer.format(_variables[i]); }