blob: ec83adc24be9e0114554dcf946bcfa402f4c6551 [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 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});
}