blob: 116763a314b6bdf0b1de177c6343bc3ba89f1cc3 [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';
/// Base class for the formatter's internal representation used for line
/// splitting.
///
/// We visit the source AST and convert it to a tree of [Piece]s. This tree
/// roughly follows the AST but includes comments and is optimized for
/// formatting and line splitting. The final output is then determined by
/// deciding which pieces split and how.
abstract class Piece {
/// The ordered list of ways this piece may split.
///
/// 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<State> 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<State> get additionalStates => const [];
/// If this piece has been pinned to a specific state, that state.
///
/// 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.
void format(CodeWriter writer, State state);
/// 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;
}
/// The name of this piece as it appears in debug output.
///
/// By default, this is the class's name with `Piece` removed.
String get debugName => runtimeType.toString().replaceAll('Piece', '');
@override
String toString() => debugName;
}
/// A simple atomic piece of code.
///
/// This may represent a series of tokens where no split can occur between them.
/// It may also contain one or more comments.
class TextPiece extends Piece {
/// The lines of text in this piece.
///
/// Most [TextPieces] will contain only a single line, but a piece with
/// preceding comments that are on their own line will have multiple. These
/// are stored as separate lines instead of a single multi-line string so that
/// each line can be indented appropriately during formatting.
final List<String> _lines = [];
/// True if this text piece contains or ends with a mandatory newline.
///
/// This can be from line comments, block comments with newlines inside,
/// multiline strings, etc.
bool _containsNewline = false;
/// Whitespace at the end of this [TextPiece].
///
/// This will be turned into actual text if non-whitespace is written after
/// the pending whitespace is set. Otherwise, it will be written to the output
/// when the [TextPiece] is formatted.
///
/// Initially [Whitespace.newline] so that we insert a new string into the
/// empty [_lines] list on the first write.
Whitespace _trailingWhitespace = Whitespace.newline;
/// The offset from the beginning of [text] where the selection starts, or
/// `null` if the selection does not start within this chunk.
int? _selectionStart;
/// The offset from the beginning of [text] where the selection ends, or
/// `null` if the selection does not start within this chunk.
int? _selectionEnd;
/// Whether the last line of this piece's text ends with [text].
bool endsWith(String text) => _lines.isNotEmpty && _lines.last.endsWith(text);
/// Append [text] to the end of this piece.
///
/// If [text] internally contains a newline, then [containsNewline] should
/// be `true`.
void append(String text, {bool containsNewline = false}) {
// Write any pending whitespace into the text.
switch (_trailingWhitespace) {
case Whitespace.none:
break; // Nothing to do.
case Whitespace.space:
// TODO(perf): Consider a faster way of accumulating text.
_lines.last += ' ';
case Whitespace.newline:
_lines.add('');
case Whitespace.blankLine:
throw UnsupportedError('No blank lines in TextPieces.');
}
_trailingWhitespace = Whitespace.none;
// TODO(perf): Consider a faster way of accumulating text.
_lines.last = _lines.last + text;
if (containsNewline) _containsNewline = true;
}
void space() {
_trailingWhitespace = _trailingWhitespace.collapse(Whitespace.space);
}
void newline() {
_trailingWhitespace = _trailingWhitespace.collapse(Whitespace.newline);
}
@override
void format(CodeWriter writer, State state) {
// Let the writer know if there are any embedded newlines even if there is
// only one "line" in [_lines].
if (_containsNewline) writer.handleNewline();
if (_selectionStart case var start?) {
writer.startSelection(start);
}
if (_selectionEnd case var end?) {
writer.endSelection(end);
}
for (var i = 0; i < _lines.length; i++) {
if (i > 0) writer.newline();
writer.write(_lines[i]);
}
writer.whitespace(_trailingWhitespace);
}
@override
void forEachChild(void Function(Piece piece) callback) {}
/// Sets [selectionStart] to be [start] code units after the end of the
/// current text in this piece.
void startSelection(int start) {
_selectionStart = _adjustSelection(start);
}
/// Sets [selectionEnd] to be [end] code units after the end of the
/// current text in this piece.
void endSelection(int end) {
_selectionEnd = _adjustSelection(end);
}
/// Adjust [offset] by the current length of this [TextPiece].
int _adjustSelection(int offset) {
for (var line in _lines) {
offset += line.length;
}
if (_trailingWhitespace == Whitespace.space) offset++;
return offset;
}
@override
String toString() => '`${_lines.join('¬')}`${_containsNewline ? '!' : ''}';
}
/// A piece that writes a single space.
class SpacePiece extends Piece {
@override
void forEachChild(void Function(Piece piece) callback) {}
@override
void format(CodeWriter writer, State state) {
writer.space();
}
}
/// A state that a piece can be in.
///
/// 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<State> {
static const unsplit = State(0, cost: 0);
/// The maximally split state a piece can be in.
///
/// The value here is somewhat arbitrary. It just needs to be larger than
/// any other value used by any [Piece] that uses this [State].
static const split = State(255);
final int _value;
/// How much a solution is penalized when this state is chosen.
final int cost;
const State(this._value, {this.cost = 1});
@override
int compareTo(State other) => _value.compareTo(other._value);
@override
String toString() => '◦$_value';
}