| // Copyright (c) 2014, 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 'chunk.dart'; |
| import 'dart_formatter.dart'; |
| import 'debug.dart' as debug; |
| import 'line_splitting/line_splitter.dart'; |
| |
| /// Given a series of chunks, splits them into lines and writes the result to |
| /// a buffer. |
| class LineWriter { |
| final _buffer = StringBuffer(); |
| |
| final List<Chunk> _chunks; |
| |
| final String _lineEnding; |
| |
| /// The number of characters allowed in a single line. |
| final int pageWidth; |
| |
| /// The number of characters of additional indentation to apply to each line. |
| /// |
| /// This is used when formatting blocks to get the output into the right |
| /// column based on where the block appears. |
| final int _blockIndentation; |
| |
| /// The cache of blocks that have already been formatted. |
| final Map<_BlockKey, FormatResult> _blockCache; |
| |
| /// The offset in [_buffer] where the selection starts in the formatted code. |
| /// |
| /// This will be `null` if there is no selection or the writer hasn't reached |
| /// the beginning of the selection yet. |
| int? _selectionStart; |
| |
| /// The offset in [_buffer] where the selection ends in the formatted code. |
| /// |
| /// This will be `null` if there is no selection or the writer hasn't reached |
| /// the end of the selection yet. |
| int? _selectionEnd; |
| |
| /// The number of characters that have been written to the output. |
| int get length => _buffer.length; |
| |
| LineWriter(DartFormatter formatter, this._chunks) |
| : _lineEnding = formatter.lineEnding!, |
| pageWidth = formatter.pageWidth, |
| _blockIndentation = 0, |
| _blockCache = {}; |
| |
| /// Creates a line writer for a block. |
| LineWriter._(this._chunks, this._lineEnding, this.pageWidth, |
| this._blockIndentation, this._blockCache); |
| |
| /// Gets the results of formatting the child block of [chunk] at starting |
| /// [column]. |
| /// |
| /// If that block has already been formatted, reuses the results. |
| /// |
| /// The column is the column for the delimiters. The contents of the block |
| /// are always implicitly one level deeper than that. |
| /// |
| /// main() { |
| /// function(() { |
| /// block; |
| /// }); |
| /// } |
| /// |
| /// When we format the function expression's body, [column] will be 2, not 4. |
| FormatResult formatBlock(BlockChunk chunk, int column) { |
| var key = _BlockKey(chunk, column); |
| |
| // Use the cached one if we have it. |
| var cached = _blockCache[key]; |
| if (cached != null) return cached; |
| |
| var writer = LineWriter._( |
| chunk.children, _lineEnding, pageWidth, column, _blockCache); |
| return _blockCache[key] = writer.writeLines(); |
| } |
| |
| /// Takes all of the chunks and divides them into sublists and line splits |
| /// each list. |
| /// |
| /// Since this is linear and line splitting is worse it's good to feed the |
| /// line splitter smaller lists of chunks when possible. |
| FormatResult writeLines({bool isCompilationUnit = false}) { |
| // Now that we know what hard splits there will be, break the chunks into |
| // independently splittable lines. |
| var totalCost = 0; |
| var start = 0; |
| |
| for (var i = 0; i < _chunks.length; i++) { |
| var chunk = _chunks[i]; |
| if (!chunk.canDivide) continue; |
| |
| totalCost += _completeLine(start, i); |
| start = i; |
| } |
| |
| if (start < _chunks.length) { |
| totalCost += _completeLine(start, _chunks.length); |
| } |
| |
| // Be a good citizen, end with a newline. |
| if (isCompilationUnit) _buffer.write(_lineEnding); |
| |
| return FormatResult( |
| _buffer.toString(), totalCost, _selectionStart, _selectionEnd); |
| } |
| |
| /// Takes the chunks from [start] to [end], removes them, and runs the |
| /// [LineSplitter] on them. |
| int _completeLine(int start, int end) { |
| var chunks = _chunks.sublist(start, end); |
| |
| if (debug.traceLineWriter) { |
| debug.log(debug.green('\nWriting:')); |
| debug.dumpChunks(0, chunks); |
| debug.log(); |
| } |
| |
| // Run the line splitter. |
| var splitter = LineSplitter(this, chunks, _blockIndentation); |
| var splits = splitter.apply(); |
| |
| // Write each chunk with the appropriate splits between them. |
| for (var i = 0; i < chunks.length; i++) { |
| var chunk = chunks[i]; |
| |
| // Write the block chunk's children first. |
| if (chunk is BlockChunk) { |
| if (!splits.shouldSplitAt(i)) { |
| // This block didn't split (which implies none of the child blocks |
| // of that block split either, recursively), so write them all inline. |
| _writeChunksUnsplit(chunk); |
| } else { |
| _buffer.write(_lineEnding); |
| |
| // Include the formatted block contents. |
| var block = formatBlock(chunk, splits.getColumn(i)); |
| |
| // If this block contains one of the selection markers, tell the |
| // writer where it ended up in the final output. |
| if (block.selectionStart != null) { |
| _selectionStart = length + block.selectionStart!; |
| } |
| |
| if (block.selectionEnd != null) { |
| _selectionEnd = length + block.selectionEnd!; |
| } |
| |
| _buffer.write(block.text); |
| } |
| } |
| |
| if (splits.shouldSplitAt(i)) { |
| // Don't write an initial single newline at the beginning of the output. |
| // If this is for a block, then the newline will be written before |
| // writing the block. If it's the top level output, then it shouldn't |
| // have an extra leading newline. |
| if (_buffer.isNotEmpty) { |
| _buffer.write(_lineEnding); |
| if (chunk.isDouble) _buffer.write(_lineEnding); |
| } |
| |
| _buffer.write(' ' * (splits.getColumn(i))); |
| } else { |
| if (chunk.spaceWhenUnsplit) _buffer.write(' '); |
| } |
| |
| _writeChunk(chunk); |
| } |
| |
| return splits.cost; |
| } |
| |
| /// Writes the block chunks of [block] (and any child chunks of them, |
| /// recursively) without any splitting. |
| void _writeChunksUnsplit(BlockChunk block) { |
| for (var chunk in block.children) { |
| if (chunk.spaceWhenUnsplit) _buffer.write(' '); |
| |
| // Recurse into the block. |
| if (chunk is BlockChunk) _writeChunksUnsplit(chunk); |
| |
| _writeChunk(chunk); |
| } |
| } |
| |
| /// Writes [chunk] to the output and updates the selection if the chunk |
| /// contains a selection marker. |
| void _writeChunk(Chunk chunk) { |
| if (chunk.selectionStart != null) { |
| _selectionStart = length + chunk.selectionStart!; |
| } |
| |
| if (chunk.selectionEnd != null) { |
| _selectionEnd = length + chunk.selectionEnd!; |
| } |
| |
| _buffer.write(chunk.text); |
| } |
| } |
| |
| /// Key type for the formatted block cache. |
| /// |
| /// To cache formatted blocks, we just need to know which block it is (the |
| /// index of its parent chunk) and how far it was indented when we formatted it |
| /// (the starting column). |
| class _BlockKey { |
| /// The index of the chunk in the surrounding chunk list that contains this |
| /// block. |
| final Chunk chunk; |
| |
| /// The absolute zero-based column number where the block starts. |
| final int column; |
| |
| _BlockKey(this.chunk, this.column); |
| |
| @override |
| bool operator ==(other) { |
| if (other is! _BlockKey) return false; |
| return chunk == other.chunk && column == other.column; |
| } |
| |
| @override |
| int get hashCode => chunk.hashCode ^ column.hashCode; |
| } |
| |
| /// The result of formatting a series of chunks. |
| class FormatResult { |
| /// The resulting formatted text, including newlines and leading whitespace |
| /// to reach the proper column. |
| final String text; |
| |
| /// The numeric cost of the chosen solution. |
| final int cost; |
| |
| /// Where in the resulting buffer the selection starting point should appear |
| /// if it was contained within this split list of chunks. |
| /// |
| /// Otherwise, this is `null`. |
| final int? selectionStart; |
| |
| /// Where in the resulting buffer the selection end point should appear if it |
| /// was contained within this split list of chunks. |
| /// |
| /// Otherwise, this is `null`. |
| final int? selectionEnd; |
| |
| FormatResult(this.text, this.cost, this.selectionStart, this.selectionEnd); |
| } |