blob: 17a9218176ad640a7f11d6935b862820b8ca66c3 [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.
library dart_style.src.chunk;
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.
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;
NestingLevel _nesting;
/// If this chunk marks the beginning of a block, this contains the child
/// chunks and other data about that nested block.
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 != null ? _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.
bool get canDivide {
// Have to call markDivide() before accessing this.
assert(_canDivide != null);
return _canDivide;
}
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 (_block == null) 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}) {
if (flushLeft == null) flushLeft = false;
if (space == null) space = false;
if (rule.isHardened) {
// A hard split always wins.
_rule = rule;
} else if (_rule == null) {
// If the chunk hasn't been initialized yet, just inherit the rule.
_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.
if (_isDouble == null) _isDouble = isDouble;
}
/// Turns this chunk into one that can contain a block of child chunks.
void makeBlock(Chunk blockArgument) {
assert(_block == null);
_block = new 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 getValue(Rule rule)) {
if (_block == null) return false;
if (_block.argument == null) return false;
return _block.argument.rule
.isSplit(getValue(_block.argument.rule), _block.argument);
}
// Mark whether this chunk can divide the range of chunks.
void markDivide(canDivide) {
// Should only do this once.
assert(_canDivide == null);
_canDivide = canDivide;
}
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");
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 collection literal arguments.
///
/// Used to prefer splitting at the argument boundary over splitting the
/// collection contents.
static const splitCollections = 2;
/// Splitting on the "." in a named constructor.
static const constructorName = 3;
/// Splitting a `[...]` index operator.
static const index = 3;
/// 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.
int get start => _start;
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);
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);
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 `*/`.
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,
{this.isLineComment, this.flushLeft});
}