blob: 156f2e96637af1d0405599d98f3c92e539433058 [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.nesting;
import 'chunk.dart';
import 'line_splitter.dart';
/// Maintains a stack of nested expressions that have currently been split.
///
/// A single statement may have multiple different levels of indentation based
/// on the expression nesting level at the point where the line is broken. For
/// example:
///
/// someFunction(argument, argument,
/// innerFunction(argument,
/// innermost), argument);
///
/// This means that when splitting a line, we need to keep track of the nesting
/// level of the previous line(s) to determine how far the next line must be
/// indented.
///
/// This class is a persistent collection. Each instance is immutable and
/// methods to modify it return a new collection.
class NestingStack {
/// The number of visible indentation levels for the current nesting.
///
/// This may be less than [_depth] since split lines can skip multiple
/// nesting depths.
final int indent;
final NestingStack _parent;
/// The number of surrounding expression nesting levels.
final int _depth;
NestingStack() : this._(null, -1, 0);
NestingStack._(this._parent, this._depth, this.indent);
/// LinePrefixes implement their own value equality to ensure that two
/// prefixes with the same nesting stack are considered equal even if the
/// nesting occurred from different splits.
///
/// For example, consider these two prefixes with `^` marking where splits
/// have been applied:
///
/// fn( first, second, ...
/// ^
/// fn( first, second, ...
/// ^
///
/// These are equivalent from the view of the suffix because they have the
/// same nesting stack, even though the nesting came from different tokens.
/// This lets us reuse memoized suffixes more frequently when solving.
bool operator ==(other) {
if (other is! NestingStack) return false;
var self = this;
while (self != null) {
if (self._depth != other._depth) return false;
self = self._parent;
other = other._parent;
// They should be the same length.
if ((self == null) != (other == null)) return false;
}
return true;
}
int get hashCode {
// TODO(rnystrom): Is it worth iterating throught the stack?
return indent.hashCode ^ _depth.hashCode;
}
/// Takes this nesting stack and produces all of the new nesting stacks that
/// are possible when followed by the nesting level of [split].
///
/// This may produce multiple solutions because a non-incremental jump in
/// nesting depth can be sliced up multiple ways. Let's say the prefix is:
///
/// first(second(third(...
///
/// The current nesting stack is empty (since we're on the first line). How
/// do we modify it by taking into account the split after `third(`? The
/// simple answer is to just increase the indentation by one level:
///
/// first(second(third(
/// argumentToThird)));
///
/// This is correct in most cases, but not all. Consider:
///
/// first(second(third(
/// argumentToThird),
/// argumentToSecond));
///
/// Oops! There's no place for `argumentToSecond` to go. To handle that, the
/// second line needs to be indented one more level to make room for the later
/// line:
///
/// first(second(third(
/// argumentToThird),
/// argumentToSecond));
///
/// It's even possible we may need to do:
///
/// first(second(third(
/// argumentToThird),
/// argumentToSecond),
/// argumentToFirst);
///
/// To accommodate those, this returns the list of all possible ways the
/// nesting stack can be modified. It generates the in order of best to worst
/// so that the line splitter can stop as soon as it finds a working solution.
/// "Best" here means it tries fewer levels of indentation first.
List<NestingStack> applySplit(Chunk split) {
assert(split.isInExpression);
if (split.nesting == _depth) return [this];
// If the new split is less nested than we currently are, pop and discard
// the previous nesting levels.
if (split.nesting < _depth) {
// Pop items off the stack until we find the level we are now at.
var stack = this;
while (stack != null) {
if (stack._depth == split.nesting) return [stack];
stack = stack._parent;
}
// If we got here, the level wasn't found. That means there is no correct
// stack level to pop to, since the stack skips past our indentation
// level.
return [];
}
// TODO(rnystrom): This eagerly generates all of the nesting stacks even
// though LineSplitter._findBestSplits() will early out of looping over
// them. Optimize by generating these only as needed.
// Going deeper, so try every indentating for every subset of expression
// nesting levels between the old and new one.
return _intermediateDepths(_depth, split.nesting).map((depths) {
var result = this;
for (var depth in depths) {
result = new NestingStack._(
result, depth, result.indent + indentsPerNest);
}
return new NestingStack._(
result, split.nesting, result.indent + indentsPerNest);
}).toList();
}
/// Given [min] and [max], generates all of the subsets of numbers in that
/// range (exclusive), including the empty set.
///
/// This is used for determine what sets of intermediate nesting levels to
/// consider when jumping from a shallow nesting level to a much deeper one.
/// Subsets are generated in order of increasing length. For example, `(2, 6)`
/// yields:
///
/// []
/// [3] [4] [5]
/// [3, 4] [3, 5] [4, 5]
/// [3, 4, 5]
///
/// This ensures the splitter prefers solutions that use the least
/// indentation.
List<List<int>> _intermediateDepths(int min, int max) {
assert(min < max);
var subsets = [[]];
var lastLengthStart = 0;
var lastLengthEnd = subsets.length;
// Generate subsets in order of increasing length.
for (var length = 1; length <= max - min + 1; length++) {
// Start with each subset containing one fewer element.
for (var i = lastLengthStart; i < lastLengthEnd; i++) {
var previousSubset = subsets[i];
var start = previousSubset.isNotEmpty ? previousSubset.last + 1 : min + 1;
// Then for each value in the remainer, make a new subset that is the
// union of the shorter subset and that value.
for (var j = start; j < max; j++) {
var subset = previousSubset.toList()..add(j);
subsets.add(subset);
}
}
// Move on to the next length range.
lastLengthStart = lastLengthEnd;
lastLengthEnd = subsets.length;
}
return subsets;
}
String toString() {
var nesting = this;
var levels = [];
while (nesting != null) {
levels.add("${nesting._depth}:${nesting.indent}");
nesting = nesting._parent;
}
return levels.join(" ");
}
}