blob: 751af83c478914ccafc4af87b52ae5bc6cf5a771 [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 'dart:math';
import '../piece/piece.dart';
import 'solution.dart';
import 'solution_cache.dart';
/// The interface used by [Piece]s to output formatted code.
///
/// The back-end lowers the tree of pieces to the final formatted code by
/// allowing each piece to produce the output for the code it represents.
/// This way, each piece has full flexibility for how to apply its own
/// formatting logic.
///
/// To build the resulting output code, when a piece is formatted, it is passed
/// an instance of this class. It has methods that the piece can call to add
/// output text to the resulting code, recursively format child pieces, insert
/// whitespace, etc.
class CodeWriter {
final int _pageWidth;
/// Previously cached formatted subtrees.
final SolutionCache _cache;
/// The solution this [CodeWriter] is generating code for.
final Solution _solution;
/// Buffer for the code being written.
final StringBuffer _buffer = StringBuffer();
/// What whitespace should be written before the next non-whitespace text.
///
/// When whitespace is written, instead of immediately writing it, we queue
/// it as pending. This ensures that we don't write trailing whitespace,
/// avoids writing spaces at the beginning of lines, and allows collapsing
/// multiple redundant newlines.
Whitespace _pendingWhitespace = Whitespace.none;
/// The number of spaces of indentation that should be begin the next line
/// when [_pendingWhitespace] is [Whitespace.newline] or
/// [Whitespace.blankLine].
int _pendingIndent = 0;
/// The number of characters in the line currently being written.
int _column = 0;
/// The stack indentation levels.
///
/// 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.
///
/// This is the first line that either overflows or contains an invalid
/// newline. When expanding solutions, we use the first solvable piece on
/// this line.
bool _foundExpandLine = false;
/// The first solvable piece on the first overflowing or invalid line, if
/// we've found one.
///
/// A piece is "solvable" if we haven't already bound it to a state and there
/// are multiple states it accepts. This is the piece whose states will be
/// bound when we expand the [Solution] that this [CodeWriter] is building
/// into further solutions.
///
/// If [_foundExpandLine] is `false`, then this is the first solvable piece
/// that has written text to the current line. It may not actually be an
/// expand piece. We don't know until we reach the end of the line to see if
/// it overflows or is invalid. If the line is OK, then [_nextPieceToExpand]
/// is cleared when the next line begins. If [_foundExpandLine] is `true`,
/// then this known to be the piece that will be expanded next for this
/// solution.
Piece? _nextPieceToExpand;
/// The stack of solvable pieces currently being formatted.
///
/// We use this to track which pieces are in play when text is written to the
/// current line so that we know which piece should be expanded in the next
/// solution if the line ends up overflowing.
final List<Piece> _currentUnsolvedPieces = [];
/// [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;
}
/// Returns the final formatted text and the next piece that can be expanded
/// from the solution this [CodeWriter] is writing, if any.
(String, Piece?) finish() {
_finishLine();
return (_buffer.toString(), _nextPieceToExpand);
}
/// Appends [text] to the output.
///
/// If [text] contains any internal newlines, the caller is responsible for
/// also calling [handleNewline()].
void write(String text) {
// TODO(tall): Calling this directly from pieces outside of TextPiece may
// not handle selections as gracefully as we could. A selection marker may
// get pushed past the text written here. Currently, this is only called
// directly for commas in list-like things, and `;` in for loops. In
// general, it's better for all text written to the output to live inside
// TextPieces because that will preserve selection markers. Consider doing
// something smarter for commas in lists and semicolons in for loops.
_flushWhitespace();
_buffer.write(text);
_column += text.length;
// If we haven't found an overflowing line yet, then this line might be one
// so keep track of the pieces we've encountered.
if (!_foundExpandLine &&
_nextPieceToExpand == null &&
_currentUnsolvedPieces.isNotEmpty) {
_nextPieceToExpand = _currentUnsolvedPieces.first;
}
}
/// Increases the number of spaces of indentation by [indent] relative to the
/// current amount of indentation.
///
/// 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 (parentCollapse == indent) {
// We're indenting by the same existing collapsible amount, so collapse
// this new indentation with that existing one.
_indentStack.add(_Indent(parentIndent, 0));
} else if (canCollapse) {
// We should never get multiple levels of nested collapsible indentation.
assert(parentCollapse == 0);
// Increase the indentation and note that it can be collapsed with
// further indentation.
_indentStack.add(_Indent(parentIndent + indent, indent));
} else {
// Regular indentation, so just increase the indent.
_indentStack.add(_Indent(parentIndent + indent, 0));
}
}
/// 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.
///
/// If [space] is `true` and [condition] is `false`, writes a space.
///
/// If [blank] is `true`, writes an extra newline to produce a blank line.
void splitIf(bool condition, {bool space = true, bool blank = false}) {
if (condition) {
newline(blank: blank);
} else if (space) {
this.space();
}
}
/// Writes a single space to the output.
void space() {
whitespace(Whitespace.space);
}
/// Inserts a line split in the output.
///
/// If [blank] is `true`, writes an extra newline to produce a blank line.
///
/// 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, bool flushLeft = false}) {
whitespace(blank ? Whitespace.blankLine : Whitespace.newline,
flushLeft: flushLeft);
}
/// Queues [whitespace] to be written to the output.
///
/// If any non-whitespace is written after this call, then this whitespace
/// will be written first. Also handles merging multiple kinds of whitespace
/// intelligently together.
///
/// 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 whitespace(Whitespace whitespace, {bool flushLeft = false}) {
if (whitespace case Whitespace.newline || Whitespace.blankLine) {
_handleNewline();
_pendingIndent = flushLeft ? 0 : _indentStack.last.indent;
}
_pendingWhitespace = _pendingWhitespace.collapse(whitespace);
}
/// Format [piece] and insert the result into the code being written and
/// returned by [finish()].
///
/// If [separate] is `true`, then [piece] is formatted and solved using a
/// separate Solver and the result inserted into this CodeWriter's Solution.
/// This lets us solve branches of the piece tree separately and compose the
/// optimal results together.
///
/// It's only safe to pass [separate] when the piece's formatting depends
/// only on its starting indentation and state. If the piece's formatting can
/// be affected by the contents of the current line, the contents after the
/// piece's ending line, or constraints between pieces, then [separate] should
/// be `false`. It's up to the parent piece to only call this when it's safe
/// to do so. In practice, this usually means when the parent piece knows that
/// [piece] will have a newline before and after it.
void format(Piece piece, {bool separate = false}) {
if (separate) {
_formatSeparate(piece);
} else {
_formatInline(piece);
}
}
/// Format [piece] using a separate [Solver] and merge the result into this
/// writer's [_solution].
void _formatSeparate(Piece piece) {
var solution = _cache.find(
_pageWidth, piece, _solution.pieceState(piece), _pendingIndent);
_pendingIndent = 0;
_flushWhitespace();
_solution.mergeSubtree(solution);
// If a selection marker was in the child piece, set it in this piece,
// relative to where the child's code is appended.
if (solution.selectionStart case var start?) {
_solution.startSelection(_buffer.length + start);
}
if (solution.selectionEnd case var end?) {
_solution.endSelection(_buffer.length + end);
}
_buffer.write(solution.text);
}
/// Format [piece] writing directly into this [CodeWriter].
void _formatInline(Piece piece) {
// 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 childHadNewline = _hadNewline;
_hadNewline = previousHadNewline;
_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.
void startSelection(int start) {
_flushWhitespace();
_solution.startSelection(_buffer.length + start);
}
/// Sets [selectionEnd] to be [end] code units into the output.
void endSelection(int end) {
_flushWhitespace();
_solution.endSelection(_buffer.length + end);
}
/// Notes that a newline has been written.
///
/// If this occurs in a place where newlines are prohibited, then invalidates
/// the solution.
void _handleNewline() {
if (!_allowNewlineStack.last) _solution.invalidate(_currentPiece!);
// Note that this piece contains a newline so that we can propagate that
// up to containing pieces too.
_hadNewline = true;
}
/// Write any pending whitespace.
///
/// This is called before non-whitespace text is about to be written, or
/// before the selection is updated since the latter requires an accurate
/// count of the written text, including whitespace.
void _flushWhitespace() {
switch (_pendingWhitespace) {
case Whitespace.none:
break; // Nothing to do.
case Whitespace.newline:
case Whitespace.blankLine:
_finishLine();
_buffer.writeln();
if (_pendingWhitespace == Whitespace.blankLine) _buffer.writeln();
_column = _pendingIndent;
_buffer.write(' ' * _column);
case Whitespace.space:
_buffer.write(' ');
_column++;
}
_pendingWhitespace = Whitespace.none;
}
void _finishLine() {
// If the completed line is too long, track the overflow.
if (_column >= _pageWidth) {
_solution.addOverflow(_column - _pageWidth);
}
// If we found a problematic line, and there is a piece on the line that
// we can try to split, then remember that piece so that the solution will
// expand it next.
if (!_foundExpandLine &&
_nextPieceToExpand != null &&
(_column > _pageWidth || !_solution.isValid)) {
// We found a problematic line, so remember it and the piece on it.
_foundExpandLine = true;
} else if (!_foundExpandLine) {
// This line was OK, so we don't need to expand the piece on it.
_nextPieceToExpand = null;
}
}
}
/// Different kinds of pending whitespace that have been requested.
///
/// Note that the order of values in the enum is significant: later ones have
/// more whitespace than previous ones.
enum Whitespace {
/// No pending whitespace.
none,
/// A single space.
space,
/// A single newline.
newline,
/// Two newlines.
blankLine;
/// Combines two pending whitespaces and returns the result.
///
/// When two whitespaces overlap, they aren't both written: we don't want
/// two spaces or a newline followed by a space. Instead, the two whitespaces
/// are collapsed such that the largest one wins.
Whitespace collapse(Whitespace other) => values[max(index, other.index)];
/// Whether this whitespace contains at least one newline.
bool get hasNewline => switch (this) {
newline || blankLine => true,
_ => false,
};
}
/// A level of indentation in the indentation stack.
class _Indent {
/// The total number of spaces of indentation.
final int indent;
/// How many spaces of [indent] can be collapsed with further indentation.
final int collapsible;
_Indent(this.indent, this.collapsible);
}