| // Copyright (c) 2024, 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 '../source_code.dart'; |
| |
| /// Base class for an object that represents fully formatted code. |
| /// |
| /// We use this instead of immediately generating a string for the resulting |
| /// formatted code because of separate formatting. Often, a subtree of the |
| /// [Piece] tree can be solved and formatted separately. The resulting |
| /// [Solution] may be used by multiple different surrounding solutions while |
| /// the [Solver] works its magic looking for the best solution. When a |
| /// separately formatted child solution is merged into its parent, we want that |
| /// to be fast. Appending strings to a [StringBuffer] is fairly fast, but not |
| /// as fast simply appending a single [GroupCode] to the parent solution's |
| /// [GroupCode]. |
| sealed class Code { |
| /// Traverse the [Code] tree and generate a string showing the [Code] tree's |
| /// structure for debugging purposes. |
| String toCodeTree() { |
| var buffer = StringBuffer(); |
| var prefix = ''; |
| |
| void write(String text) { |
| if (buffer.isNotEmpty) buffer.write(prefix); |
| buffer.writeln(text); |
| } |
| |
| void trace(Code code) { |
| switch (code) { |
| case _NewlineCode(): |
| write('Newline(blank: ${code._blank}, indent: ${code._indent})'); |
| |
| case _TextCode(): |
| write('`${code._text}`'); |
| |
| case GroupCode(): |
| write('Group(indent: ${code._indent}):'); |
| prefix += '| '; |
| for (var child in code._children) { |
| trace(child); |
| } |
| prefix = prefix.substring(2); |
| |
| case _MarkerCode(): |
| write('Marker(${code._marker}, offset: ${code._offset})'); |
| |
| case _EnableFormattingCode(): |
| write( |
| 'EnableFormattingCode(enabled: ${code._enabled}, ' |
| 'offset: ${code._sourceOffset})', |
| ); |
| } |
| } |
| |
| trace(this); |
| |
| return buffer.toString(); |
| } |
| |
| /// Write the [Code] to a string of output code, ignoring selection and |
| /// format on/off markers. |
| String toDebugString() { |
| var builder = _DebugStringBuilder(); |
| builder.traverse(this); |
| return builder.finish(); |
| } |
| } |
| |
| /// A [Code] object which can be written to and contain other child [Code] |
| /// objects. |
| final class GroupCode extends Code { |
| /// How many spaces the first text inside this group should be indented. |
| final int _indent; |
| |
| /// The child [Code] objects contained in this group. |
| final List<Code> _children = []; |
| |
| GroupCode(this._indent); |
| |
| /// Appends [text] to this code. |
| void write(String text) { |
| _children.add(_TextCode(text)); |
| } |
| |
| /// Writes a newline and the subsequent indentation to this code. |
| /// |
| /// If [blank] is `true`, then a blank line is written. Otherwise, only a |
| /// single newline is written. The [indent] parameter is the number of spaces |
| /// of leading indentation on the next line after the newline. |
| void newline({required bool blank, required int indent}) { |
| // Don't insert a redundant newline at the top of a group. |
| if (_children.isNotEmpty) { |
| _children.add(_NewlineCode(blank: blank, indent: indent)); |
| } |
| } |
| |
| /// Adds an entire existing code [group] as a child of this one. |
| void group(GroupCode group) { |
| _children.add(group); |
| } |
| |
| /// Mark the selection start as occurring [offset] characters after the code |
| /// that has already been written. |
| void startSelection(int offset) { |
| _children.add(_MarkerCode(_Marker.start, offset)); |
| } |
| |
| /// Mark the selection end as occurring [offset] characters after the code |
| /// that has already been written. |
| void endSelection(int offset) { |
| _children.add(_MarkerCode(_Marker.end, offset)); |
| } |
| |
| /// Disables or re-enables formatting in a region of code. |
| void setFormattingEnabled(bool enabled, int sourceOffset) { |
| _children.add(_EnableFormattingCode(enabled, sourceOffset)); |
| } |
| |
| /// Traverse the [Code] tree and build the final formatted string. |
| /// |
| /// Whenever a newline is written, writes [lineEnding]. If omitted, defaults |
| /// to '\n'. |
| /// |
| /// Returns the formatted string and the selection markers if there are any. |
| SourceCode build(SourceCode source, [String? lineEnding]) { |
| lineEnding ??= '\n'; |
| |
| var builder = _StringBuilder(source, lineEnding); |
| builder.traverse(this); |
| return builder.finish(); |
| } |
| } |
| |
| /// A [Code] object for a newline followed by any leading indentation. |
| final class _NewlineCode extends Code { |
| /// True if a blank line (two newlines) should be written. |
| final bool _blank; |
| |
| /// The number of spaces of indentation after this newline. |
| final int _indent; |
| |
| _NewlineCode({required bool blank, required int indent}) |
| : _indent = indent, |
| _blank = blank; |
| } |
| |
| /// A [Code] object for literal source text. |
| final class _TextCode extends Code { |
| final String _text; |
| |
| _TextCode(this._text); |
| } |
| |
| /// Marks the location of the beginning or end of a selection as occurring |
| /// [_offset] characters past the point where this marker object appears in the |
| /// list of [Code] objects. |
| final class _MarkerCode extends Code { |
| /// What kind of selection endpoint is being marked. |
| final _Marker _marker; |
| |
| /// The number of characters into the next [Code] object where the marker |
| /// should appear in the resulting output. |
| final int _offset; |
| |
| _MarkerCode(this._marker, this._offset); |
| } |
| |
| final class _EnableFormattingCode extends Code { |
| /// Whether this comment disables formatting (`format off`) or re-enables it |
| /// (`format on`). |
| final bool _enabled; |
| |
| /// The number of code points from the beginning of the unformatted source |
| /// where the unformatted code should begin or end. |
| /// |
| /// If this piece is for `// dart format off`, then the offset is just past |
| /// the `off`. If this piece is for `// dart format on`, it points to just |
| /// before `//`. |
| final int _sourceOffset; |
| |
| _EnableFormattingCode(this._enabled, this._sourceOffset); |
| } |
| |
| /// Which selection marker is pointed to by a [_MarkerCode]. |
| enum _Marker { start, end } |
| |
| /// Traverses a [Code] tree and produces the final string of output code and |
| /// the selection markers, if any. |
| final class _StringBuilder { |
| /// Pre-calculated whitespace strings for various common levels of |
| /// indentation. |
| /// |
| /// Generating these ahead of time is faster than concatenating multiple |
| /// spaces at runtime. |
| static const _indents = { |
| 2: ' ', |
| 4: ' ', |
| 6: ' ', |
| 8: ' ', |
| 10: ' ', |
| 12: ' ', |
| 14: ' ', |
| 16: ' ', |
| 18: ' ', |
| 20: ' ', |
| 22: ' ', |
| 24: ' ', |
| 26: ' ', |
| 28: ' ', |
| 30: ' ', |
| 32: ' ', |
| 34: ' ', |
| 36: ' ', |
| 38: ' ', |
| 40: ' ', |
| 42: ' ', |
| 44: ' ', |
| 46: ' ', |
| 48: ' ', |
| 50: ' ', |
| 52: ' ', |
| 54: ' ', |
| 56: ' ', |
| 58: ' ', |
| 60: ' ', |
| }; |
| |
| final SourceCode _source; |
| final String _lineEnding; |
| final StringBuffer _buffer = StringBuffer(); |
| |
| /// The offset from the beginning of the source to where the selection start |
| /// marker is, if there is one. |
| int? _selectionStart; |
| |
| /// The offset from the beginning of the source to where the selection end |
| /// marker is, if there is one. |
| int? _selectionEnd; |
| |
| /// How many spaces of indentation should be written before the next text. |
| int _indent = 0; |
| |
| /// If formatting has been disabled, then this is the offset from the |
| /// beginning of the source, to where the disabled formatting begins. |
| /// |
| /// Otherwise, -1 to indicate that formatting is enabled. |
| int _disableFormattingStart = -1; |
| |
| _StringBuilder(this._source, this._lineEnding); |
| |
| void traverse(Code code) { |
| switch (code) { |
| case _NewlineCode(): |
| // If formatting has been disabled, then don't write the formatted |
| // output. The unformatted output will be written when formatting is |
| // re-enabled. |
| if (_disableFormattingStart == -1) { |
| _buffer.write(_lineEnding); |
| if (code._blank) _buffer.write(_lineEnding); |
| _indent = code._indent; |
| } |
| |
| case _TextCode(): |
| // If formatting has been disabled, then don't write the formatted |
| // output. The unformatted output will be written when formatting is |
| // re-enabled. |
| if (_disableFormattingStart == -1) { |
| // Write any pending indentation. |
| _buffer.write(_indents[_indent] ?? (' ' * _indent)); |
| _indent = 0; |
| |
| _buffer.write(code._text); |
| } |
| |
| case GroupCode(): |
| _indent = code._indent; |
| for (var i = 0; i < code._children.length; i++) { |
| var child = code._children[i]; |
| traverse(child); |
| } |
| |
| case _MarkerCode(): |
| if (_disableFormattingStart == -1) { |
| // Calculate the absolute offset from the beginning of the formatted |
| // output where the selection marker will appear based on how much |
| // formatted output we've written, pending indentation, and then the |
| // relative offset of the marker into the subsequent [Code] we will |
| // write. |
| var absolutePosition = _buffer.length + _indent + code._offset; |
| switch (code._marker) { |
| case _Marker.start: |
| _selectionStart = absolutePosition; |
| case _Marker.end: |
| _selectionEnd = absolutePosition; |
| } |
| } else { |
| // The marker appears inside a region where formatting is disabled. |
| // In that case, calculating where the marker will end up in the |
| // final formatted output is more complicated because we haven't |
| // actually written any of the code between the `// dart format off` |
| // comment and this marker to [_buffer] yet. However, we do know the |
| // *absolute* position of the selection markers in the original |
| // source. |
| // |
| // Let's say the source file looks like: |
| // |
| // 1 2 3 |
| // 0123456789012345678901234567890123456789 |
| // bef + ore off code | inside on more |
| // |
| // Here, `bef + ore` is some amount of code appearing before |
| // formatting is disabled, `off` is the `// dart format off` comment, |
| // `code` is some code inside the unformatted region, `|` is the |
| // selection marker, `inside` is more code in the unformatted region, |
| // `on` turns formatting back on, and `more` is formatted code at the |
| // end. |
| // |
| // We know the beginning of the unformatted region is at offset 15 |
| // (just after the comment) in the original source. We know the |
| // selection marker is at offset 21 in the original source. From that, |
| // we know the selection marker should end up 6 code points after the |
| // beginning of the unformatted region in the resulting output. |
| switch (code._marker) { |
| case _Marker.start: |
| // Calculate how far into the unformatted code where the marker |
| // should appear. |
| var markerOffsetInUnformatted = |
| _source.selectionStart! - _disableFormattingStart; |
| _selectionStart = _buffer.length + markerOffsetInUnformatted; |
| |
| case _Marker.end: |
| var end = _source.selectionStart! + _source.selectionLength!; |
| |
| // Calculate how far into the unformatted code where the marker |
| // should appear. |
| var markerOffsetInUnformatted = end - _disableFormattingStart; |
| _selectionEnd = _buffer.length + markerOffsetInUnformatted; |
| } |
| } |
| |
| case _EnableFormattingCode(_enabled: false): |
| // Region markers don't nest. If we've already turned off formatting, |
| // then ignore any subsequent `// dart format off` comments until it's |
| // been turned back on. |
| if (_disableFormattingStart == -1) { |
| _disableFormattingStart = code._sourceOffset; |
| } |
| |
| case _EnableFormattingCode(_enabled: true): |
| // If we didn't disable formatting, then enabling it does nothing. |
| if (_disableFormattingStart != -1) { |
| // Write all of the unformatted text from the `// dart format off` |
| // comment to the end of the `// dart format on` comment. |
| _buffer.write( |
| _source.text.substring(_disableFormattingStart, code._sourceOffset), |
| ); |
| _disableFormattingStart = -1; |
| } |
| } |
| } |
| |
| SourceCode finish() { |
| if (_disableFormattingStart != -1) { |
| // Formatting was disabled and never re-enabled, so write the rest of the |
| // source file as unformatted text. |
| _buffer.write(_source.text.substring(_disableFormattingStart)); |
| } else if (_source.isCompilationUnit) { |
| // Be a good citizen, end with a newline. |
| _buffer.write(_lineEnding); |
| } |
| |
| var selectionStart = _selectionStart; |
| 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 ??= _buffer.length; |
| var selectionEnd = _selectionEnd ?? _buffer.length; |
| selectionLength = selectionEnd - selectionStart; |
| } |
| |
| return SourceCode( |
| _buffer.toString(), |
| uri: _source.uri, |
| isCompilationUnit: _source.isCompilationUnit, |
| selectionStart: selectionStart, |
| selectionLength: selectionLength, |
| ); |
| } |
| } |
| |
| /// Traverses a [Code] tree and produces a string of output code, ignoring |
| /// selection and format on/off markers. |
| /// |
| /// This is a simpler version of [_StringBuilder] that doesn't require having |
| /// access to the original [SourceCode] and line ending. |
| final class _DebugStringBuilder { |
| final StringBuffer _buffer = StringBuffer(); |
| |
| /// How many spaces of indentation should be written before the next text. |
| int _indent = 0; |
| |
| void traverse(Code code) { |
| switch (code) { |
| case _NewlineCode(): |
| _buffer.writeln(); |
| if (code._blank) _buffer.writeln(); |
| _indent = code._indent; |
| |
| case _TextCode(): |
| // Write any pending indentation. |
| _buffer.write(' ' * _indent); |
| _indent = 0; |
| _buffer.write(code._text); |
| |
| case GroupCode(): |
| _indent = code._indent; |
| for (var i = 0; i < code._children.length; i++) { |
| traverse(code._children[i]); |
| } |
| |
| case _MarkerCode(): |
| case _EnableFormattingCode(): |
| // The debug output doesn't support disabled formatting or selections. |
| break; |
| } |
| } |
| |
| String finish() => _buffer.toString(); |
| } |