blob: 8aa941283b15632a09872b4ebf790530b1844b86 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// 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 piece for an assignment-like construct where an operator is followed by
/// an expression but where the left side of the operator isn't also an
/// expression. Used for:
///
/// - Assignment (`=`, `+=`, etc.)
/// - Named arguments (`:`)
/// - Map entries (`:`)
/// - Record fields (`:`)
/// - Expression function bodies (`=>`)
///
/// These constructs can be formatted four ways:
///
/// [State.unsplit] No split at all:
///
/// var x = 123;
///
/// This state also allows splitting the right side if it can be block
/// formatted:
///
/// var list = [
/// element,
/// ];
///
/// [_blockSplitLeft] Force the left-hand side, which must be a [ListPiece], to
/// split. Allow the right side to split or not. Allows all of:
///
/// var [
/// element,
/// ] = unsplitRhs;
///
/// var [
/// element,
/// ] = [
/// 'block split RHS',
/// ];
///
/// var [
/// element,
/// ] = 'expression split' +
/// 'the right hand side';
///
/// [_blockSplitRight] Allow the right-hand side to block split or not, if it
/// wants. Since [State.unsplit] and [_blockSplitLeft] also allow the
/// right-hand side to block split, this state is only used when the left-hand
/// side expression splits, like:
///
/// var (variable &&
/// anotherVariable) = [
/// element,
/// ];
///
/// [_atOperator] Split at the `=` or `in` operator and allow expression
/// splitting in either operand. Allows all of:
///
/// var (longVariable &&
/// anotherVariable) =
/// longOperand +
/// anotherOperand;
///
/// var [unsplitBlock] =
/// longOperand +
/// anotherOperand;
class AssignPiece extends Piece {
/// Force the block left-hand side to split and allow the right-hand side to
/// split.
static const State _blockSplitLeft = State(1);
/// Allow the right-hand side to block split.
static const State _blockSplitRight = State(2);
/// Split at the operator.
static const State _atOperator = State(3);
/// The left-hand side of the operation.
final Piece? _left;
// TODO(rnystrom): If it wasn't for the need to constrain [_left] to split
// in [applyConstraints()], we could write the operator into the same piece
// as [_left]. In the common case where the AssignPiece is for a named
// argument, the name and `:` would then end up in a single atomic
// [CodePiece].
/// The `=` or other operator.
final Piece _operator;
/// The right-hand side of the operation.
final Piece _right;
/// If `true`, then the left side supports being block-formatted, like:
///
/// var [
/// element1,
/// element2,
/// ] = value;
final bool _canBlockSplitLeft;
/// If `true` then the right side supports being block-formatted, like:
///
/// var list = [
/// element1,
/// element2,
/// ];
final bool _canBlockSplitRight;
AssignPiece(this._operator, this._right,
{Piece? left,
bool canBlockSplitLeft = false,
bool canBlockSplitRight = false})
: _left = left,
_canBlockSplitLeft = canBlockSplitLeft,
_canBlockSplitRight = canBlockSplitRight;
@override
List<State> get additionalStates => [
// If at least one operand can block split, allow splitting in operands
// without splitting at the operator.
if (_canBlockSplitLeft) _blockSplitLeft,
if (_canBlockSplitRight) _blockSplitRight,
_atOperator,
];
@override
void applyConstraints(State state, Constrain constrain) {
// Force the left side to block split when in that state.
//
// Otherwise, the solver may instead leave it unsplit and then split the
// right side incorrectly as in:
//
// (x, y) = longOperand2 +
// longOperand2 +
// longOperand3;
if (state == _blockSplitLeft) constrain(_left!, State.split);
}
@override
void format(CodeWriter writer, State state) {
switch (state) {
case State.unsplit:
_writeLeft(writer, allowNewlines: false);
_writeOperator(writer);
// Always allow block-splitting the right side if it supports it.
_writeRight(writer, allowNewlines: _canBlockSplitRight);
case _atOperator:
// When splitting at the operator, both operands may split or not and
// will be indented if they do.
writer.pushIndent(Indent.expression);
_writeLeft(writer);
_writeOperator(writer, split: state == _atOperator);
_writeRight(writer);
writer.popIndent();
case _blockSplitLeft:
_writeLeft(writer);
_writeOperator(writer);
_writeRight(writer, indent: !_canBlockSplitRight);
case _blockSplitRight:
_writeLeft(writer);
_writeOperator(writer, split: state == _atOperator);
_writeRight(writer);
}
}
void _writeLeft(CodeWriter writer, {bool allowNewlines = true}) {
if (_left case var left?) writer.format(left, allowNewlines: allowNewlines);
}
void _writeOperator(CodeWriter writer, {bool split = false}) {
writer.pushIndent(Indent.expression);
writer.format(_operator);
writer.popIndent();
writer.splitIf(split);
}
void _writeRight(CodeWriter writer,
{bool indent = false, bool allowNewlines = true}) {
if (indent) writer.pushIndent(Indent.expression);
writer.format(_right, allowNewlines: allowNewlines);
if (indent) writer.popIndent();
}
@override
void forEachChild(void Function(Piece piece) callback) {
if (_left case var left?) callback(left);
callback(_operator);
callback(_right);
}
@override
State? fixedStateForPageWidth(int pageWidth) {
// If either side (or both) can block split, then they may allow a long
// assignment to still not end up splitting at the operator.
if (_canBlockSplitLeft || _canBlockSplitRight) return null;
// Edge case: If the left operand is only a single character, then splitting
// at the operator won't actually make the line any smaller, so don't apply
// the optimization in that case:
//
// e = someVeryLongExpression;
//
// Is no worse than:
//
// e =
// someVeryLongExpression;
if (_left case var left? when left.totalCharacters == 1) return null;
// If either operand contains a newline or the whole assignment doesn't
// fit then it will split at the operator since there's no other way it
// can split because there are no block operands.
var totalLength = 0;
if (_left case var left? when !_canBlockSplitLeft) {
if (left.containsNewline) return _atOperator;
totalLength += left.totalCharacters;
}
totalLength += _operator.totalCharacters;
if (!_canBlockSplitRight) {
if (_right.containsNewline) return _atOperator;
totalLength += _right.totalCharacters;
}
if (totalLength > pageWidth) return _atOperator;
return null;
}
}