Allow command-line API to pass in selection to preserve. Fix #194.

R=pquitslund@google.com

Review URL: https://chromiumcodereview.appspot.com//968053004
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b16a46..796af87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 0.1.6
+
+* Allow passing in selection to preserve through command line (#194).
+
 # 0.1.5+1, 0.1.5+2, 0.1.5+3
 
 * Fix test files to work in main Dart repo test runner.
diff --git a/bin/format.dart b/bin/format.dart
index 7703107..1238f39 100644
--- a/bin/format.dart
+++ b/bin/format.dart
@@ -10,6 +10,7 @@
 import 'package:dart_style/src/formatter_exception.dart';
 import 'package:dart_style/src/formatter_options.dart';
 import 'package:dart_style/src/io.dart';
+import 'package:dart_style/src/source_code.dart';
 
 void main(List<String> args) {
   var parser = new ArgParser(allowTrailingOptions: true);
@@ -19,6 +20,8 @@
   parser.addOption("line-length", abbr: "l",
       help: "Wrap lines longer than this.",
       defaultsTo: "80");
+  parser.addOption("preserve",
+      help: 'Selection to preserve, formatted as "start:length".');
   parser.addFlag("dry-run", abbr: "n", negatable: false,
       help: "Show which files would be modified but make no changes.");
   parser.addFlag("overwrite", abbr: "w", negatable: false,
@@ -35,9 +38,7 @@
   try {
     argResults = parser.parse(args);
   } on FormatException catch (err) {
-    printUsage(parser, err.message);
-    exitCode = 64;
-    return;
+    usageError(parser, err.message);
   }
 
   if (argResults["help"]) {
@@ -45,36 +46,45 @@
     return;
   }
 
+  // Can only preserve a selection when parsing from stdin.
+  var selection;
+
+  if (argResults["preserve"] != null && argResults.rest.isNotEmpty) {
+    usageError(parser, "Can only use --preserve when reading from stdin.");
+  }
+
+  try {
+    selection = parseSelection(argResults["preserve"]);
+  } on FormatException catch (_) {
+    usageError(parser,
+        '--preserve must be a colon-separated pair of integers, was '
+        '"${argResults['preserve']}".');
+  }
+
   if (argResults["dry-run"] && argResults["overwrite"]) {
-    printUsage(parser,
+    usageError(parser,
         "Cannot use --dry-run and --overwrite at the same time.");
-    exitCode = 64;
-    return;
   }
 
   checkForReporterCollision(String chosen, String other) {
-    if (!argResults[other]) return false;
+    if (!argResults[other]) return;
 
-    printUsage(parser,
+    usageError(parser,
         "Cannot use --$chosen and --$other at the same time.");
-    exitCode = 64;
-    return true;
   }
 
   var reporter = OutputReporter.print;
   if (argResults["dry-run"]) {
-    if (checkForReporterCollision("dry-run", "overwrite")) return;
-    if (checkForReporterCollision("dry-run", "machine")) return;
+    checkForReporterCollision("dry-run", "overwrite");
+    checkForReporterCollision("dry-run", "machine");
 
     reporter = OutputReporter.dryRun;
   } else if (argResults["overwrite"]) {
-    if (checkForReporterCollision("overwrite", "machine")) return;
+    checkForReporterCollision("overwrite", "machine");
 
     if (argResults.rest.isEmpty) {
-      printUsage(parser,
+      usageError(parser,
           "Cannot use --overwrite without providing any paths to format.");
-      exitCode = 64;
-      return;
     }
 
     reporter = OutputReporter.overwrite;
@@ -87,10 +97,8 @@
   try {
     pageWidth = int.parse(argResults["line-length"]);
   } on FormatException catch (_) {
-    printUsage(parser, '--line-length must be an integer, was '
+    usageError(parser, '--line-length must be an integer, was '
                        '"${argResults['line-length']}".');
-    exitCode = 64;
-    return;
   }
 
   var followLinks = argResults["follow-links"];
@@ -99,20 +107,43 @@
       pageWidth: pageWidth, followLinks: followLinks);
 
   if (argResults.rest.isEmpty) {
-    formatStdin(options);
+    formatStdin(options, selection);
   } else {
     formatPaths(options, argResults.rest);
   }
 }
 
+List<int> parseSelection(String selection) {
+  if (selection == null) return null;
+
+  var coordinates = selection.split(":");
+  if (coordinates.length != 2) {
+    throw new FormatException(
+        'Selection should be a colon-separated pair of integers, "123:45".');
+  }
+
+  return coordinates.map((coord) => coord.trim()).map(int.parse).toList();
+}
+
 /// Reads input from stdin until it's closed, and the formats it.
-void formatStdin(FormatterOptions options) {
+void formatStdin(FormatterOptions options, List<int> selection) {
+  var selectionStart = 0;
+  var selectionLength = 0;
+
+  if (selection != null) {
+    selectionStart = selection[0];
+    selectionLength = selection[1];
+  }
+
   var input = new StringBuffer();
   stdin.transform(new Utf8Decoder()).listen(input.write, onDone: () {
     var formatter = new DartFormatter(pageWidth: options.pageWidth);
     try {
-      var source = input.toString();
-      var output = formatter.format(source, uri: "stdin");
+      var source = new SourceCode(input.toString(),
+          uri: "stdin",
+          selectionStart: selectionStart,
+          selectionLength: selectionLength);
+      var output = formatter.formatSource(source);
       options.reporter.showFile(null, "<stdin>", output,
           changed: source != output);
       return true;
@@ -149,6 +180,12 @@
   }
 }
 
+/// Prints [error] and usage help then exits with exit code 64.
+void usageError(ArgParser parser, String error) {
+  printUsage(parser, error);
+  exit(64);
+}
+
 void printUsage(ArgParser parser, [String error]) {
   var output = stdout;
 
diff --git a/lib/src/formatter_options.dart b/lib/src/formatter_options.dart
index 396e10a..3869e8a 100644
--- a/lib/src/formatter_options.dart
+++ b/lib/src/formatter_options.dart
@@ -7,6 +7,8 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'source_code.dart';
+
 /// Global options that affect how the formatter produces and uses its outputs.
 class FormatterOptions {
   /// The [OutputReporter] used to show the formatting results.
@@ -52,13 +54,13 @@
   ///
   /// If the contents of the file are the same as the formatted output,
   /// [changed] will be false.
-  void showFile(File file, String label, String output, {bool changed});
+  void showFile(File file, String label, SourceCode output, {bool changed});
 }
 
 /// Prints only the names of files whose contents are different from their
 /// formatted version.
 class _DryRunReporter extends OutputReporter {
-  void showFile(File file, String label, String output, {bool changed}) {
+  void showFile(File file, String label, SourceCode output, {bool changed}) {
     // Only show the changed files.
     if (changed) print(label);
   }
@@ -78,16 +80,16 @@
     print("Skipping hidden file $path");
   }
 
-  void showFile(File file, String label, String output, {bool changed}) {
+  void showFile(File file, String label, SourceCode output, {bool changed}) {
     // Don't add an extra newline.
-    stdout.write(output);
+    stdout.write(output.text);
   }
 }
 
 /// Prints the formatted result and selection info of each file to stdout as a
 /// JSON map.
 class _PrintJsonReporter extends OutputReporter {
-  void showFile(File file, String label, String output, {bool changed}) {
+  void showFile(File file, String label, SourceCode output, {bool changed}) {
     // TODO(rnystrom): Put an empty selection in here to remain compatible with
     // the old formatter. Since there's no way to pass a selection on the
     // command line, this will never be used, which is why it's hard-coded to
@@ -95,17 +97,20 @@
     // result here.
     print(JSON.encode({
       "path": label,
-      "source": output,
-      "selection": {"offset": -1, "length": -1}
+      "source": output.text,
+      "selection": {
+        "offset": output.selectionStart != null ? output.selectionStart : -1,
+        "length": output.selectionLength != null ? output.selectionLength : -1
+      }
     }));
   }
 }
 
 /// Overwrites each file with its formatted result.
 class _OverwriteReporter extends _PrintReporter {
-  void showFile(File file, String label, String output, {bool changed}) {
+  void showFile(File file, String label, SourceCode output, {bool changed}) {
     if (changed) {
-      file.writeAsStringSync(output);
+      file.writeAsStringSync(output.text);
       print("Formatted $label");
     } else {
       print("Unchanged $label");
diff --git a/lib/src/io.dart b/lib/src/io.dart
index a1f5361..e7c8555 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -11,6 +11,7 @@
 import 'dart_formatter.dart';
 import 'formatter_options.dart';
 import 'formatter_exception.dart';
+import 'source_code.dart';
 
 /// Runs the formatter on every .dart file in [path] (and its subdirectories),
 /// and replaces them with their formatted output.
@@ -47,15 +48,15 @@
 /// Runs the formatter on [file].
 ///
 /// Returns `true` if successful or `false` if an error occurred.
-bool processFile(FormatterOptions options, File file,
-    {String label}) {
+bool processFile(FormatterOptions options, File file, {String label}) {
   if (label == null) label = file.path;
 
   var formatter = new DartFormatter(pageWidth: options.pageWidth);
   try {
-    var source = file.readAsStringSync();
-    var output = formatter.format(source, uri: file.path);
-    options.reporter.showFile(file, label, output, changed: source != output);
+    var source = new SourceCode(file.readAsStringSync(), uri: file.path);
+    var output = formatter.formatSource(source);
+    options.reporter.showFile(file, label, output,
+        changed: source.text != output.text);
     return true;
   } on FormatterException catch (err) {
     stderr.writeln(err.message());
diff --git a/pubspec.yaml b/pubspec.yaml
index 6611764..a7eefab 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart_style
-version: 0.1.5+3
+version: 0.1.6
 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/command_line_test.dart b/test/command_line_test.dart
index b2cf84a..4d1e0d9 100644
--- a/test/command_line_test.dart
+++ b/test/command_line_test.dart
@@ -147,6 +147,41 @@
     });
   });
 
+  group("--preserve", () {
+    test("errors if given paths.", () {
+      var process = runFormatter(["--preserve", "path", "another"]);
+      process.shouldExit(64);
+    });
+
+    test("errors on wrong number of components.", () {
+      var process = runFormatter(["--preserve", "1"]);
+      process.shouldExit(64);
+
+      process = runFormatter(["--preserve", "1:2:3"]);
+      process.shouldExit(64);
+    });
+
+    test("errors on non-integer component.", () {
+      var process = runFormatter(["--preserve", "1:2.3"]);
+      process.shouldExit(64);
+    });
+
+    test("updates selection.", () {
+      var process = runFormatter(["--preserve", "6:10", "-m"]);
+      process.writeLine(unformattedSource);
+      process.closeStdin();
+
+      var json = JSON.encode({
+        "path": "<stdin>",
+        "source": formattedSource,
+        "selection": {"offset": 5, "length": 9}
+      });
+
+      process.stdout.expect(json);
+      process.shouldExit();
+    });
+  });
+
   group("with no paths", () {
     test("errors on --overwrite.", () {
       var process = runFormatter(["--overwrite"]);