| // 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 'fast_hash.dart'; |
| import 'nesting_level.dart'; |
| import 'rule/rule.dart'; |
| |
| /// Tracks where a selection start or end point may appear in some piece of |
| /// text. |
| abstract class Selection { |
| /// The chunk of text. |
| String get text; |
| |
| /// The offset from the beginning of [text] where the selection starts, or |
| /// `null` if the selection does not start within this chunk. |
| int? get selectionStart => _selectionStart; |
| int? _selectionStart; |
| |
| /// The offset from the beginning of [text] where the selection ends, or |
| /// `null` if the selection does not start within this chunk. |
| int? get selectionEnd => _selectionEnd; |
| int? _selectionEnd; |
| |
| /// Sets [selectionStart] to be [start] characters into [text]. |
| void startSelection(int start) { |
| _selectionStart = start; |
| } |
| |
| /// Sets [selectionStart] to be [fromEnd] characters from the end of [text]. |
| void startSelectionFromEnd(int fromEnd) { |
| _selectionStart = text.length - fromEnd; |
| } |
| |
| /// Sets [selectionEnd] to be [end] characters into [text]. |
| void endSelection(int end) { |
| _selectionEnd = end; |
| } |
| |
| /// Sets [selectionEnd] to be [fromEnd] characters from the end of [text]. |
| void endSelectionFromEnd(int fromEnd) { |
| _selectionEnd = text.length - fromEnd; |
| } |
| } |
| |
| /// A chunk of non-breaking output text that may begin on a newline. |
| /// |
| /// Chunks are created by [ChunkBuilder] and fed into [LineSplitter]. Each |
| /// contains the data describing where the chunk should appear when starting a |
| /// new line, how desireable it is to split, and the subsequent text for that |
| /// line. |
| /// |
| /// Line splitting before a chunk comes in a few different forms. |
| /// |
| /// * A "hard" split is a mandatory newline. The formatted output will contain |
| /// at least one newline before the chunk's text. |
| /// * A "soft" split is a discretionary newline. If a line doesn't fit within |
| /// the page width, one or more soft splits may be turned into newlines to |
| /// wrap the line to fit within the bounds. If a soft split is not turned |
| /// into a newline, it may instead appear as a space or zero-length string |
| /// in the output, depending on [spaceWhenUnsplit]. |
| /// * A "double" split expands to two newlines. In other words, it leaves a |
| /// blank line in the output. Hard or soft splits may be doubled. This is |
| /// determined by [isDouble]. |
| /// |
| /// A split controls the leading spacing of the line before the chunk's text, |
| /// both block-based [indent] and expression-wrapping-based [nesting]. |
| class Chunk extends Selection { |
| /// The literal text output for the chunk. |
| @override |
| String get text => _text; |
| String _text; |
| |
| /// The number of characters of indentation from the left edge of the block |
| /// that contains this chunk. |
| /// |
| /// For top level chunks that are not inside any block, this also includes |
| /// leading indentation. |
| final int indent; |
| |
| /// The expression nesting level preceding this chunk. |
| /// |
| /// This is used to determine how much to increase the indentation when a |
| /// line starts at this chunk. A single statement may be indented multiple |
| /// times if the splits occur in more deeply nested expressions, for example: |
| /// |
| /// // 40 columns | |
| /// someFunctionName(argument, argument, |
| /// argument, anotherFunction(argument, |
| /// argument)); |
| final NestingLevel nesting; |
| |
| /// If this chunk marks the beginning of a block, this contains the child |
| /// chunks and other data about that nested block. |
| /// |
| /// This should only be accessed when [isBlock] is `true`. |
| ChunkBlock get block => _block!; |
| ChunkBlock? _block; |
| |
| /// Whether this chunk has a [block]. |
| bool get isBlock => _block != null; |
| |
| /// The [Rule] that controls when a split should occur before this chunk. |
| /// |
| /// Multiple splits may share a [Rule]. |
| final Rule rule; |
| |
| /// Whether or not an extra blank line should be output before this chunk if |
| /// it splits. |
| bool get isDouble => _isDouble; |
| bool _isDouble; |
| |
| /// If `true`, then the line beginning with this chunk should always be at |
| /// column zero regardless of any indentation or expression nesting. |
| /// |
| /// Used for multi-line strings and commented out code. |
| bool get flushLeft => _flushLeft; |
| bool _flushLeft; |
| |
| /// Whether this chunk should prepend an extra space if it does not split. |
| /// |
| /// This is `true`, for example, in a chunk following a ",". |
| bool get spaceWhenUnsplit => _spaceWhenUnsplit; |
| bool _spaceWhenUnsplit; |
| |
| /// Whether this chunk marks the end of a range of chunks that can be line |
| /// split independently of the following chunks. |
| /// |
| /// You must call markDivide() before accessing this. |
| bool get canDivide => _canDivide; |
| late final bool _canDivide; |
| |
| /// The number of characters in this chunk when unsplit. |
| int get length => (_spaceWhenUnsplit ? 1 : 0) + _text.length; |
| |
| /// The unsplit length of all of this chunk's block contents. |
| /// |
| /// Does not include this chunk's own length, just the length of its child |
| /// block chunks (recursively). |
| int get unsplitBlockLength { |
| if (!isBlock) return 0; |
| |
| var length = 0; |
| for (var chunk in block.chunks) { |
| length += chunk.length + chunk.unsplitBlockLength; |
| } |
| |
| return length; |
| } |
| |
| /// The [Span]s that contain this chunk. |
| final spans = <Span>[]; |
| |
| /// Creates a new empty chunk with the given split properties. |
| Chunk(this.rule, this.indent, this.nesting, |
| {required bool space, required bool flushLeft, required bool isDouble}) |
| : _text = '', |
| _flushLeft = flushLeft, |
| _isDouble = isDouble, |
| _spaceWhenUnsplit = space; |
| |
| /// Creates a dummy chunk. |
| /// |
| /// This is returned in some places by [ChunkBuilder] when there is no useful |
| /// chunk to yield and it will not end up being used by the caller anyway. |
| Chunk.dummy() |
| : _text = '(dummy)', |
| rule = Rule.dummy, |
| indent = 0, |
| nesting = NestingLevel(), |
| _spaceWhenUnsplit = false, |
| _flushLeft = false, |
| _isDouble = false; |
| |
| /// Append [text] to the end of the chunk's text. |
| void appendText(String text) { |
| _text += text; |
| } |
| |
| /// Updates the split information for a previously created chunk in response |
| /// to a split from a comment. |
| void updateSplit({bool? flushLeft, bool isDouble = false, bool? space}) { |
| assert(text.isEmpty); |
| |
| if (flushLeft != null) _flushLeft = flushLeft; |
| |
| // Don't discard an already known blank newline, but do potentially add one. |
| if (isDouble) _isDouble = true; |
| |
| if (space != null) _spaceWhenUnsplit = space; |
| } |
| |
| /// Turns this chunk into one that can contain a block of child chunks. |
| void makeBlock(Chunk? blockArgument) { |
| assert(_block == null); |
| _block = ChunkBlock(blockArgument); |
| } |
| |
| /// Returns `true` if the block body owned by this chunk should be expression |
| /// indented given a set of rule values provided by [getValue]. |
| bool indentBlock(int Function(Rule) getValue) { |
| if (!isBlock) return false; |
| |
| var argument = block.argument; |
| if (argument == null) return false; |
| |
| var rule = argument.rule; |
| |
| // There may be no rule if the block occurs inside a string interpolation. |
| // In that case, it's not clear if anything will look particularly nice, but |
| // expression nesting is probably marginally better. |
| if (rule == Rule.dummy) return true; |
| |
| return rule.isSplit(getValue(rule), argument); |
| } |
| |
| // Mark whether this chunk can divide the range of chunks. |
| void markDivide(bool canDivide) { |
| _canDivide = canDivide; |
| } |
| |
| @override |
| String toString() { |
| var parts = [ |
| 'indent:$indent', |
| if (spaceWhenUnsplit) 'space', |
| if (isDouble) 'double', |
| if (flushLeft) 'flush', |
| '$rule${rule.isHardened ? '!' : ''}', |
| if (rule.constrainedRules.isNotEmpty) |
| "-> ${rule.constrainedRules.join(' ')}" |
| ]; |
| |
| return '[${parts.join(' ')}] `$text`'; |
| } |
| } |
| |
| /// The child chunks owned by a chunk that begins a "block" -- an actual block |
| /// statement, function expression, or collection literal. |
| class ChunkBlock { |
| /// If this block is for a collection literal in an argument list, this will |
| /// be the chunk preceding this literal argument. |
| /// |
| /// That chunk is owned by the argument list and if it splits, this collection |
| /// may need extra expression-level indentation. |
| final Chunk? argument; |
| |
| /// The child chunks in this block. |
| final List<Chunk> chunks = []; |
| |
| ChunkBlock(this.argument); |
| } |
| |
| /// The in-progress state for a [Span] that has been started but has not yet |
| /// been completed. |
| class OpenSpan { |
| /// Index of the first chunk contained in this span. |
| final int start; |
| |
| /// The cost applied when the span is split across multiple lines or `null` |
| /// if the span is for a multisplit. |
| final int cost; |
| |
| OpenSpan(this.start, this.cost); |
| |
| @override |
| String toString() => 'OpenSpan($start, \$$cost)'; |
| } |
| |
| /// Delimits a range of chunks that must end up on the same line to avoid an |
| /// additional cost. |
| /// |
| /// These are used to encourage the line splitter to try to keep things |
| /// together, like parameter lists and binary operator expressions. |
| /// |
| /// This is a wrapper around the cost so that spans have unique identities. |
| /// This way we can correctly avoid paying the cost multiple times if the same |
| /// span is split by multiple chunks. |
| class Span extends FastHash { |
| /// The cost applied when the span is split across multiple lines or `null` |
| /// if the span is for a multisplit. |
| final int cost; |
| |
| Span(this.cost); |
| |
| @override |
| String toString() => '$id\$$cost'; |
| } |
| |
| enum CommentType { |
| /// A `///` or `/**` doc comment. |
| doc, |
| |
| /// A non-doc line comment. |
| line, |
| |
| /// A `/* ... */` comment that should be on its own line. |
| block, |
| |
| /// A `/* ... */` comment that can share a line with other code. |
| inlineBlock, |
| } |
| |
| /// A comment in the source, with a bit of information about the surrounding |
| /// whitespace. |
| class SourceComment extends Selection { |
| /// The text of the comment, including `//`, `/*`, and `*/`. |
| @override |
| final String text; |
| |
| /// What kind of comment this is. |
| final CommentType type; |
| |
| /// The number of newlines between the comment or token preceding this comment |
| /// and the beginning of this one. |
| /// |
| /// Will be zero if the comment is a trailing one. |
| int linesBefore; |
| |
| /// Whether this comment starts at column one in the source. |
| /// |
| /// Comments that start at the start of the line will not be indented in the |
| /// output. This way, commented out chunks of code do not get erroneously |
| /// re-indented. |
| final bool flushLeft; |
| |
| SourceComment(this.text, this.type, this.linesBefore, |
| {required this.flushLeft}); |
| } |