| // 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 'package:analyzer/dart/ast/ast.dart'; | 
 | import 'package:analyzer/dart/ast/token.dart'; | 
 |  | 
 | import '../back_end/code_writer.dart'; | 
 | import '../back_end/solution_cache.dart'; | 
 | import '../back_end/solver.dart'; | 
 | import '../dart_formatter.dart'; | 
 | import '../debug.dart' as debug; | 
 | import '../piece/adjacent.dart'; | 
 | import '../piece/list.dart'; | 
 | import '../piece/piece.dart'; | 
 | import '../piece/text.dart'; | 
 | import '../profile.dart'; | 
 | import '../source_code.dart'; | 
 | import 'comment_writer.dart'; | 
 | import 'delimited_list_builder.dart'; | 
 | import 'piece_factory.dart'; | 
 | import 'sequence_builder.dart'; | 
 |  | 
 | /// Builds [TextPiece]s for [Token]s and comments. | 
 | /// | 
 | /// Handles updating selection markers and attaching comments to the tokens | 
 | /// before and after the comments. | 
 | class PieceWriter { | 
 |   final DartFormatter _formatter; | 
 |  | 
 |   final SourceCode _source; | 
 |  | 
 |   final CommentWriter _comments; | 
 |  | 
 |   /// The most recent previously-created [CodePiece]. | 
 |   /// | 
 |   /// We hold a reference to this so we can attach hanging comments to it, | 
 |   /// which we don't discover until we reach the token after the one used to | 
 |   /// create this piece. | 
 |   CodePiece? _previousCode; | 
 |  | 
 |   /// Whether we have reached a token or comment that lies at or beyond the | 
 |   /// selection start offset in the original code. | 
 |   /// | 
 |   /// Makes sure we insert the start marker in some piece even if it happens to | 
 |   /// lie between two tokens in the input. | 
 |   bool _passedSelectionStart = false; | 
 |  | 
 |   /// Whether we have reached a token or comment that lies at or beyond the | 
 |   /// selection end offset in the original code. | 
 |   /// | 
 |   /// Makes sure we insert the end marker in some piece even if it happens to | 
 |   /// lie between two tokens in the input. | 
 |   bool _passedSelectionEnd = false; | 
 |  | 
 |   /// The character offset of the end of the selection with any trailing | 
 |   /// whitespace removed. | 
 |   /// | 
 |   /// This can only be accessed if there is a selection. | 
 |   late final int _selectionEnd = _findSelectionEnd(); | 
 |  | 
 |   /// The stack of pieces being built by calls to [build()]. | 
 |   /// | 
 |   /// Each call to [build()] pushes a new list onto this stack. All of the | 
 |   /// pieces written during that call to [build()] end up in that list. When | 
 |   /// the [build()] callback returns, the topmost list is popped and the result | 
 |   /// returned as an [AdjacentPiece] (or just the single piece if there is | 
 |   /// only one). | 
 |   final List<List<Piece>> _pieces = []; | 
 |  | 
 |   /// The last piece in [_elements], if it's a [CodePiece] that can have more | 
 |   /// code appended to it or `null` if there is no trailing element or the | 
 |   /// trailing piece can't be appended to. | 
 |   CodePiece? _currentCode; | 
 |  | 
 |   /// If [space()] has been called and we haven't appended a space to the | 
 |   /// previous code or adding a [SpacePiece] yet. | 
 |   bool _pendingSpace = false; | 
 |  | 
 |   PieceWriter(this._formatter, this._source, this._comments); | 
 |  | 
 |   /// Wires the [PieceWriter] to the [AstNodeVisitor] (which implements | 
 |   /// [PieceFactory]) so that [PieceWriter] can visit nodes. | 
 |   void bindVisitor(PieceFactory visitor) { | 
 |     _visitor = visitor; | 
 |   } | 
 |  | 
 |   late final PieceFactory _visitor; | 
 |  | 
 |   /// Writes [token] to the piece currently being written. | 
 |   /// | 
 |   /// Does nothing if [token] is `null`. If [spaceBefore] is `true`, writes a | 
 |   /// space before the token, likewise with [spaceAfter]. | 
 |   void token(Token? token, | 
 |       {bool spaceBefore = false, bool spaceAfter = false}) { | 
 |     if (token == null) return; | 
 |  | 
 |     if (spaceBefore) space(); | 
 |  | 
 |     // TODO(perf): If [_currentCode] is `null` but [_pendingSpace] is `true`, | 
 |     // it should be possible to create a new code piece and write the leading | 
 |     // space to it instead of having a leading SpacePiece. Unfortunately, that | 
 |     // sometimes leads to duplicate spaces in the output, so it might take some | 
 |     // tweaking to get working. | 
 |  | 
 |     if (token.precedingComments != null) { | 
 |       // Don't append to the previous token if there is a comment after it. | 
 |       _beginCodeToken(token); | 
 |     } else if (_currentCode case var code?) { | 
 |       // Append to the current code piece. | 
 |       if (_pendingSpace) { | 
 |         code.append(' '); | 
 |         _pendingSpace = false; | 
 |       } | 
 |  | 
 |       _write(code, token.lexeme, token.offset); | 
 |     } else { | 
 |       _beginCodeToken(token); | 
 |     } | 
 |  | 
 |     if (spaceAfter) space(); | 
 |   } | 
 |  | 
 |   /// Writes [token], which may contain internal newlines. | 
 |   void multilineToken(Token token) { | 
 |     var comments = _comments.commentsBefore(token); | 
 |  | 
 |     var piece = CodePiece(_splitComments(comments, token)); | 
 |     _write(piece, token.lexeme, token.offset, multiline: true); | 
 |  | 
 |     // Remember it so we can attach hanging comments later. | 
 |     _previousCode = piece; | 
 |  | 
 |     // Multiline tokens are always their own pieces. | 
 |     add(piece); | 
 |   } | 
 |  | 
 |   /// Visits [node] if not `null` and writes the result. | 
 |   void visit(AstNode? node, | 
 |       {bool spaceBefore = false, | 
 |       bool spaceAfter = false, | 
 |       NodeContext context = NodeContext.none}) { | 
 |     if (node == null) return; | 
 |  | 
 |     if (spaceBefore) space(); | 
 |     _visitor.visitNode(node, context); | 
 |     if (spaceAfter) space(); | 
 |   } | 
 |  | 
 |   /// Appends a space before the previous code being written and the next. | 
 |   void space() { | 
 |     _pendingSpace = true; | 
 |   } | 
 |  | 
 |   /// Writes an optional modifier that precedes other code. | 
 |   void modifier(Token? keyword) { | 
 |     token(keyword, spaceAfter: true); | 
 |   } | 
 |  | 
 |   /// Adds [piece] to the current piece being built. | 
 |   void add(Piece piece) { | 
 |     _flushSpace(); | 
 |     _pieces.last.add(piece); | 
 |     _currentCode = null; | 
 |   } | 
 |  | 
 |   /// Creates a returns a new piece. | 
 |   /// | 
 |   /// Invokes [buildCallback]. All tokens and AST nodes written during that | 
 |   /// callback are collected into the returned piece. | 
 |   /// | 
 |   /// If [metadata] is non-empty, then wraps the resulting piece in another | 
 |   /// piece beginning with that metadata. If [inlineMetadata] is `true`, then | 
 |   /// the metadata is allowed to stay on the same line as the content. | 
 |   /// Otherwise, a newline is inserted after every annotation. | 
 |   Piece build(void Function() buildCallback, | 
 |       {List<Annotation> metadata = const [], bool inlineMetadata = false}) { | 
 |     _flushSpace(); | 
 |     _currentCode = null; | 
 |  | 
 |     var metadataPieces = const <Piece>[]; | 
 |     if (metadata.isNotEmpty) { | 
 |       metadataPieces = [ | 
 |         for (var annotation in metadata) _visitor.nodePiece(annotation) | 
 |       ]; | 
 |     } | 
 |  | 
 |     _pieces.add([]); | 
 |  | 
 |     buildCallback(); | 
 |  | 
 |     _flushSpace(); | 
 |     _currentCode = null; | 
 |  | 
 |     var builtPieces = _pieces.removeLast(); | 
 |     assert(builtPieces.isNotEmpty); | 
 |  | 
 |     var builtPiece = builtPieces.length == 1 | 
 |         ? builtPieces.first | 
 |         : AdjacentPiece(builtPieces); | 
 |  | 
 |     if (metadataPieces.isEmpty) { | 
 |       // No metadata, so return the content piece directly. | 
 |       return builtPiece; | 
 |     } else if (inlineMetadata) { | 
 |       // Wrap the metadata and content in a splittable list. | 
 |       var list = DelimitedListBuilder( | 
 |           _visitor, | 
 |           const ListStyle( | 
 |             commas: Commas.none, | 
 |             spaceWhenUnsplit: true, | 
 |           )); | 
 |  | 
 |       for (var piece in metadataPieces) { | 
 |         list.add(piece); | 
 |       } | 
 |  | 
 |       list.add(builtPiece); | 
 |       return list.build(); | 
 |     } else { | 
 |       // Wrap the metadata and content in a sequence. | 
 |       var sequence = SequenceBuilder(_visitor); | 
 |       for (var piece in metadataPieces) { | 
 |         sequence.add(piece); | 
 |       } | 
 |  | 
 |       sequence.add(builtPiece); | 
 |       return sequence.build(forceSplit: true); | 
 |     } | 
 |   } | 
 |  | 
 |   /// Creates a separate piece for [token], including any comments that should | 
 |   /// be attached to that token. | 
 |   /// | 
 |   /// If [discardedToken] is given, it is a token immediately before [token] | 
 |   /// that is going to be discarded. Passing it in here ensures any comments | 
 |   /// before it are preserved. | 
 |   /// | 
 |   /// If [commaAfter] is `true`, looks for and writes a comma following the | 
 |   /// token if there is one. | 
 |   Piece tokenPiece(Token token, | 
 |       {Token? discardedToken, bool commaAfter = false}) { | 
 |     var tokenPiece = _makeCodePiece(discardedToken: discardedToken, token); | 
 |  | 
 |     if (commaAfter) { | 
 |       var nextToken = token.next!; | 
 |       if (nextToken.lexeme == ',') { | 
 |         return AdjacentPiece([tokenPiece, _makeCodePiece(nextToken)]); | 
 |       } | 
 |     } | 
 |  | 
 |     return tokenPiece; | 
 |   } | 
 |  | 
 |   /// Writes [metadata] followed by the code written by [buildCallback]. | 
 |   /// | 
 |   /// If [metadata] is empty, then invokes [buildCallback] directly. Otherwise, | 
 |   /// creates a new [Piece] that contains the pieces written from [metadata] | 
 |   /// followed by the code written by [buildCallback]. | 
 |   void withMetadata(List<Annotation> metadata, void Function() buildCallback, | 
 |       {bool inlineMetadata = false}) { | 
 |     // If there's no metadata (the common case), then call the callback | 
 |     // directly instead of creating a separate AdjacentBuilder. That way, we | 
 |     // avoid splitting pieces at the boundary here if not needed. | 
 |     if (metadata.isEmpty) { | 
 |       buildCallback(); | 
 |     } else { | 
 |       add(build(buildCallback, | 
 |           metadata: metadata, inlineMetadata: inlineMetadata)); | 
 |     } | 
 |   } | 
 |  | 
 |   // TODO(tall): Much of the comment handling code in CommentWriter got moved | 
 |   // into here, so there isn't great separation of concerns anymore. Can we | 
 |   // organize this code better? Or just combine CommentWriter with this class | 
 |   // completely? | 
 |  | 
 |   /// Creates a new [Piece] for [comment] and returns it. | 
 |   Piece commentPiece(SourceComment comment, | 
 |       [Whitespace trailingWhitespace = Whitespace.none]) { | 
 |     var piece = CommentPiece(trailingWhitespace); | 
 |     _write(piece, comment.text, comment.offset, | 
 |         multiline: comment.type.mayBeMultiline); | 
 |     return piece; | 
 |   } | 
 |  | 
 |   /// Applies any hanging comments before [token] to the preceding [CodePiece] | 
 |   /// and takes and returns any remaining leading comments. | 
 |   List<Piece> takeCommentsBefore(Token token) { | 
 |     return _splitComments(_comments.takeCommentsBefore(token), token); | 
 |   } | 
 |  | 
 |   /// Begins a new [CodeToken] that can potentially have more code written to | 
 |   /// it. | 
 |   void _beginCodeToken(Token token) { | 
 |     _flushSpace(); | 
 |     var code = _makeCodePiece(token); | 
 |     _pieces.last.add(code); | 
 |     _currentCode = code; | 
 |   } | 
 |  | 
 |   /// Outputs any pending space before more code is written or the current | 
 |   /// piece is completed. | 
 |   void _flushSpace() { | 
 |     if (!_pendingSpace) return; | 
 |  | 
 |     _pieces.last.add(SpacePiece()); | 
 |     _pendingSpace = false; | 
 |   } | 
 |  | 
 |   /// Creates a [CodePiece] for [token] and handles any comments that precede | 
 |   /// it, which get attached either as hanging comments on the preceding | 
 |   /// [CodePiece] or leading comments on this one. | 
 |   /// | 
 |   /// If [discardedToken] is given, it is a token immediately before [token] | 
 |   /// that is going to be discarded. Passing it in here ensures any comments | 
 |   /// before it are preserved. | 
 |   CodePiece _makeCodePiece(Token token, {Token? discardedToken}) { | 
 |     var comments = _comments.commentsBefore(token); | 
 |  | 
 |     // Include any comments on the preceding discarded token, if there is one. | 
 |     if (discardedToken != null) { | 
 |       comments = _comments.commentsBefore(discardedToken).concatenate(comments); | 
 |     } | 
 |  | 
 |     var piece = CodePiece(_splitComments(comments, token)); | 
 |     _write(piece, token.lexeme, token.offset); | 
 |  | 
 |     // Remember it so we can attach hanging comments later. | 
 |     return _previousCode = piece; | 
 |   } | 
 |  | 
 |   /// Splits [comments] which precede [token] into [CommentPiece]s that hang | 
 |   /// off the preceding [CodePiece] and those that are leading comments on the | 
 |   /// [CodePiece] for [token]. | 
 |   /// | 
 |   /// Attaches hanging comments to [_previousCode]. Returns the list of leading | 
 |   /// comments that should precede [token]. | 
 |   List<Piece> _splitComments(CommentSequence comments, Token token) { | 
 |     if (comments.isEmpty) return const []; | 
 |  | 
 |     var leadingComments = <Piece>[]; | 
 |     for (var i = 0; i < comments.length; i++) { | 
 |       var comment = comments[i]; | 
 |  | 
 |       // The whitespace after this comment before the next comment or code. | 
 |       var trailingWhitespace = switch (token.lexeme) { | 
 |         _ when comment.requiresNewline => Whitespace.newline, | 
 |         // No space between a comment and delimiting punctuation. | 
 |         ']' || '}' || ',' || ';' => Whitespace.none, | 
 |         _ => Whitespace.space, | 
 |       }; | 
 |  | 
 |       var piece = commentPiece(comment, trailingWhitespace); | 
 |  | 
 |       if (comments.isHanging(i)) { | 
 |         // Attach it to the previous CodePiece. | 
 |         _previousCode!.addHangingComment(piece); | 
 |       } else { | 
 |         // Add it to the list of leading comments for the upcoming token. | 
 |         leadingComments.add(piece); | 
 |       } | 
 |     } | 
 |  | 
 |     return leadingComments; | 
 |   } | 
 |  | 
 |   /// Appends [text] to [piece] and updates any selection markers that fall | 
 |   /// within it. | 
 |   /// | 
 |   /// The [offset] parameter is the offset in the original source code of the | 
 |   /// beginning of where [text] appears. | 
 |   void _write(TextPiece piece, String text, int offset, | 
 |       {bool multiline = false}) { | 
 |     piece.append(text, | 
 |         multiline: multiline, | 
 |         selectionStart: _findSelectionStartWithin(offset, text.length), | 
 |         selectionEnd: _findSelectionEndWithin(offset, text.length)); | 
 |   } | 
 |  | 
 |   /// Finishes writing and returns a [SourceCode] containing the final output | 
 |   /// and updated selection, if any. | 
 |   SourceCode finish(Piece rootPiece) { | 
 |     if (debug.tracePieceBuilder) { | 
 |       debug.log(debug.pieceTree(rootPiece)); | 
 |     } | 
 |  | 
 |     Profile.begin('PieceWriter.finish() format piece tree'); | 
 |  | 
 |     var cache = SolutionCache(); | 
 |     var solver = Solver(cache, | 
 |         pageWidth: _formatter.pageWidth, leadingIndent: _formatter.indent); | 
 |     var solution = solver.format(rootPiece); | 
 |     var (:code, :selectionStart, :selectionEnd) = | 
 |         solution.code.build(_formatter.lineEnding); | 
 |  | 
 |     Profile.end('PieceWriter.finish() format piece tree'); | 
 |  | 
 |     // Be a good citizen, end with a newline. | 
 |     if (_source.isCompilationUnit) code += _formatter.lineEnding!; | 
 |  | 
 |     int? selectionLength; | 
 |     if (_source.selectionStart != null) { | 
 |       // If we haven't hit the beginning and/or end of the selection yet, they | 
 |       // must be at the very end of the code. | 
 |       selectionStart ??= code.length; | 
 |       selectionEnd ??= code.length; | 
 |       selectionLength = selectionEnd - selectionStart; | 
 |     } | 
 |  | 
 |     return SourceCode(code, | 
 |         uri: _source.uri, | 
 |         isCompilationUnit: _source.isCompilationUnit, | 
 |         selectionStart: selectionStart, | 
 |         selectionLength: selectionLength); | 
 |   } | 
 |  | 
 |   /// Returns the number of characters past [position] in the source where the | 
 |   /// selection start appears if it appears within `position + length`. | 
 |   /// | 
 |   /// Returns `null` if the selection start has already been processed or is | 
 |   /// not within that range. | 
 |   int? _findSelectionStartWithin(int position, int length) { | 
 |     // If there is no selection, do nothing. | 
 |     var absoluteStart = _source.selectionStart; | 
 |     if (absoluteStart == null) return null; | 
 |  | 
 |     // If we've already passed it, don't consider it again. | 
 |     if (_passedSelectionStart) return null; | 
 |  | 
 |     // Calculate the start position relative to [offset]. | 
 |     var relativeStart = absoluteStart - position; | 
 |  | 
 |     // If it started in whitespace before this text, push it forward to the | 
 |     // beginning of the non-whitespace text. | 
 |     if (relativeStart < 0) relativeStart = 0; | 
 |  | 
 |     // If we haven't reached it yet, don't consider it. If the start point is | 
 |     // right at the end of the token, don't consider that as reaching it. | 
 |     // Instead, we'll reach it on the next token, which will correctly push | 
 |     // it past any whitespace after this token and move it to the beginning of | 
 |     // the next one. | 
 |     if (relativeStart >= length) return null; | 
 |  | 
 |     // We found it. | 
 |     _passedSelectionStart = true; | 
 |     return relativeStart; | 
 |   } | 
 |  | 
 |   /// Returns the number of characters past [position] in the source where the | 
 |   /// selection endpoint appears if it appears before `position + length`. | 
 |   /// | 
 |   /// Returns `null` if the selection endpoint has already been processed or is | 
 |   /// not within that range. | 
 |   int? _findSelectionEndWithin(int position, int length) { | 
 |     // If there is no selection, do nothing. | 
 |     if (_source.selectionLength == null) return null; | 
 |  | 
 |     // If we've already passed it, don't consider it again. | 
 |     if (_passedSelectionEnd) return null; | 
 |  | 
 |     var relativeEnd = _selectionEnd - position; | 
 |  | 
 |     // If it started in whitespace before this text, push it forward to the | 
 |     // beginning of the non-whitespace text. | 
 |     if (relativeEnd < 0) relativeEnd = 0; | 
 |  | 
 |     // If we haven't reached the end point yet, don't consider it. Note that, | 
 |     // unlike [_findSelectionStartWithin], we do consider the end point being | 
 |     // right at the end of this token to be reaching it. That way, we don't | 
 |     // push the end point *past* the next span of whitespace and instead pull | 
 |     // it tight to the end of this text. | 
 |     if (relativeEnd > length) return null; | 
 |  | 
 |     // In [_findSelectionStartWithin], if the start marker is between two | 
 |     // tokens, we push it forward to the next one. In the above statement, we | 
 |     // push the end marker earlier to the previous token. If the entire | 
 |     // selection is in whitespace between two tokens, that would cause the | 
 |     // start and ends to cross. Prevent that and instead push the end marker | 
 |     // to the beginning of the next token where the start marker will also be | 
 |     // pushed. | 
 |     if (relativeEnd == length && _selectionEnd == _source.selectionStart!) { | 
 |       return null; | 
 |     } | 
 |  | 
 |     // We found it. | 
 |     _passedSelectionEnd = true; | 
 |  | 
 |     return relativeEnd; | 
 |   } | 
 |  | 
 |   /// Calculates the character offset in the source text of the end of the | 
 |   /// selection. | 
 |   /// | 
 |   /// Removes any trailing whitespace from the selection. For example, if the | 
 |   /// original selection markers are: | 
 |   /// | 
 |   ///     function(lotsOfSpac‹eAfter,     ›     andBefore); | 
 |   /// | 
 |   /// Then this function moves the end marker to: | 
 |   /// | 
 |   ///     function(lotsOfSpac‹eAfter,›          andBefore); | 
 |   /// | 
 |   /// We do this because the formatter itself rewrites whitespace so it's not | 
 |   /// useful or even meaningful to try to preserve a selection's location within | 
 |   /// whitespace. Instead, we "rubberband" the end marker forward to the nearest | 
 |   /// non-whitespace character. | 
 |   int _findSelectionEnd() { | 
 |     var end = _source.selectionStart! + _source.selectionLength!; | 
 |  | 
 |     // If the selection bumps to the end of the source, pin it there. | 
 |     if (end == _source.text.length) return end; | 
 |  | 
 |     // Trim off any trailing whitespace. | 
 |     while (end > _source.selectionStart!) { | 
 |       // Stop if we hit anything other than space, tab, newline or carriage | 
 |       // return. | 
 |       var char = _source.text.codeUnitAt(end - 1); | 
 |       if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) { | 
 |         break; | 
 |       } | 
 |  | 
 |       end--; | 
 |     } | 
 |  | 
 |     return end; | 
 |   } | 
 | } |