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