Format multiline strings as block arguments. (#634)

Closes #570
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e0483..4dc3916 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 # 1.0.7-dev
 
+* Format multiline strings as block arguments (#570).
 * Fix call to analyzer API.
 * Support assert in initializer list experimental syntax (#522).
 
diff --git a/lib/src/argument_list_visitor.dart b/lib/src/argument_list_visitor.dart
index 3d0f7dc..632a60f 100644
--- a/lib/src/argument_list_visitor.dart
+++ b/lib/src/argument_list_visitor.dart
@@ -15,8 +15,8 @@
 import 'source_visitor.dart';
 
 /// Helper class for [SourceVisitor] that handles visiting and writing an
-/// [ArgumentList], including all of the special code needed to handle function
-/// and collection arguments.
+/// [ArgumentList], including all of the special code needed to handle
+/// block-formatted arguments.
 class ArgumentListVisitor {
   final SourceVisitor _visitor;
 
@@ -47,13 +47,14 @@
   bool get _isSingle =>
       _allArguments.length == 1 && _allArguments.single is! NamedExpression;
 
-  /// Whether this argument list has any collection or block function arguments.
+  /// Whether this argument list has any arguments that should be formatted as
+  /// blocks.
   // TODO(rnystrom): Returning true based on collections is non-optimal. It
   // forces a method chain to break into two but the result collection may not
   // actually split which can lead to a method chain that's allowed to break
   // where it shouldn't.
   bool get hasBlockArguments =>
-      _arguments._collections.isNotEmpty || _functions != null;
+      _arguments._blocks.isNotEmpty || _functions != null;
 
   factory ArgumentListVisitor(SourceVisitor visitor, ArgumentList node) {
     return new ArgumentListVisitor.forArguments(
@@ -102,7 +103,7 @@
     //
     //     function(named: () {
     //       something();
-    //     }
+    //     },
     //         another: argument);
     if (functionsStart != null &&
         arguments[0] is NamedExpression &&
@@ -308,23 +309,23 @@
   /// The named arguments, in order.
   final List<Expression> _named;
 
-  /// Maps each argument that is a collection literal that get special
-  /// formatting to the token for the collection's open bracket.
-  final Map<Expression, Token> _collections;
+  /// Maps each block argument, excluding functions, to the first token for that
+  /// argument.
+  final Map<Expression, Token> _blocks;
 
-  /// The number of leading collections.
+  /// The number of leading block arguments, excluding functions.
   ///
-  /// If all arguments are collections, this counts them.
-  final int _leadingCollections;
+  /// If all arguments are blocks, this counts them.
+  final int _leadingBlocks;
 
-  /// The number of trailing collections.
+  /// The number of trailing blocks arguments.
   ///
-  /// If all arguments are collections, this is zero.
-  final int _trailingCollections;
+  /// If all arguments are blocks, this is zero.
+  final int _trailingBlocks;
 
-  /// The rule used to split the bodies of all of the collection arguments.
-  Rule get collectionRule => _collectionRule;
-  Rule _collectionRule;
+  /// The rule used to split the bodies of all block arguments.
+  Rule get blockRule => _blockRule;
+  Rule _blockRule;
 
   /// The most recent chunk that split before an argument.
   Chunk get previousSplit => _previousSplit;
@@ -337,48 +338,48 @@
         arguments.takeWhile((arg) => arg is! NamedExpression).toList();
     var named = arguments.skip(positional.length).toList();
 
-    var collections = <Expression, Token>{};
+    var blocks = <Expression, Token>{};
     for (var argument in arguments) {
-      var bracket = _getCollectionBracket(argument);
-      if (bracket != null) collections[argument] = bracket;
+      var bracket = _blockToken(argument);
+      if (bracket != null) blocks[argument] = bracket;
     }
 
-    // Count the leading arguments that are collection literals.
-    var leadingCollections = 0;
+    // Count the leading arguments that are blocks.
+    var leadingBlocks = 0;
     for (var argument in arguments) {
-      if (!collections.containsKey(argument)) break;
-      leadingCollections++;
+      if (!blocks.containsKey(argument)) break;
+      leadingBlocks++;
     }
 
-    // Count the trailing arguments that are collection literals.
-    var trailingCollections = 0;
-    if (leadingCollections != arguments.length) {
+    // Count the trailing arguments that are blocks.
+    var trailingBlocks = 0;
+    if (leadingBlocks != arguments.length) {
       for (var argument in arguments.reversed) {
-        if (!collections.containsKey(argument)) break;
-        trailingCollections++;
+        if (!blocks.containsKey(argument)) break;
+        trailingBlocks++;
       }
     }
 
-    // Collections must all be a prefix or suffix of the argument list (and not
+    // Blocks must all be a prefix or suffix of the argument list (and not
     // both).
-    if (leadingCollections != collections.length) leadingCollections = 0;
-    if (trailingCollections != collections.length) trailingCollections = 0;
+    if (leadingBlocks != blocks.length) leadingBlocks = 0;
+    if (trailingBlocks != blocks.length) trailingBlocks = 0;
 
-    // Ignore any collections in the middle of the argument list.
-    if (leadingCollections == 0 && trailingCollections == 0) {
-      collections.clear();
+    // Ignore any blocks in the middle of the argument list.
+    if (leadingBlocks == 0 && trailingBlocks == 0) {
+      blocks.clear();
     }
 
-    return new ArgumentSublist._(allArguments, positional, named, collections,
-        leadingCollections, trailingCollections);
+    return new ArgumentSublist._(allArguments, positional, named, blocks,
+        leadingBlocks, trailingBlocks);
   }
 
   ArgumentSublist._(this._allArguments, this._positional, this._named,
-      this._collections, this._leadingCollections, this._trailingCollections);
+      this._blocks, this._leadingBlocks, this._trailingBlocks);
 
   void visit(SourceVisitor visitor) {
-    if (_collections.isNotEmpty) {
-      _collectionRule = new Rule(Cost.splitCollections);
+    if (_blocks.isNotEmpty) {
+      _blockRule = new Rule(Cost.splitBlocks);
     }
 
     var rule = _visitPositional(visitor);
@@ -390,11 +391,11 @@
     if (_positional.isEmpty) return null;
 
     // Allow splitting after "(".
-    // Only count the collections in the positional rule.
-    var leadingCollections = math.min(_leadingCollections, _positional.length);
-    var trailingCollections = math.max(_trailingCollections - _named.length, 0);
+    // Only count the blocks in the positional rule.
+    var leadingBlocks = math.min(_leadingBlocks, _positional.length);
+    var trailingBlocks = math.max(_trailingBlocks - _named.length, 0);
     var rule = new PositionalRule(
-        _collectionRule, leadingCollections, trailingCollections);
+        _blockRule, leadingBlocks, trailingBlocks);
     _visitArguments(visitor, _positional, rule);
 
     return rule;
@@ -404,12 +405,12 @@
   void _visitNamed(SourceVisitor visitor, PositionalRule positionalRule) {
     if (_named.isEmpty) return;
 
-    // Only count the collections in the named rule.
-    var leadingCollections =
-        math.max(_leadingCollections - _positional.length, 0);
-    var trailingCollections = math.min(_trailingCollections, _named.length);
+    // Only count the blocks in the named rule.
+    var leadingBlocks =
+        math.max(_leadingBlocks - _positional.length, 0);
+    var trailingBlocks = math.min(_trailingBlocks, _named.length);
     var namedRule =
-        new NamedRule(_collectionRule, leadingCollections, trailingCollections);
+        new NamedRule(_blockRule, leadingBlocks, trailingBlocks);
 
     // Let the positional args force the named ones to split.
     if (positionalRule != null) {
@@ -450,12 +451,12 @@
 
   void _visitArgument(
       SourceVisitor visitor, ArgumentRule rule, Expression argument) {
-    // If we're about to write a collection argument, handle it specially.
-    if (_collections.containsKey(argument)) {
+    // If we're about to write a block argument, handle it specially.
+    if (_blocks.containsKey(argument)) {
       rule.disableSplitOnInnerRules();
 
       // Tell it to use the rule we've already created.
-      visitor.beforeCollection(_collections[argument], this);
+      visitor.beforeBlock(_blocks[argument], this);
     } else if (_allArguments.length > 1) {
       // Edge case: Only bump the nesting if there are multiple arguments. This
       // lets us avoid spurious indentation in cases like:
@@ -479,7 +480,7 @@
       visitor.visit(argument);
     }
 
-    if (_collections.containsKey(argument)) {
+    if (_blocks.containsKey(argument)) {
       rule.enableSplitOnInnerRules();
     } else if (_allArguments.length > 1) {
       visitor.builder.endBlockArgumentNesting();
@@ -493,12 +494,12 @@
     }
   }
 
-  /// Returns the token for the left bracket if [expression] denotes a
-  /// collection literal argument.
+  /// If [expression] can be formatted as a block, returns the token that opens
+  /// the block, such as a collection's bracket.
   ///
-  /// Similar to block functions, collection arguments can get special
-  /// indentation to make them look more statement-like.
-  static Token _getCollectionBracket(Expression expression) {
+  /// Block-formatted arguments can get special indentation to make them look
+  /// more statement-like.
+  static Token _blockToken(Expression expression) {
     if (expression is NamedExpression) {
       expression = (expression as NamedExpression).expression;
     }
@@ -507,6 +508,9 @@
 
     if (expression is ListLiteral) return expression.leftBracket;
     if (expression is MapLiteral) return expression.leftBracket;
+    if (expression is SingleStringLiteral && expression.isMultiline) {
+      return expression.beginToken;
+    }
 
     // Not a collection literal.
     return null;
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
index 17a9218..3fd3e28 100644
--- a/lib/src/chunk.dart
+++ b/lib/src/chunk.dart
@@ -320,11 +320,11 @@
   /// Splitting inside the brackets of a list with only one element.
   static const singleElementList = 2;
 
-  /// Splitting the internals of collection literal arguments.
+  /// Splitting the internals of block arguments.
   ///
-  /// Used to prefer splitting at the argument boundary over splitting the
-  /// collection contents.
-  static const splitCollections = 2;
+  /// 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 = 3;
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index 8f2ecb3..cef1340 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -68,20 +68,19 @@
   /// in unison.
   final List<MetadataRule> _metadataRules = [];
 
-  /// The mapping for collection literals that are managed by the argument
-  /// list that contains them.
+  /// The mapping for blocks that are managed by the argument list that contains
+  /// them.
   ///
-  /// When a collection literal appears inside an [ArgumentSublist], the
-  /// argument list provides a rule for the body to split to ensure that all
-  /// collections split in unison. It also tracks the chunk before the
-  /// argument that determines whether or not the collection body is indented
-  /// like an expression or a statement.
+  /// When a block expression, such as a collection literal or a multiline
+  /// string, appears inside an [ArgumentSublist], the argument list provides a
+  /// rule for the body to split to ensure that all blocks split in unison. It
+  /// also tracks the chunk before the argument that determines whether or not
+  /// the block body is indented like an expression or a statement.
   ///
-  /// Before a collection literal argument is visited, [ArgumentSublist] binds
-  /// itself to the left bracket token of each collection literal it controls.
-  /// When we later visit that literal, we use the token to find that
-  /// association.
-  final Map<Token, ArgumentSublist> _collectionArgumentLists = {};
+  /// Before a block argument is visited, [ArgumentSublist] binds itself to the
+  /// beginning token of each block it controls. When we later visit that
+  /// literal, we use the token to find that association.
+  final Map<Token, ArgumentSublist> _blockArgumentLists = {};
 
   /// Initialize a newly created visitor to write source code representing
   /// the visited nodes to the given [writer].
@@ -2386,9 +2385,9 @@
     // handle splitting and indenting it. If not, we'll use a default rule.
     var rule;
     var argumentChunk;
-    if (_collectionArgumentLists.containsKey(leftBracket)) {
-      var argumentList = _collectionArgumentLists[leftBracket];
-      rule = argumentList.collectionRule;
+    if (_blockArgumentLists.containsKey(leftBracket)) {
+      var argumentList = _blockArgumentLists[leftBracket];
+      rule = argumentList.blockRule;
       argumentChunk = argumentList.previousSplit;
     }
 
@@ -2470,13 +2469,13 @@
     builder.unnest();
   }
 
-  /// Marks the collection literal that starts with [leftBracket] as being
-  /// controlled by [argumentList].
+  /// Marks the block that starts with [token] as being controlled by
+  /// [argumentList].
   ///
-  /// When the collection is visited, [argumentList] will determine the
-  /// indentation and splitting rule for the collection.
-  void beforeCollection(Token leftBracket, ArgumentSublist argumentList) {
-    _collectionArgumentLists[leftBracket] = argumentList;
+  /// When the block is visited, [argumentList] will determine the
+  /// indentation and splitting rule for the block.
+  void beforeBlock(Token token, ArgumentSublist argumentList) {
+    _blockArgumentLists[token] = argumentList;
   }
 
   /// Writes the beginning of a brace-delimited body and handles indenting and
diff --git a/test/splitting/strings.stmt b/test/splitting/strings.stmt
index 39d4beb..fc262fe 100644
--- a/test/splitting/strings.stmt
+++ b/test/splitting/strings.stmt
@@ -53,4 +53,88 @@
 <<<
 someMethod(
     """first line does not fits here
-""");
\ No newline at end of file
+""");
+>>> format multiline as a block with a leading argument
+someMethod("foo", """
+  some
+  text
+""");
+<<<
+someMethod("foo", """
+  some
+  text
+""");
+>>> format multiline as a block with a trailing argument
+someMethod("""
+  some
+  text
+""", "foo");
+<<<
+someMethod("""
+  some
+  text
+""", "foo");
+>>> format multiline as a block with leading and trailing arguments
+someMethod(
+  "foo",
+  """
+  some
+  text
+  """,
+  "foo");
+<<<
+someMethod(
+    "foo",
+    """
+  some
+  text
+  """,
+    "foo");
+>>> format multiple multilines as blocks
+someMethod("""
+  some
+  text
+""", """
+  some
+  more
+""", """
+  even more
+""");
+<<<
+someMethod("""
+  some
+  text
+""", """
+  some
+  more
+""", """
+  even more
+""");
+>>> format multilines with other blocks as blocks
+someMethod("""
+  some
+  text
+""", () {
+  some;
+  function;
+}, [
+  "some",
+  "collection",
+  "with",
+  "many",
+  "elements"
+]);
+<<<
+someMethod("""
+  some
+  text
+""", () {
+  some;
+  function;
+}, [
+  "some",
+  "collection",
+  "with",
+  "many",
+  "elements"
+]);