Hard split in interpolation (#714)

* Allow hard splits inside string interpolation.

Removing *all* splits broken several things. In particular, multiline
strings and line comments inside interpolation really do need to
maintain their splits.

This allows all hard splits to split but not soft splits. It also allows
collection literals to split. That's not ideal, but it should be a rare
case anyway. If you don't want that to split, don't have a collection
literal inside a string interpolation in a line that doesn't fit the
page width.

Fix #711.

* Add another edge case test.

* Bump version and update changelog.

* Revise README.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 686a406..2f87722 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 1.1.3
+
+* Preserve whitespace in multi-line strings inside string interpolations (#711).
+  **Note!** This bug means that dart_style 1.1.2 may make semantics changes to
+  your strings. You should avoid that version and use 1.1.3.
+
 # 1.1.2
 
 * Don't split inside string interpolations.
diff --git a/README.md b/README.md
index 3746768..a0d4400 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,10 @@
 
 [dart style guide]: https://www.dartlang.org/guides/language/effective-dart/style
 
-The formatter handles indentation, inline whitespace, and
-(by far the most difficult) intelligent line wrapping.
-It has no problems with nested collections, function
-expressions, long argument lists, or otherwise tricky code.
+The formatter handles indentation, inline whitespace, and (by far the most
+difficult) intelligent line wrapping. It has no problems with nested
+collections, function expressions, long argument lists, or otherwise tricky
+code.
 
 The formatter turns code like this:
 
@@ -36,8 +36,8 @@
 
 The formatter can also apply non-whitespace changes to make your code
 consistently idiomatic. You must opt into these by passing either `--fix` which
-applies all style fixes, or any of the `--fix-`-prefixed flags to apply
-specific fixes.
+applies all style fixes, or any of the `--fix-`-prefixed flags to apply specific
+fixes.
 
 For example, running with `--fix-named-default-separator` changes this:
 
@@ -57,19 +57,23 @@
 
 ## Getting dartfmt
 
-Dartfmt is included in the Dart SDK, so you might want to add the SDK's bin
-directory to your system path.
+Dartfmt is included in the Dart SDK, so most users get it directly from there.
+That has the latest version of dartfmt that was available when the SDK was
+released.
 
-If you want to make sure you are running the latest version of dartfmt,
-you can [globally activate][] the package from the dart_style package
-on pub.dartlang.org, and let pub put its executable on your path:
+If you want to make sure you are running the latest version of dartfmt, you can
+[globally activate][] the package from the dart_style package on
+pub.dartlang.org:
 
     $ pub global activate dart_style
     $ dartfmt ...
 
+For this to work, you need to put pub's bin directory on your PATH before the
+Dart SDL directory. Otherwise, the SDK's dartfmt will shadow this one.
+
 [globally activate]: https://www.dartlang.org/tools/pub/cmd/pub-global.html
 
-If you don't want `dartfmt` on your path, you can run it explicitly:
+If you don't want pub to put `dartfmt` on your PATH, you can run it explicitly:
 
     $ pub global activate dart_style --no-executables
     $ pub global run dart_style:format ...
@@ -77,28 +81,27 @@
 ## Using dartfmt
 
 IDEs and editors that support Dart usually provide easy ways to run the
-formatter. For example, in WebStorm you can right-click a .dart file
-and then choose **Reformat with Dart Style**.
+formatter. For example, in WebStorm you can right-click a .dart file and then
+choose **Reformat with Dart Style**.
 
 Here's a simple example of using dartfmt on the command line:
 
     $ dartfmt test.dart
 
-This command formats the `test.dart` file and writes the result to
-standard output.
+This command formats the `test.dart` file and writes the result to standard
+output.
 
-Dartfmt takes a list of paths, which can point to directories or files.
-If the path is a directory, it processes every `.dart` file in that directory
-or any of its subdirectories.
-If no file or directory is specified, dartfmt reads from standard input.
+Dartfmt takes a list of paths, which can point to directories or files. If the
+path is a directory, it processes every `.dart` file in that directory or any of
+its subdirectories. If no file or directory is specified, dartfmt reads from
+standard input.
 
 By default, it formats each file and just prints the resulting code to stdout.
-If you pass `-w`, it overwrites your existing files with the
-formatted results.
+If you pass `-w`, it overwrites your existing files with the formatted results.
 
-You may pass a `-l` option to control the width of the page that it
-wraps lines to fit within, but you're strongly encouraged to keep the default
-line length of 80 columns.
+You may pass a `-l` option to control the width of the page that it wraps lines
+to fit within, but you're strongly encouraged to keep the default line length of
+80 columns.
 
 ### Validating files
 
@@ -115,6 +118,7 @@
 
 The package also exposes a single dart_style library containing a programmatic
 API for formatting code. Simple usage looks like this:
+
 ```dart
 import 'package:dart_style/dart_style.dart';
 
@@ -134,6 +138,7 @@
   }
 }
 ```
+
 ## Other resources
 
 * Before sending an email, see if you are asking a
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 574efd1..e4b1b8f 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,5 +1,3 @@
-analyzer:
-  strong-mode: true
 linter:
   rules:
     - unawaited_futures
diff --git a/bin/format.dart b/bin/format.dart
index e0de12d..45756a3 100644
--- a/bin/format.dart
+++ b/bin/format.dart
@@ -15,7 +15,7 @@
 import 'package:dart_style/src/style_fix.dart';
 
 // Note: The following line of code is modified by tool/grind.dart.
-const version = "1.1.2";
+const version = "1.1.3";
 
 void main(List<String> args) {
   var parser = new ArgParser(allowTrailingOptions: true);
diff --git a/lib/src/chunk_builder.dart b/lib/src/chunk_builder.dart
index 4bdd166..a192f21 100644
--- a/lib/src/chunk_builder.dart
+++ b/lib/src/chunk_builder.dart
@@ -171,9 +171,20 @@
   /// 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 flushLeft, bool isDouble, bool nest, bool space}) =>
-      _writeSplit(_rules.last,
-          flushLeft: flushLeft, isDouble: isDouble, nest: nest, space: space);
+  Chunk split({bool flushLeft, bool isDouble, bool nest, bool space}) {
+    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) {
+      if (space) _pendingWhitespace = Whitespace.space;
+      return null;
+    }
+
+    return _writeSplit(_rules.last,
+        flushLeft: flushLeft, isDouble: isDouble, nest: nest, space: space);
+  }
 
   /// Outputs the series of [comments] and associated whitespace that appear
   /// before [token] (which is not written by this).
@@ -512,10 +523,6 @@
   ///
   /// Nested blocks are handled using their own independent [LineWriter].
   ChunkBuilder startBlock(Chunk argumentChunk) {
-    // If we are not allowed to split at all, don't create a new block. Instead,
-    // the block contents will end up in the current chunk.
-    if (_preventSplitNesting > 0) return this;
-
     var chunk = _chunks.last;
     chunk.makeBlock(argumentChunk);
 
@@ -538,11 +545,6 @@
   ///
   /// Returns the previous writer for the surrounding block.
   ChunkBuilder endBlock(Rule ignoredSplit, {bool forceSplit}) {
-    // If we are not allowed to split at all, we didn't create a new block and
-    // thus didn't create a new ChunkBuilder, so there is no builder to pop.
-    // We are still in the current one.
-    if (_preventSplitNesting > 0) return this;
-
     _divideChunks();
 
     // If we don't already know if the block is going to split, see if it
@@ -767,21 +769,6 @@
   /// to be at column zero. Otherwise, it uses the normal indentation and
   /// nesting behavior.
   void _writeHardSplit({bool isDouble, bool flushLeft, bool nest: false}) {
-    // If we are not allowed to split at all, simply write a space. Instead of:
-    //
-    //     foo("${() {
-    //       a;
-    //       b;
-    //     }}");
-    //
-    // produces:
-    //
-    //     foo("${() { a; b; }}");
-    if (_preventSplitNesting > 0) {
-      _writeText(" ");
-      return;
-    }
-
     // A hard split overrides any other whitespace.
     _pendingWhitespace = null;
     _writeSplit(new Rule.hard(),
@@ -796,14 +783,6 @@
     nest ??= true;
     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) {
-      if (space) write(" ");
-      return null;
-    }
-
     if (_chunks.isEmpty) {
       if (flushLeft != null) _firstFlushLeft = flushLeft;
 
diff --git a/pubspec.lock b/pubspec.lock
index 54d2a9a..b648322 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -366,4 +366,4 @@
     source: hosted
     version: "2.1.14"
 sdks:
-  dart: ">=2.0.0-dev.62.0 <=2.0.0-edge.edf26852e6f6f096bb935ab8a545ce0646cf05a6"
+  dart: ">=2.0.0-dev.62.0 <=2.0.0-edge.69a15d20069891ed13e7c65a6810527b4a9f9e40"
diff --git a/pubspec.yaml b/pubspec.yaml
index 634c852..37f5a8d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: dart_style
 # Note: See tool/grind.dart for how to bump the version.
-version: 1.1.2
+version: 1.1.3
 author: Dart Team <misc@dartlang.org>
 description: Opinionated, automatic Dart source code formatter.
 homepage: https://github.com/dart-lang/dart_style
diff --git a/test/regression/0700/0711.stmt b/test/regression/0700/0711.stmt
new file mode 100644
index 0000000..8fd2179
--- /dev/null
+++ b/test/regression/0700/0711.stmt
@@ -0,0 +1,12 @@
+>>>
+var str = '''${"""
+a
+b
+c
+"""}''';
+<<<
+var str = '''${"""
+a
+b
+c
+"""}''';
\ No newline at end of file
diff --git a/test/splitting/strings.stmt b/test/splitting/strings.stmt
index d3bdaab..5321b70 100644
--- a/test/splitting/strings.stmt
+++ b/test/splitting/strings.stmt
@@ -161,8 +161,84 @@
 someMethod(
     "some text that is ${pretty + 'long ${interpolate + a + thing} more'} text",
     "another arg");
->>> hard splits are not split in interpolation
+>>> hard splits are split in interpolation
 someMethod("before ${(){statement();statement();statement();}} after");
 <<<
-someMethod(
-    "before ${() { statement(); statement(); statement(); }} after");
\ No newline at end of file
+someMethod("before ${() {
+  statement();
+  statement();
+  statement();
+}} after");
+>>> collections split in interpolation if needed
+method(
+"b ${[1, 2, 3, 4, 5, 6]} a",
+"before ${[first, second, third, fourth, {fifth: sixth}]} after");
+<<<
+method(
+    "b ${[1, 2, 3, 4, 5, 6]} a",
+    "before ${[
+      first,
+      second,
+      third,
+      fourth,
+      {fifth: sixth}
+    ]} after");
+>>> nested multiline strings are not merged in interpolation
+"before ${"""a
+b"""} ${aft
++
+er}";
+<<<
+"before ${"""a
+b"""} ${aft + er}";
+>>> multiply-nested interpolation
+'''a
+${b +
+"""c
+${d
++ '''e
+f'''
++
+g}
+h"""
++ i}
+j ${k
++
+l}''';
+<<<
+'''a
+${b + """c
+${d + '''e
+f''' + g}
+h""" + i}
+j ${k + l}''';
+>>> nested interpolation inside function
+"before ${ () { a(); """b
+c"""; d(); }} after";
+<<<
+"before ${() {
+  a();
+  """b
+c""";
+  d();
+}} after";
+>>> comment inside interpolation
+"before ${// comment
+a
++
+b
++  // another
+c} after";
+<<<
+"before ${ // comment
+    a + b + // another
+        c} after";
+>>>
+function(
+  "long string long string ${interpolated + interpolated} long string",
+  longLongLongLongObject.longLongLongLongMethod());
+<<<
+function(
+    "long string long string ${interpolated + interpolated} long string",
+    longLongLongLongObject
+        .longLongLongLongMethod());
\ No newline at end of file
diff --git a/test/utils.dart b/test/utils.dart
index 74fa489..0d5f889 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -62,6 +62,8 @@
 
   var entries = new Directory(p.join(testDir, name))
       .listSync(recursive: true, followLinks: false);
+  entries.sort((a, b) => a.path.compareTo(b.path));
+
   for (var entry in entries) {
     if (!entry.path.endsWith(".stmt") && !entry.path.endsWith(".unit")) {
       continue;
diff --git a/test/whitespace/strings.stmt b/test/whitespace/strings.stmt
new file mode 100644
index 0000000..b44f8bf
--- /dev/null
+++ b/test/whitespace/strings.stmt
@@ -0,0 +1,9 @@
+40 columns                              |
+>>>
+"a"     "b" "c";
+<<<
+"a" "b" "c";
+>>> empty multi-line
+"""""" '''''';
+<<<
+"""""" '''''';
\ No newline at end of file