blob: e610b3e4fecaed913c880ad6c15cad0083a872ce [file] [log] [blame] [edit]
// 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 '../debug.dart' as debug;
import '../piece/piece.dart';
import '../profile.dart';
import 'code.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.
final class CodeWriter {
final int _pageWidth;
/// Previously cached formatted subtrees.
final SolutionCache _cache;
/// The solution this [CodeWriter] is generating code for.
final Solution _solution;
/// The code being written.
final GroupCode _code;
/// 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.
///
/// Initially [Whitespace.newline] so that we write the leading indentation
/// before the first token.
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 of 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<_IndentLevel> _indentStack = [];
/// The stack of information for each [Piece] currently being formatted.
///
/// This allows [CodeWriter] to pass itself data from parent to child through
/// [format()] calls and back up from child to parent without every override
/// of [format()] having to thread that data through.
final List<_FormatState> _pieceFormats = [];
/// 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 solvable pieces on the first overflowing or invalid line, if we've
/// found any.
///
/// 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 `true`, then this contains the list of unsolved
/// pieces that were being formatted when text was written to the first
/// problematic line.
final List<Piece> _expandPieces = [];
/// 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 = [];
/// The set of unsolved pieces that were being formatted when text was
/// written to the current line.
final Set<Piece> _currentLinePieces = {};
/// [leadingIndent] is the number of spaces of leading indentation at the
/// beginning of the first line and [subsequentIndent] is the indentation of
/// each line after that, independent of indentation created by pieces being
/// written.
CodeWriter(
this._pageWidth,
int leadingIndent,
int subsequentIndent,
this._cache,
this._solution,
) : _code = GroupCode(leadingIndent) {
_indentStack.add(_IndentLevel(Indent.none, leadingIndent));
// Track the leading indent before the first line.
_pendingIndent = leadingIndent;
_column = _pendingIndent;
// If there is additional indentation on subsequent lines, then push that
// onto the stack. When the first newline is written, [_pendingIndent] will
// pick this up and use it for subsequent lines.
if (subsequentIndent > leadingIndent) {
_indentStack.add(_IndentLevel(Indent.none, subsequentIndent));
}
}
/// Returns the final formatted code and the next pieces that can be expanded
/// from the solution this [CodeWriter] is writing, if any.
(GroupCode, List<Piece>) finish() {
_finishLine();
return (_code, _expandPieces);
}
/// Appends [text] to the output.
///
/// If [text] contains any internal newlines, the caller is responsible for
/// also calling [handleNewline()].
///
/// When possible, avoid calling this directly. Instead, any input code
/// lexemes should be written to TextPieces which then call this. That way,
/// selections inside lexemes are correctly updated.
void write(String text) {
_flushWhitespace();
_code.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 unsolved pieces we've encountered on it.
if (!_foundExpandLine) {
_currentLinePieces.addAll(_currentUnsolvedPieces);
}
}
/// Increases the indentation by [indent] relative to the current amount of
/// indentation.
void pushIndent(Indent indent) {
if (_cache.is3Dot7) {
_pushIndent3Dot7(indent);
} else {
var parent = _indentStack.last;
// Combine the new indentation with the surrounding one.
var offset = switch ((parent.type, indent)) {
// On the right-hand side of `=`, `:`, or `=>`, don't indent subsequent
// infix operands so that they all align:
//
// variable =
// operand +
// another;
(Indent.assignment, Indent.infix) => 0,
// We have already indented the control flow header, so collapse the
// duplicate indentation.
(Indent.controlFlowClause, Indent.expression) => 0,
(Indent.controlFlowClause, Indent.infix) => 0,
// If we get here, the parent context has no effect, so just apply the
// indentation directly.
(_, _) => indent.spaces,
};
_indentStack.add(_IndentLevel(indent, parent.spaces + offset));
if (debug.traceIndent) {
debug.log('pushIndent: ${_indentStack.join(' ')}');
}
}
}
/// Increases the indentation in a control flow clause in a "collapsible" way.
///
/// This is only used in a couple of corners of if-case and for-in headers
/// where the indentation is unusual.
void pushCollapsibleIndent() {
if (_cache.is3Dot7) {
_pushIndent3Dot7(Indent.expression, canCollapse: true);
} else {
pushIndent(Indent.controlFlowClause);
}
}
/// The 3.7 style of indentation and collapsible indentation tracking.
///
/// Splits in if-case and for-in loop headers are tricky to indent gracefully.
/// For example, if an infix expression inside the case splits, we don't want
/// it to be double indented:
///
/// if (object
/// case veryLongConstant
/// as VeryLongType) {
/// ;
/// }
///
/// That suggests that the [IfCasePiece] shouldn't add indentation for the
/// case pattern since the [InfixPiece] inside it will already indent the RHS.
///
/// But if the case is a variable pattern that splits, the [VariablePiece]
/// does *not* add indentation because in most other places where it occurs,
/// that's what we want. If the [IfCasePiece] doesn't indent the pattern, you
/// get:
///
/// if (object
/// case VeryLongType
/// veryLongVariable
/// ) {
/// ;
/// }
///
/// To deal with this, 3.7 had a notion of "collapsible" indentation. In 3.8
/// and later, there is a different mechanism for merging indentation kinds.
/// This function implements the former.
void _pushIndent3Dot7(Indent indent, {bool canCollapse = false}) {
var parentIndent = _indentStack.last.spaces;
var parentCollapse = _indentStack.last.collapsible;
if (parentCollapse == indent.spaces) {
// We're indenting by the same existing collapsible amount, so collapse
// this new indentation with that existing one.
_indentStack.add(_IndentLevel.v3Dot7(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(
_IndentLevel.v3Dot7(parentIndent + indent.spaces, indent.spaces),
);
} else {
// Regular indentation, so just increase the indent.
_indentStack.add(_IndentLevel.v3Dot7(parentIndent + indent.spaces, 0));
}
}
/// Discards the indentation change from the last call to [pushIndent()].
void popIndent() {
_indentStack.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) {
_applyNewlineToShape(_pieceFormats.last);
_pendingIndent = flushLeft ? 0 : _indentStack.last.spaces;
}
_pendingWhitespace = _pendingWhitespace.collapse(whitespace);
}
/// When a newline is written by the current piece of one of its children,
/// determines how that affects the current piece's shape.
void setShapeMode(ShapeMode mode) {
_pieceFormats.last.mode = mode;
}
/// 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) {
Profile.count('CodeWriter.format() piece separate');
_formatSeparate(piece);
} else {
Profile.count('CodeWriter.format() piece inline');
_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(
piece,
_solution.pieceStateIfBound(piece),
pageWidth: _pageWidth,
indent: _pendingIndent,
subsequentIndent: _indentStack.last.spaces,
);
_pendingIndent = 0;
_flushWhitespace();
_solution.mergeSubtree(solution);
_code.group(solution.code);
}
/// Format [piece] writing directly into this [CodeWriter].
void _formatInline(Piece piece) {
var isUnsolved =
!_solution.isBound(piece) && piece.additionalStates.isNotEmpty;
// See if we can immediately bind it based on the page width and the piece's
// contents.
if (isUnsolved) {
// If the solution doesn't bind the piece already, we may be able to
// eagerly bind it to a state knowing just the page width (minus any
// leading indentation). If so, do that now. We do that here instead of
// pinning the pieces because doing so here lets us take leading
// indication into account which may vary based on the surrounding pieces
// when we get here.
Profile.begin('CodeWriter try to bind by page width');
isUnsolved =
!_solution.tryBindByPageWidth(
piece,
_pageWidth - _indentStack.first.spaces,
);
Profile.end('CodeWriter try to bind by page width');
}
if (isUnsolved) _currentUnsolvedPieces.add(piece);
// Begin a new formatting context for this child.
_pieceFormats.add(_FormatState(piece));
// Format the child piece.
piece.format(this, _solution.pieceState(piece));
var child = _pieceFormats.removeLast();
// Restore the surrounding piece's context.
if (isUnsolved) _currentUnsolvedPieces.removeLast();
// Now that we know the child's shape, see if the parent permits it.
if (_pieceFormats.lastOrNull case var parent?) {
var allowedShapes = parent.piece.allowedChildShapes(
_solution.pieceState(parent.piece),
child.piece,
);
bool invalid;
if (_cache.is3Dot7) {
// If the child must be inline, then invalidate because we know it
// contains some kind of newline.
// TODO(rnystrom): It would be better if this logic wasn't different for
// 3.7. The only place where the distinction between this code and the
// logic in the else clause comes into play is with CaseExpressionPiece.
invalid =
child.shape != Shape.inline &&
allowedShapes.length == 1 &&
allowedShapes.contains(Shape.inline);
} else {
invalid = !allowedShapes.contains(child.shape);
}
if (invalid) _solution.invalidate(parent.piece);
// If the child had newlines, propagate that to the parent's shape.
if (child.shape != Shape.inline) {
_applyNewlineToShape(parent, child.shape);
}
}
}
/// Sets [selectionStart] to be [start] code units into the output.
void startSelection(int start) {
_flushWhitespace();
_code.startSelection(start);
}
/// Sets [selectionEnd] to be [end] code units into the output.
void endSelection(int end) {
_flushWhitespace();
_code.endSelection(end);
}
/// Disables or re-enables formatting in a region of code.
void setFormattingEnabled(bool enabled, int sourceOffset) {
_code.setFormattingEnabled(enabled, sourceOffset);
}
/// 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();
_column = _pendingIndent;
_code.newline(
blank: _pendingWhitespace == Whitespace.blankLine,
indent: _column,
);
case Whitespace.space:
_code.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 are pieces on the line that
// we can try to split, then remember them so that the solution will expand
// them next.
if (_foundExpandLine) return;
if (_currentLinePieces.isNotEmpty &&
(_column > _pageWidth || !_solution.isValid)) {
_expandPieces.addAll(_currentLinePieces);
_foundExpandLine = true;
} else {
// This line was OK, so we don't need to expand the pieces on it.
_currentLinePieces.clear();
}
}
/// Determine how a newline affects the current piece's shape.
void _applyNewlineToShape(_FormatState state, [Shape shape = Shape.other]) {
state.shape = switch (state.mode) {
ShapeMode.merge => state.shape.merge(shape),
ShapeMode.block => Shape.block,
ShapeMode.beforeHeadline => Shape.other,
// If there were no newlines inside the headline, now that there is one,
// we have a headline shape.
ShapeMode.afterHeadline when state.shape == Shape.inline =>
Shape.headline,
// If there was already a newline in the headline, preserve that shape.
ShapeMode.afterHeadline => state.shape,
ShapeMode.other => Shape.other,
};
}
}
/// 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 kind of indentation that a [Piece] may output to control the leading
/// whitespace at the beginning of a line.
///
/// Each indentation type defines the number of spaces it writes. Indentation
/// is also semantic: a type describes *why* it writes that, or what kind of
/// syntax its coming from. This allows us to merge or combine indentation in
/// smarter ways in some contexts.
enum Indent {
// No indentation.
none(0),
/// The right-hand side of an `=`, `:`, or `=>`.
assignment(4),
/// The contents of a block-like structure: block, collection literal,
/// argument list, etc.
block(2),
/// A split cascade chain.
cascade(2),
/// Indentation when splits occur inside for-in and if-case clause headers.
controlFlowClause(4),
/// Any general sort of split expression.
expression(4),
/// "Indentation" for parenthesized expressions and other contexts where we
/// want to prevent some inner expression's indentation from merging with
/// the surrounding one.
grouping(0),
/// An infix operator expression: `+`, `*`, `is`, etc.
infix(4),
/// Constructor initializer when the parameter list doesn't have optional
/// or named parameters.
initializer(2),
/// Constructor initializer when the parameter list does have optional or
/// named parameters.
initializerWithOptionalParameter(3);
/// The number of spaces this type of indentation applies.
final int spaces;
const Indent(this.spaces);
}
/// Information for each piece currently being formatted while [CodeWriter]
/// traverses the piece tree.
class _FormatState {
/// The piece being formatted.
final Piece piece;
/// The piece's shape.
///
/// This changes based on the newlines the piece writes.
Shape shape = Shape.inline;
/// How a newline affects the shape of this piece.
ShapeMode mode = ShapeMode.merge;
_FormatState(this.piece);
}
/// Determines how a newline inside a piece or a child piece affects the shape
/// of the current piece.
enum ShapeMode {
/// The piece's shape is merged with the incoming shape.
///
/// If a newline is written directly by the piece itself, that has shape
/// [Shape.other].
merge,
/// A newline makes this piece block-shaped.
block,
/// We are in the first line of a potentially headline-shaped piece.
///
/// A newline here means it's not headline shaped.
beforeHeadline,
/// We've already written the headline part of a piece so a newline after
/// this is fine and still leaves it headline shaped.
afterHeadline,
/// A newline makes this piece have [Shape.other].
other,
}
/// A level of indentation in the indentation stack.
final class _IndentLevel {
/// The reason this indentation was added.
///
/// Not used for 3.7 style.
final Indent type;
/// The total number of spaces of indentation.
final int spaces;
/// How many spaces of [spaces] can be collapsed with further indentation.
///
/// Only used for 3.7 style.
final int collapsible;
_IndentLevel.v3Dot7(this.spaces, this.collapsible) : type = Indent.none;
_IndentLevel(this.type, this.spaces) : collapsible = 0;
@override
String toString() => '${type.name}:$spaces';
}