blob: 02dbfb6265c849b02ecdde934ebabd66de4adb25 [file] [log] [blame]
// 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 terminated by a hard or soft newline.
///
/// Chunks are created by [LineWriter] and fed into [LineSplitter]. Each
/// contains some text, along with the data needed to tell how the next line
/// should be formatted and how desireable it is to split after the chunk.
///
/// Line splitting after chunks comes in a few different forms.
///
/// * A "hard" split is a mandatory newline. The formatted output will contain
/// at least one newline after 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 subsequent line, 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.
int? get indent => _indent;
int? _indent;
/// The expression nesting level following this chunk.
///
/// This is used to determine how much to increase the indentation when a
/// line starts after 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));
NestingLevel get nesting => _nesting;
late 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;
/// Whether it's valid to add more text to this chunk or not.
///
/// Chunks are built up by adding text and then "capped off" by having their
/// split information set by calling [handleSplit]. Once the latter has been
/// called, no more text should be added to the chunk since it would appear
/// *before* the split.
bool get canAddText => _rule == null;
/// The [Rule] that controls when a split should occur after this chunk.
///
/// Multiple splits may share a [Rule].
Rule? get rule => _rule;
Rule? _rule;
/// Whether or not an extra blank line should be output after this chunk if
/// it's split.
///
/// Internally, this can be either `true`, `false`, or `null`. The latter is
/// an indeterminate state that lets later modifications to the split decide
/// whether it should be double or not.
///
/// However, this getter does not expose that. It will return `false` if the
/// chunk is still indeterminate.
bool get isDouble => _isDouble ?? false;
bool? _isDouble;
/// If `true`, then the line after 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 = false;
/// If `true`, then the line after this chunk and its contained block should
/// be flush left.
bool get flushLeftAfter {
if (!isBlock) return _flushLeft;
return block.chunks.last.flushLeftAfter;
}
/// Whether this chunk should append an extra space if it does not split.
///
/// This is `true`, for example, in a chunk that ends with a ",".
bool get spaceWhenUnsplit => _spaceWhenUnsplit;
bool _spaceWhenUnsplit = false;
/// 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 => _text.length + (spaceWhenUnsplit ? 1 : 0);
/// 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 chunk starting with [_text].
Chunk(this._text);
/// Discard the split for the chunk and put it back into the state where more
/// text can be appended.
void allowText() {
_rule = null;
}
/// Append [text] to the end of the split's text.
void appendText(String text) {
assert(canAddText);
_text += text;
}
/// Finishes off this chunk with the given [rule] and split information.
///
/// This may be called multiple times on the same split since the splits
/// produced by walking the source and the splits coming from comments and
/// preserved whitespace often overlap. When that happens, this has logic to
/// combine that information into a single split.
void applySplit(Rule rule, int indent, NestingLevel nesting,
{bool? flushLeft, bool? isDouble, bool? space}) {
flushLeft ??= false;
space ??= false;
if (rule.isHardened) {
// A hard split always wins.
_rule = rule;
} else {
_rule ??= rule;
}
// Last split settings win.
_flushLeft = flushLeft;
_nesting = nesting;
_indent = indent;
_spaceWhenUnsplit = space;
// Pin down the double state, if given and we haven't already.
_isDouble ??= isDouble;
}
/// 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;
return argument.rule!.isSplit(getValue(argument.rule!), argument);
}
// Mark whether this chunk can divide the range of chunks.
void markDivide(bool canDivide) {
_canDivide = canDivide;
}
@override
String toString() {
var parts = [];
if (text.isNotEmpty) parts.add(text);
if (_indent != null) parts.add('indent:$_indent');
if (spaceWhenUnsplit == true) parts.add('space');
if (_isDouble == true) parts.add('double');
if (_flushLeft == true) parts.add('flush');
var rule = _rule;
if (rule == null) {
parts.add('(no split)');
} else {
parts.add(rule.toString());
if (rule.isHardened) parts.add('(hard)');
if (rule.constrainedRules.isNotEmpty) {
parts.add("-> ${rule.constrainedRules.join(' ')}");
}
}
return parts.join(' ');
}
}
/// 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);
}
/// Constants for the cost heuristics used to determine which set of splits is
/// most desirable.
class Cost {
/// The cost of splitting after the `=>` in a lambda or arrow-bodied member.
///
/// We make this zero because there is already a span around the entire body
/// and we generally do prefer splitting after the `=>` over other places.
static const arrow = 0;
/// The default cost.
///
/// This isn't zero because we want to ensure all splitting has *some* cost,
/// otherwise, the formatter won't try to keep things on one line at all.
/// Most splits and spans use this. Greater costs tend to come from a greater
/// number of nested spans.
static const normal = 1;
/// Splitting after a "=".
static const assign = 1;
/// Splitting after a "=" when the right-hand side is a collection or cascade.
static const assignBlock = 2;
/// Splitting before the first argument when it happens to be a function
/// expression with a block body.
static const firstBlockArgument = 2;
/// The series of positional arguments.
static const positionalArguments = 2;
/// Splitting inside the brackets of a list with only one element.
static const singleElementList = 2;
/// Splitting the internals of block arguments.
///
/// Used to prefer splitting at the argument boundary over splitting the block
/// contents.
static const splitBlocks = 2;
/// Splitting on the "." in a named constructor.
static const constructorName = 4;
/// Splitting a `[...]` index operator.
static const index = 4;
/// Splitting before a type argument or type parameter.
static const typeArgument = 4;
/// Split between a formal parameter name and its type.
static const parameterType = 4;
}
/// 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';
}
/// 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;
/// 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 is a line comment.
final bool isLineComment;
/// 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;
/// Whether this comment is an inline block comment.
bool get isInline => linesBefore == 0 && !isLineComment;
SourceComment(this.text, this.linesBefore,
{required this.isLineComment, required this.flushLeft});
}