blob: 8a2bc4b5dd32fe6968b7586d89a8d3765873968c [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 'dart:math' as math;
import 'chunk.dart';
import 'comment_type.dart';
import 'constants.dart';
import 'dart_formatter.dart';
import 'debug.dart' as debug;
import 'line_writer.dart';
import 'nesting_builder.dart';
import 'nesting_level.dart';
import 'rule/rule.dart';
import 'source_code.dart';
import 'source_comment.dart';
import 'style_fix.dart';
/// Matches if the last character of a string is an identifier character.
final _trailingIdentifierChar = RegExp(r'[a-zA-Z0-9_]$');
/// Matches a JavaDoc-style doc comment that starts with "/**" and ends with
/// "*/" or "**/".
final _javaDocComment = RegExp(r'^/\*\*([^*/][\s\S]*?)\*?\*/$');
/// Matches the leading "*" in a line in the middle of a JavaDoc-style comment.
final _javaDocLine = RegExp(r'^\s*\*(.*)');
/// Matches spaces at the beginning of as string.
final _leadingIndentation = RegExp(r'^(\s*)');
/// Takes the incremental serialized output of [SourceVisitor]--the source text
/// along with any comments and preserved whitespace--and produces a coherent
/// tree of [Chunk]s which can then be split into physical lines.
///
/// Keeps track of leading indentation, expression nesting, and all of the hairy
/// code required to seamlessly integrate existing comments into the pure
/// output produced by [SourceVisitor].
class ChunkBuilder {
final DartFormatter _formatter;
/// The builder for the code surrounding the block that this writer is for, or
/// `null` if this is writing the top-level code.
final ChunkBuilder? _parent;
final SourceCode _source;
final List<Chunk> _chunks;
/// The number of newlines that should be written before the next
/// non-whitespace token.
///
/// This will always be 0, 1, or 2.
int _pendingNewlines = 0;
/// Whether a non-breaking space should be written before the next text.
bool _pendingSpace = false;
/// Whether the next chunk should use expression nesting.
bool _pendingNested = false;
/// Whether the next chunk should be flush left.
bool _pendingFlushLeft = false;
/// Whether the most recently written output was a comment.
bool _afterComment = false;
/// The nested stack of rules that are currently in use.
///
/// New chunks are implicitly split by the innermost rule when the chunk is
/// ended.
final _rules = <Rule>[];
/// The set of rules known to contain hard splits that will in turn force
/// these rules to harden.
///
/// This is accumulated lazily while chunks are being built. Then, once they
/// are all done, the rules are all hardened. We do this later because some
/// rules may not have all of their constraints fully wired up until after
/// the hard split appears. For example, a hard split in a positional
/// argument list needs to force the named arguments to split too, but we
/// don't create that rule until after the positional arguments are done.
final _hardSplitRules = <Rule>{};
/// The list of rules that are waiting until the next whitespace has been
/// written before they start.
final _lazyRules = <Rule>[];
/// The nested stack of spans that are currently being written.
final _openSpans = <OpenSpan>[];
/// The current state.
final _nesting = NestingBuilder();
/// The stack of nesting levels where block arguments may start.
///
/// A block argument's contents will nest at the last level in this stack.
final _blockArgumentNesting = <NestingLevel>[];
/// The number of calls to [preventSplit()] that have not been ended by a
/// call to [endPreventSplit()].
///
/// Splitting is completely disabled inside string interpolation. We do want
/// to fix the whitespace inside interpolation, though, so we still format
/// them. This tracks whether we're inside an interpolation. We can't use a
/// simple bool because interpolation can nest.
///
/// When this is non-zero, splits are ignored.
int _preventSplitNesting = 0;
/// The number of characters of code that can fit in a single line.
int get pageWidth => _formatter.pageWidth;
/// The current innermost rule.
Rule get rule => _rules.last;
ChunkBuilder(this._formatter, this._source)
: _parent = null,
_chunks = [] {
indent(_formatter.indent);
startBlockArgumentNesting();
}
ChunkBuilder._(this._parent, this._formatter, this._source, this._chunks) {
startBlockArgumentNesting();
}
/// Writes [string], the text for a single token, to the output.
///
/// By default, this also implicitly adds one level of nesting if we aren't
/// currently nested at all. We do this here so that if a comment appears
/// after any token within a statement or top-level form and that comment
/// leads to splitting, we correctly nest. Even pathological cases like:
///
///
/// import // comment
/// "this_gets_nested.dart";
///
/// If we didn't do this here, we'd have to call [nestExpression] after the
/// first token of practically every grammar production.
///
/// If [mergeEmptySplits] is `true`, the default, then any pending split
/// information will be combined with a previously created split if no text
/// has been written since. This generally comes into play when text is
/// written after a comment. The comment may leave some pending split
/// information while [SourceVisitor] may have also created a split and we
/// want to combine those.
///
/// It is only `false` when writing the contents of a multiline string. There,
/// we may *want* to have a series of empty chunks because those represent
/// empty lines in the multiline string.
void write(String string, {bool mergeEmptySplits = true}) {
_emitPendingWhitespace(mergeEmptySplits: mergeEmptySplits);
_writeText(string);
_lazyRules.forEach(_activateRule);
_lazyRules.clear();
_nesting.commitNesting();
_afterComment = false;
}
/// Writes one or two hard newlines.
///
/// Doesn't immediately write them. That way line breaking is correctly
/// interleaved with any comments that appear before the next token.
///
/// If [isDouble] is `true`, inserts an extra blank line. If [flushLeft] is
/// `true`, the next line will start at column 1 and ignore indentation and
/// nesting. If [nest] is `true` then the next line will use expression
/// nesting.
void writeNewline(
{bool isDouble = false, bool flushLeft = false, bool nest = false}) {
_pendingNewlines = isDouble ? 2 : 1;
_pendingFlushLeft = flushLeft;
_pendingNested = nest;
}
/// Writes a space before the subsequent non-whitespace text.
void writeSpace() {
_pendingSpace = true;
}
/// Write a split owned by the current innermost rule.
///
/// If [nest] is `false`, ignores any current expression nesting. Otherwise,
/// uses the current nesting level. If unsplit, it expands to a space if
/// [space] is `true`.
Chunk split({bool nest = true, bool space = false}) {
// If we are not allowed to split at all, don't. Returning null for the
// chunk is safe since the rule that uses the chunk will itself get
// discarded because no chunk references it.
if (_preventSplitNesting > 0) {
_pendingNewlines = 0;
_pendingNested = false;
if (space) _pendingSpace = true;
return Chunk.dummy();
}
// If a hard split after a comment is already pending, then prefer that over
// a soft split.
if (_pendingNewlines > 0) return Chunk.dummy();
return _writeSplit(isHard: false, nest: nest, space: space);
}
/// Outputs the series of [comments] and associated whitespace that appear
/// before [token] (which is not written by this).
///
/// The list contains each comment as it appeared in the source between the
/// last token written and the next one that's about to be written.
///
/// [linesBeforeToken] is the number of lines between the last comment (or
/// previous token if there are no comments) and the next token.
void writeComments(
List<SourceComment> comments, int linesBeforeToken, String token) {
// Edge case: if we require a blank line, but there exists one between
// some of the comments, or after the last one, then we don't need to
// enforce one before the first comment. Example:
//
// library foo;
// // comment
//
// class Bar {}
//
// Normally, a blank line is required after `library`, but since there is
// one after the comment, we don't need one before it. This is mainly so
// that commented out directives stick with their preceding group.
if (_pendingNewlines == 2 && comments.first.linesBefore < 2) {
if (linesBeforeToken > 1) {
writeNewline();
} else {
for (var i = 1; i < comments.length; i++) {
if (comments[i].linesBefore > 1) {
writeNewline();
break;
}
}
}
}
// Edge case: if the previous output was also from a call to
// [writeComments()] which ended with a line comment, force a newline.
// Normally, comments are strictly interleaved with tokens and you never
// get two sequences of comments in a row. However, when applying a fix
// that removes a token (like `new`), it's possible to get two sets of
// comments in a row, as in:
//
// // a
// new // b
// Foo();
//
// When that happens, we need to make sure to preserve the split at the end
// of the first sequence of comments if there is one.
if (_afterComment && _pendingNewlines > 0) {
comments.first.linesBefore = 1;
_pendingNewlines = 0;
}
// Edge case: if the comments are completely inline (i.e. just a series of
// block comments with no newlines before, after, or between them), then
// they will eat any pending newlines. Make sure that doesn't happen by
// putting the pending whitespace before the first comment. Turns this:
//
// library foo; /* a */ /* b */ import 'a.dart';
//
// into:
//
// library foo;
//
// /* a */ /* b */ import 'a.dart';
if (linesBeforeToken == 0 &&
_pendingNewlines > comments.first.linesBefore &&
comments.every((comment) => comment.type == CommentType.inlineBlock)) {
comments.first.linesBefore = _pendingNewlines;
}
// Write each comment and the whitespace between them.
for (var i = 0; i < comments.length; i++) {
var comment = comments[i];
// See if the comment should follow text on the current line.
var chunk = _chunkForComment(comment, token);
if (chunk != null) {
// The comment follows other text, so decide if it gets a space before
// it.
_pendingSpace = _needsSpaceBeforeComment(comment, chunk);
if (_pendingSpace && chunk != _chunks.last) {
// We've already created a split after the comment, so if it doesn't
// split, it should get a space.
_chunks.last.updateSplit(space: true);
}
} else {
// Split before the comment if it starts a line.
if (_pendingNewlines == 0) {
if (comment.linesBefore > 0 &&
(_afterComment || comment.type != CommentType.inlineBlock)) {
writeNewline(
isDouble: _needsBlankLineBeforeComment(comment),
flushLeft: comment.flushLeft,
nest: true);
} else if (_chunks.isNotEmpty) {
_pendingSpace = _needsSpaceBeforeComment(comment, _chunks.last);
}
} else {
_pendingFlushLeft = comment.flushLeft;
}
_emitPendingWhitespace(isDouble: _needsBlankLineBeforeComment(comment));
}
_writeCommentText(comment, chunk);
if (comment.selectionStart != null) {
startSelectionFromEnd(comment.text.length - comment.selectionStart!);
}
if (comment.selectionEnd != null) {
endSelectionFromEnd(comment.text.length - comment.selectionEnd!);
}
// Make sure there is at least one newline after a line comment and allow
// one or two after a block comment that has nothing after it.
int linesAfter;
if (i < comments.length - 1) {
linesAfter = comments[i + 1].linesBefore;
} else {
linesAfter = linesBeforeToken;
// Always force a newline after multi-line block comments. Prevents
// mistakes like:
//
// /**
// * Some doc comment.
// */ someFunction() { ... }
if (linesAfter == 0 && comments.last.text.contains('\n')) {
linesAfter = 1;
}
}
if (linesAfter > 0) {
writeNewline(
isDouble: _pendingNewlines == 2 || linesAfter > 1, nest: true);
}
}
// If the comment has text following it (aside from a grouping character),
// it needs a trailing space.
_pendingSpace = _needsSpaceAfterComment(comments.last, token);
_afterComment = true;
}
/// Writes the text of [comment].
///
/// If it's a JavaDoc comment that should be fixed to use `///`, fixes it.
void _writeCommentText(SourceComment comment, [Chunk? chunk]) {
if (!_formatter.fixes.contains(StyleFix.docComments)) {
_writeText(comment.text, chunk);
return;
}
// See if it's a JavaDoc comment.
var match = _javaDocComment.firstMatch(comment.text);
if (match == null) {
_writeText(comment.text, chunk);
return;
}
var lines = match[1]!.split('\n').toList();
var leastIndentation = comment.text.length;
for (var i = 0; i < lines.length; i++) {
// Trim trailing whitespace and turn any all-whitespace lines to "".
var line = lines[i].trimRight();
// Remove any leading "*" from the middle lines.
if (i > 0 && i < lines.length - 1) {
var match = _javaDocLine.firstMatch(line);
if (match != null) {
line = match[1]!;
}
}
// Find the line with the least indentation.
if (line.isNotEmpty) {
var indentation = _leadingIndentation.firstMatch(line)![1]!.length;
leastIndentation = math.min(leastIndentation, indentation);
}
lines[i] = line;
}
// Trim the first and last lines if empty.
if (lines.first.isEmpty) lines.removeAt(0);
if (lines.isNotEmpty && lines.last.isEmpty) lines.removeLast();
// Don't completely eliminate an empty block comment.
if (lines.isEmpty) lines.add('');
for (var line in lines) {
_writeText('///', chunk);
if (line.isNotEmpty) {
// Discard any indentation shared by all lines.
line = line.substring(leastIndentation);
_writeText(' $line', chunk);
}
writeNewline();
_emitPendingWhitespace();
}
}
/// Creates a new indentation level [spaces] deeper than the current one.
///
/// If omitted, [spaces] defaults to [Indent.block].
void indent([int? spaces]) {
_nesting.indent(spaces);
}
/// Discards the most recent indentation level.
void unindent() {
_nesting.unindent();
}
/// Starts a new span with [cost].
///
/// Each call to this needs a later matching call to [endSpan].
void startSpan([int cost = Cost.normal]) {
_openSpans.add(OpenSpan(_chunks.length, cost));
}
/// Ends the innermost span.
void endSpan() {
var openSpan = _openSpans.removeLast();
// A span that just covers a single chunk can't be split anyway.
var end = _chunks.length;
if (openSpan.start == end) return;
// Add the span to every chunk that can split it.
var span = Span(openSpan.cost);
for (var i = openSpan.start; i < end; i++) {
var chunk = _chunks[i];
if (!chunk.rule.isHardened) chunk.spans.add(span);
}
}
/// Starts a new [Rule].
///
/// If omitted, defaults to a new [Rule].
void startRule([Rule? rule]) {
rule ??= Rule();
// If there are any pending lazy rules, start them now so that the proper
// stack ordering of rules is maintained.
_lazyRules.forEach(_activateRule);
_lazyRules.clear();
_activateRule(rule);
}
void _activateRule(Rule rule) {
// See if any of the rules that contain this one care if it splits.
for (var outer in _rules) {
if (!outer.splitsOnInnerRules) continue;
rule.constrainWhenSplit(outer);
}
_rules.add(rule);
}
/// Starts a new [Rule] that comes into play *after* the next whitespace
/// (including comments) is written.
///
/// This is used for operators who want to start a rule before the first
/// operand but not get forced to split if a comment appears before the
/// entire expression.
///
/// If [rule] is omitted, defaults to a new [Rule].
void startLazyRule([Rule? rule]) {
rule ??= Rule();
_lazyRules.add(rule);
}
/// Ends the innermost rule.
void endRule() {
if (_lazyRules.isNotEmpty) {
_lazyRules.removeLast();
} else {
_rules.removeLast();
}
}
/// Pre-emptively forces all of the current rules to become hard splits.
///
/// This is called by [SourceVisitor] when it can determine that a rule will
/// will always be split. Turning it (and the surrounding rules) into hard
/// splits lets the writer break the output into smaller pieces for the line
/// splitter, which helps performance and avoids failing on very large input.
///
/// In particular, it's easy for the visitor to know that collections with a
/// large number of items must split. Doing that early avoids crashing the
/// splitter when it tries to recurse on huge collection literals.
void forceRules() => _handleHardSplit();
/// Begins a new expression nesting level [indent] spaces deeper than the
/// current one if it splits.
///
/// If [indent] is omitted, defaults to [Indent.expression]. If [now] is
/// `true`, commits the nesting change immediately instead of waiting until
/// after the next chunk of text is written.
void nestExpression({int? indent, bool? now}) {
now ??= false;
_nesting.nest(indent);
if (now) _nesting.commitNesting();
}
/// Discards the most recent level of expression nesting.
///
/// Expressions that are more nested will get increased indentation when split
/// if the previous line has a lower level of nesting.
///
/// If [now] is `false`, does not commit the nesting change until after the
/// next chunk of text is written.
void unnest({bool? now}) {
now ??= true;
_nesting.unnest();
if (now) _nesting.commitNesting();
}
/// Marks the selection starting point as occurring [fromEnd] characters to
/// the left of the end of what's currently been written.
///
/// It counts backwards from the end because this is called *after* the chunk
/// of text containing the selection has been output.
void startSelectionFromEnd(int fromEnd) {
assert(_chunks.isNotEmpty);
_chunks.last.startSelectionFromEnd(fromEnd);
}
/// Marks the selection ending point as occurring [fromEnd] characters to the
/// left of the end of what's currently been written.
///
/// It counts backwards from the end because this is called *after* the chunk
/// of text containing the selection has been output.
void endSelectionFromEnd(int fromEnd) {
assert(_chunks.isNotEmpty);
// If the selection marker is right on a split, then put it before the
// newline.
if (_chunks.last.text.isNotEmpty) {
_chunks.last.endSelectionFromEnd(fromEnd);
} else {
_chunks[_chunks.length - 2].endSelectionFromEnd(fromEnd);
}
}
/// Captures the current nesting level as marking where subsequent block
/// arguments should start.
void startBlockArgumentNesting() {
_blockArgumentNesting.add(_nesting.currentNesting);
}
/// Releases the last nesting level captured by [startBlockArgumentNesting].
void endBlockArgumentNesting() {
_blockArgumentNesting.removeLast();
}
/// Starts a new block chunk and returns the [ChunkBuilder] for it.
///
/// Nested blocks are handled using their own independent [LineWriter].
ChunkBuilder startBlock(
{Chunk? argumentChunk, bool indent = true, bool space = false}) {
// Start a block chunk for the block. It will contain the chunks for the
// contents of the block, and its own text will be the closing block
// delimiter.
var chunk = BlockChunk(argumentChunk, _rules.last, _nesting.indentation,
_blockArgumentNesting.last,
space: space, flushLeft: _pendingFlushLeft);
_chunks.add(chunk);
_pendingFlushLeft = false;
var builder = ChunkBuilder._(this, _formatter, _source, chunk.children);
if (indent) builder.indent();
// Create a hard split for the contents. The rule on the parent BlockChunk
// determines whether the body is split or not. This hard rule is only when
// the block's contents are split.
var rule = Rule.hard();
builder.startRule(rule);
builder.split(nest: false, space: space);
return builder;
}
/// Ends this [ChunkBuilder], which must have been created by [startBlock()].
///
/// Forces the chunk that owns the block to split if it can tell that the
/// block contents will always split. It does that by looking for hard splits
/// in the block that aren't for top level elements in the block. If
/// [forceSplit] is `true`, the block always splits.
///
/// Returns the previous writer for the surrounding block.
ChunkBuilder endBlock({bool forceSplit = true}) {
_divideChunks();
// If the last chunk ends with a comment that wants a newline after it,
// then force the block contents to split.
forceSplit |= _pendingNested;
// If we don't already know if the block is going to split, see if it
// contains any hard splits or is longer than a page.
if (!forceSplit) {
var length = 0;
for (var chunk in _chunks) {
length += chunk.length + chunk.unsplitBlockLength;
if (length > _formatter.pageWidth) {
forceSplit = true;
break;
}
// If there are any hardened splits in the chunks (aside from ones
// using the initial hard rule created by [startBlock()] which are for
// the top level elements in the block), then force the block to split.
if (chunk.rule.isHardened && chunk.rule != _rules.first) {
forceSplit = true;
break;
}
}
}
// If there is a hard newline within the block, force the surrounding rule
// for it so that we apply that constraint.
var parent = _parent!;
if (forceSplit) parent.forceRules();
return parent;
}
/// Finishes writing and returns a [SourceCode] containing the final output
/// and updated selection, if any.
SourceCode end() {
assert(_rules.isEmpty);
_divideChunks();
if (debug.traceChunkBuilder) {
debug.log(debug.green('\nBuilt:'));
debug.dumpChunks(0, _chunks);
debug.log();
}
var writer = LineWriter(_formatter, _chunks);
var result =
writer.writeLines(isCompilationUnit: _source.isCompilationUnit);
int? selectionStart;
int? selectionLength;
if (_source.selectionStart != null) {
selectionStart = result.selectionStart;
var selectionEnd = result.selectionEnd;
// 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 ??= writer.length;
selectionEnd ??= writer.length;
selectionLength = selectionEnd - selectionStart;
}
return SourceCode(result.text,
uri: _source.uri,
isCompilationUnit: _source.isCompilationUnit,
selectionStart: selectionStart,
selectionLength: selectionLength);
}
void preventSplit() {
_preventSplitNesting++;
}
void endPreventSplit() {
_preventSplitNesting--;
assert(_preventSplitNesting >= 0, 'Mismatched calls.');
}
/// Writes the current pending [Whitespace] to the output, if any.
///
/// This should only be called after source lines have been preserved to turn
/// any ambiguous whitespace into a concrete choice.
void _emitPendingWhitespace(
{bool isDouble = false, bool mergeEmptySplits = true}) {
if (_pendingNewlines == 0) return;
if (_pendingNewlines == 2) isDouble = true;
_writeSplit(
isDouble: isDouble,
nest: _pendingNested,
mergeEmptySplits: mergeEmptySplits);
}
/// Tries to find an existing chunk to append [comment] to.
///
/// If [comment] should be appending to an existing line (in other words,
/// should be moved before a split), then this returns that [Chunk].
/// Otherwise, returns `null`.
Chunk? _chunkForComment(SourceComment comment, String token) {
// Not if there is nothing before it.
if (_chunks.isEmpty) return null;
// Don't move a comment to a preceding line.
if (comment.linesBefore != 0) return null;
// Multi-line comments are always pushed to the next line.
if (comment.type == CommentType.doc) return null;
if (comment.type == CommentType.block) return null;
var chunk = _chunks.last;
// We may have started a split for a new chunk but not written any text yet.
// In that case, the comment may get written to the previous chunk. Keep a
// generic method comment before '(' with the '(', so don't move it before
// the split.
if (chunk.text.isEmpty &&
_chunks.length > 1 &&
(!_isGenericMethodComment(comment) || token != '(')) {
chunk = _chunks[_chunks.length - 2];
}
// A block comment following a comma probably refers to the following item.
var text = chunk.text;
if (text.endsWith(',') && comment.type == CommentType.inlineBlock) {
return null;
}
// If the text before the split is an open grouping character, it looks
// better to keep it with the elements than with the bracket itself.
if (text.endsWith('(') ||
text.endsWith('[') ||
(text.endsWith('{') && !text.endsWith('\${'))) {
return null;
}
return chunk;
}
/// Returns `true` if [comment] appears to be a magic generic method comment.
///
/// Those get spaced a little differently to look more like real syntax:
///
/// int f/*<S, T>*/(int x) => 3;
bool _isGenericMethodComment(SourceComment comment) {
return comment.text.startsWith('/*<') || comment.text.startsWith('/*=');
}
/// Returns `true` if a space should be output between the end of the current
/// output and the subsequent comment which is about to be written.
///
/// This is only called if the comment is trailing text in the unformatted
/// source. In most cases, a space will be output to separate the comment
/// from what precedes it. This returns false if:
///
/// * This comment does begin the line in the output even if it didn't in
/// the source.
/// * The comment is a block comment immediately following a grouping
/// character (`(`, `[`, or `{`). This is to allow `foo(/* comment */)`,
/// et. al.
bool _needsSpaceBeforeComment(SourceComment comment, Chunk chunk) {
// Not at the beginning of a line.
var text = chunk.text;
if (text.isEmpty) return false;
// Always put a space before line comments.
if (comment.type == CommentType.line) return true;
// Magic generic method comments like "Foo/*<T>*/" don't get spaces.
if (_isGenericMethodComment(comment) &&
_trailingIdentifierChar.hasMatch(text)) {
return false;
}
// Block comments do not get a space if following a grouping character.
return !text.endsWith('(') && !text.endsWith('[') && !text.endsWith('{');
}
/// Returns `true` if a space should be output after the last comment which
/// was just written and the token that will be written.
bool _needsSpaceAfterComment(SourceComment comment, String token) {
// Not at the beginning of a line.
if (_chunks.last.text.isEmpty) return false;
if (_pendingNewlines > 0) return false;
// Magic generic method comments like "Foo/*<T>*/" don't get spaces.
if (_isGenericMethodComment(comment) && token == '(') {
return false;
}
// Otherwise, it gets a space if the following token is not a delimiter or
// the empty string, for EOF.
return token != ')' &&
token != ']' &&
token != '}' &&
token != ',' &&
token != ';' &&
token != '';
}
bool _needsBlankLineBeforeComment(SourceComment comment) {
// Only if the source code has a blank line.
if (comment.linesBefore < 2) return false;
// Don't allow blank lines at the beginning of a block.
if (_chunks.isEmpty) return false;
// Don't allow blank lines at the beginning of a child block.
var text = _chunks.last.text;
if (text.endsWith('{') || text.endsWith('[')) return false;
return true;
}
/// Starts a new chunk with the given split information.
///
/// Returns the chunk.
Chunk _writeSplit(
{bool isHard = true,
bool isDouble = false,
required bool nest,
bool space = false,
bool mergeEmptySplits = true}) {
Chunk chunk;
// If we've already just created a split (i.e. we have a new chunk but it's
// still empty) then update that split with the new information. This avoids
// duplicate splits when line comments occur in places where SourceVisitor
// also inserts splits.
if (mergeEmptySplits && _chunks.isNotEmpty && _chunks.last.text.isEmpty) {
chunk = _chunks.last;
// Don't allow a blank newline at the top of a block.
if (isDouble) {
if (_chunks.length > 1 &&
_chunks[_chunks.length - 2].text.endsWith('{')) {
isDouble = false;
}
}
// Must split before it so that there is a newline after the line comment.
chunk.rule.harden();
chunk.updateSplit(flushLeft: _pendingFlushLeft, isDouble: isDouble);
} else {
chunk = _startChunk(nest ? _nesting.nesting : NestingLevel(),
isHard: isHard, isDouble: isDouble, space: space);
}
if (chunk.rule.isHardened) _handleHardSplit();
_pendingNewlines = 0;
_pendingNested = false;
return chunk;
}
/// Writes [text] to either the current chunk or a new one if the current
/// chunk is complete.
void _writeText(String text, [Chunk? chunk]) {
if (chunk == null) {
if (_chunks.isEmpty) {
_startChunk(NestingLevel(), isHard: true);
}
chunk = _chunks.last;
}
if (_pendingSpace && chunk.text.isNotEmpty) chunk.appendText(' ');
_pendingSpace = false;
chunk.appendText(text);
}
Chunk _startChunk(NestingLevel nesting,
{required bool isHard, bool isDouble = false, bool space = false}) {
var rule = isHard ? Rule.hard() : _rules.last;
var chunk = Chunk(rule, _nesting.indentation, nesting,
space: space, flushLeft: _pendingFlushLeft, isDouble: isDouble);
_chunks.add(chunk);
_pendingFlushLeft = false;
return chunk;
}
/// Pre-processes the chunks after they are done being written by the visitor
/// but before they are run through the line splitter.
///
/// Marks ranges of chunks that can be line split independently to keep the
/// batches we send to [LineSplitter] small.
void _divideChunks() {
// Harden all of the rules that we know get forced by containing hard
// splits, along with all of the other rules they constrain.
_hardenRules();
for (var i = 0; i < _chunks.length; i++) {
var chunk = _chunks[i];
if (!_canDivide(chunk)) chunk.preventDivide();
}
}
/// Returns true if we can divide the chunks at [index] and line split the
/// ones before and after that separately.
bool _canDivide(Chunk chunk) {
// Don't divide at the first chunk.
if (chunk == _chunks.first) return false;
// Can't divide soft rules.
if (!chunk.rule.isHardened) return false;
// Can't divide in the middle of expression nesting.
if (chunk.nesting.isNested) return false;
// If the chunk is the ending delimiter of a block, then don't separate it
// and its children from the preceding beginning of the block.
if (chunk is BlockChunk) return false;
return true;
}
/// Hardens the active rules when a hard split occurs within them.
void _handleHardSplit() {
if (_rules.isEmpty) return;
// If the current rule doesn't care, it will "eat" the hard split and no
// others will care either.
if (!_rules.last.splitsOnInnerRules) return;
// Start with the innermost rule. This will traverse the other rules it
// constrains.
_hardSplitRules.add(_rules.last);
}
/// Replaces all of the previously hardened rules with hard splits, along
/// with every rule that those constrain to also split.
///
/// This should only be called after all chunks have been written.
void _hardenRules() {
if (_hardSplitRules.isEmpty) return;
void walkConstraints(Rule rule) {
rule.harden();
// Follow this rule's constraints, recursively.
for (var other in rule.constrainedRules) {
if (other == rule) continue;
if (!other.isHardened &&
rule.constrain(rule.fullySplitValue, other) ==
other.fullySplitValue) {
walkConstraints(other);
}
}
}
for (var rule in _hardSplitRules) {
walkConstraints(rule);
}
// Discard spans in hardened chunks since we know for certain they will
// split anyway.
for (var chunk in _chunks) {
if (chunk.rule.isHardened) {
chunk.spans.clear();
}
}
}
}