| // 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); |
| } |