Make observable transform a barback transform.

R=jmesserly@google.com, nweiz@google.com, rnystrom@google.com

Review URL: https://codereview.chromium.org//22396004

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/source_maps@25985 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/printer.dart b/lib/printer.dart
index 333aadc..95539d2 100644
--- a/lib/printer.dart
+++ b/lib/printer.dart
@@ -12,8 +12,8 @@
 const int _LF = 10;
 const int _CR = 13;
 
-/// A printer that keeps track of offset locations and records source maps
-/// locations.
+/// A simple printer that keeps track of offset locations and records source
+/// maps locations.
 class Printer {
   final String filename;
   final StringBuffer _buff = new StringBuffer();
@@ -87,3 +87,159 @@
     _loc = loc;
   }
 }
+
+/// A more advanced printer that keeps track of offset locations to record
+/// source maps, but additionally allows nesting of different kind of items,
+/// including [NestedPrinter]s, and it let's you automatically indent text.
+///
+/// This class is especially useful when doing code generation, where different
+/// peices of the code are generated independently on separate printers, and are
+/// finally put together in the end.
+class NestedPrinter implements NestedItem {
+
+  /// Items recoded by this printer, which can be [String] literals,
+  /// [NestedItem]s, and source map information like [Location] and [Span].
+  List _items = [];
+
+  /// Internal buffer to merge consecutive strings added to this printer.
+  StringBuffer _buff;
+
+  /// Current indentation, which can be updated from outside this class.
+  int indent;
+
+  /// Item used to indicate that the following item is copied from the original
+  /// source code, and hence we should preserve source-maps on every new line.
+  static final _ORIGINAL = new Object();
+
+  NestedPrinter([this.indent = 0]);
+
+  /// Adds [object] to this printer. [object] can be a [String],
+  /// [NestedPrinter], or anything implementing [NestedItem]. If [object] is a
+  /// [String], the value is appended directly, without doing any formatting
+  /// changes. If you wish to add a line of code with automatic indentation, use
+  /// [addLine] instead.  [NestedPrinter]s and [NestedItem]s are not processed
+  /// until [build] gets called later on. We ensure that [build] emits every
+  /// object in the order that they were added to this printer.
+  ///
+  /// The [location] and [span] parameters indicate the corresponding source map
+  /// location of [object] in the original input. Only one, [location] or
+  /// [span], should be provided at a time.
+  ///
+  /// Indicate [isOriginal] when [object] is copied directly from the user code.
+  /// Setting [isOriginal] will make this printer propagate source map locations
+  /// on every line-break.
+  void add(object, {Location location, Span span, bool isOriginal: false}) {
+    if (object is! String || location != null || span != null || isOriginal) {
+      _flush();
+      assert(location == null || span == null);
+      if (location != null) _items.add(location);
+      if (span != null) _items.add(span);
+      if (isOriginal) _items.add(_ORIGINAL);
+    }
+
+    if (object is String) {
+      _appendString(object);
+    } else {
+      _items.add(object);
+    }
+  }
+
+  /// Append `2 * indent` spaces to this printer.
+  void insertIndent() => _indent(indent);
+
+  /// Add a [line], autoindenting to the current value of [indent]. Note,
+  /// indentation is not inferred from the contents added to this printer. If a
+  /// line starts or ends an indentation block, you need to also update [indent]
+  /// accordingly. Also, indentation is not adapted for nested printers. If
+  /// you add a [NestedPrinter] to this printer, its indentation is set
+  /// separately and will not include any the indentation set here.
+  ///
+  /// The [location] and [span] parameters indicate the corresponding source map
+  /// location of [object] in the original input. Only one, [location] or
+  /// [span], should be provided at a time.
+  void addLine(String line, {Location location, Span span}) {
+    if (location != null || span != null) {
+      _flush();
+      assert(location == null || span == null);
+      if (location != null) _items.add(location);
+      if (span != null) _items.add(span);
+    }
+    if (line == null) return;
+    if (line != '') {
+      // We don't indent empty lines.
+      _indent(indent);
+      _appendString(line);
+    }
+    _appendString('\n');
+  }
+
+  /// Appends a string merging it with any previous strings, if possible.
+  void _appendString(String s) {
+    if (_buff == null) _buff = new StringBuffer();
+    _buff.write(s);
+  }
+
+  /// Adds all of the current [_buff] contents as a string item.
+  void _flush() {
+    if (_buff != null) {
+      _items.add(_buff.toString());
+      _buff = null;
+    }
+  }
+
+  void _indent(int indent) {
+    for (int i = 0; i < indent; i++) _appendString('  ');
+  }
+
+  /// Returns a string representation of all the contents appended to this
+  /// printer, including source map location tokens.
+  String toString() {
+    _flush();
+    return (new StringBuffer()..writeAll(_items)).toString();
+  }
+
+  /// [Printer] used during the last call to [build], if any.
+  Printer printer;
+
+  /// Returns the text produced after calling [build].
+  String get text => printer.text;
+
+  /// Returns the source-map information produced after calling [build].
+  String get map => printer.map;
+
+  /// Builds the output of this printer and source map information. After
+  /// calling this function, you can use [text] and [map] to retrieve the
+  /// geenrated code and source map information, respectively.
+  void build(String filename) {
+    writeTo(printer = new Printer(filename));
+  }
+
+  /// Implements the [NestedItem] interface.
+  void writeTo(Printer printer) {
+    _flush();
+    bool propagate = false;
+    for (var item in _items) {
+      if (item is NestedItem) {
+        item.writeTo(printer);
+      } else if (item is String) {
+        printer.add(item, projectMarks: propagate);
+        propagate = false;
+      } else if (item is Location || item is Span) {
+        printer.mark(item);
+      } else if (item == _ORIGINAL) {
+        // we insert booleans when we are about to quote text that was copied
+        // from the original source. In such case, we will propagate marks on
+        // every new-line.
+        propagate = true;
+      } else {
+        throw new UnsupportedError('Unknown item type: $item');
+      }
+    }
+  }
+}
+
+/// An item added to a [NestedPrinter].
+abstract class NestedItem {
+  /// Write the contents of this item into [printer].
+  void writeTo(Printer printer);
+}
diff --git a/lib/refactor.dart b/lib/refactor.dart
new file mode 100644
index 0000000..45fb069
--- /dev/null
+++ b/lib/refactor.dart
@@ -0,0 +1,131 @@
+// Copyright (c) 2013, 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.
+
+/// Tools to help implement refactoring like transformations to Dart code.
+///
+/// [TextEditTransaction] supports making a series of changes to a text buffer.
+/// [guessIndent] helps to guess the appropriate indentiation for the new code.
+library source_maps.refactor;
+
+import 'span.dart';
+import 'printer.dart';
+
+/// Editable text transaction.
+///
+/// Applies a series of edits using original location
+/// information, and composes them into the edited string.
+class TextEditTransaction {
+  final SourceFile file;
+  final String original;
+  final _edits = <_TextEdit>[];
+
+  TextEditTransaction(this.original, this.file);
+
+  bool get hasEdits => _edits.length > 0;
+
+  /// Edit the original text, replacing text on the range [begin] and [end]
+  /// with the [replacement]. [replacement] can be either a string or a
+  /// [NestedPrinter].
+  void edit(int begin, int end, replacement) {
+    _edits.add(new _TextEdit(begin, end, replacement));
+  }
+
+  /// Create a source map [Location] for [offset].
+  Location _loc(int offset) =>
+      file != null ? file.location(offset) : null;
+
+  /// Applies all pending [edit]s and returns a [NestedPrinter] containing the
+  /// rewritten string and source map information. [filename] is given to the
+  /// underlying printer to indicate the name of the generated file that will
+  /// contains the source map information.
+  /// 
+  /// Throws [UnsupportedError] if the edits were overlapping. If no edits were
+  /// made, the printer simply contains the original string.
+  NestedPrinter commit() {
+    var printer = new NestedPrinter();
+    if (_edits.length == 0) {
+      return printer..add(original, location: _loc(0), isOriginal: true);
+    }
+
+    // Sort edits by start location.
+    _edits.sort();
+
+    int consumed = 0;
+    for (var edit in _edits) {
+      if (consumed > edit.begin) {
+        var sb = new StringBuffer();
+        sb..write(file.location(edit.begin).formatString)
+            ..write(': overlapping edits. Insert at offset ')
+            ..write(edit.begin)
+            ..write(' but have consumed ')
+            ..write(consumed)
+            ..write(' input characters. List of edits:');
+        for (var e in _edits) sb..write('\n    ')..write(e);
+        throw new UnsupportedError(sb.toString());
+      }
+
+      // Add characters from the original string between this edit and the last
+      // one, if any.
+      var betweenEdits = original.substring(consumed, edit.begin);
+      printer..add(betweenEdits, location: _loc(consumed), isOriginal: true)
+             ..add(edit.replace, location: _loc(edit.begin));
+      consumed = edit.end;
+    }
+
+    // Add any text from the end of the original string that was not replaced.
+    printer.add(original.substring(consumed),
+        location: _loc(consumed), isOriginal: true);
+    return printer;
+  }
+}
+
+class _TextEdit implements Comparable<_TextEdit> {
+  final int begin;
+  final int end;
+
+  /// The replacement used by the edit, can be a string or a [NestedPrinter].
+  final replace;
+
+  _TextEdit(this.begin, this.end, this.replace);
+
+  int get length => end - begin;
+
+  String toString() => '(Edit @ $begin,$end: "$replace")';
+
+  int compareTo(_TextEdit other) {
+    int diff = begin - other.begin;
+    if (diff != 0) return diff;
+    return end - other.end;
+  }
+}
+
+/// Returns all whitespace characters at the start of [charOffset]'s line.
+String guessIndent(String code, int charOffset) {
+  // Find the beginning of the line
+  int lineStart = 0;
+  for (int i = charOffset - 1; i >= 0; i--) {
+    var c = code.codeUnitAt(i);
+    if (c == _LF || c == _CR) {
+      lineStart = i + 1;
+      break;
+    }
+  }
+
+  // Grab all the whitespace
+  int whitespaceEnd = code.length;
+  for (int i = lineStart; i < code.length; i++) {
+    var c = code.codeUnitAt(i);
+    if (c != _SPACE && c != _TAB) {
+      whitespaceEnd = i;
+      break;
+    }
+  }
+
+  return code.substring(lineStart, whitespaceEnd);
+}
+
+const int _CR = 13;
+const int _LF = 10;
+const int _TAB = 9;
+const int _SPACE = 32;
diff --git a/lib/source_maps.dart b/lib/source_maps.dart
index 8fc48c9..2d4a4cc 100644
--- a/lib/source_maps.dart
+++ b/lib/source_maps.dart
@@ -39,4 +39,5 @@
 export "builder.dart";
 export "parser.dart";
 export "printer.dart";
+export "refactor.dart";
 export "span.dart";
diff --git a/test/printer_test.dart b/test/printer_test.dart
index d038ad5..06bed57 100644
--- a/test/printer_test.dart
+++ b/test/printer_test.dart
@@ -79,4 +79,44 @@
     expect(printer2.text, out);
     expect(printer2.map, printer.map);
   });
+
+  group('nested printer', () {
+    test('simple use', () {
+      var printer = new NestedPrinter();
+      printer..add('var ')
+             ..add('x = 3;\n', location: inputVar1)
+             ..add('f(', location: inputFunction)
+             ..add('y) => ', location: inputVar2)
+             ..add('x + y;\n', location: inputExpr)
+             ..build('output.dart');
+      expect(printer.text, OUTPUT);
+      expect(printer.map, json.stringify(EXPECTED_MAP));
+    });
+
+    test('nested use', () {
+      var printer = new NestedPrinter();
+      printer..add('var ')
+             ..add(new NestedPrinter()..add('x = 3;\n', location: inputVar1))
+             ..add('f(', location: inputFunction)
+             ..add(new NestedPrinter()..add('y) => ', location: inputVar2))
+             ..add('x + y;\n', location: inputExpr)
+             ..build('output.dart');
+      expect(printer.text, OUTPUT);
+      expect(printer.map, json.stringify(EXPECTED_MAP));
+    });
+
+    test('add indentation', () {
+      var out = INPUT.replaceAll('long', '_s');
+      var lines = INPUT.trim().split('\n');
+      expect(lines.length, 7);
+      var printer = new NestedPrinter();
+      for (int i = 0; i < lines.length; i++) {
+        if (i == 5) printer.indent++;
+        printer.addLine(lines[i].replaceAll('long', '_s').trim());
+        if (i == 5) printer.indent--;
+      }
+      printer.build('output.dart');
+      expect(printer.text, out);
+    });
+  });
 }
diff --git a/test/refactor_test.dart b/test/refactor_test.dart
new file mode 100644
index 0000000..c153d90
--- /dev/null
+++ b/test/refactor_test.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2013, 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 polymer.test.refactor_test;
+
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/refactor.dart';
+import 'package:source_maps/span.dart';
+import 'package:source_maps/parser.dart' show parse, Mapping;
+
+main() {
+  group('conflict detection', () {
+    var original = "0123456789abcdefghij";
+    var file = new SourceFile.text('', original);
+
+    test('no conflict, in order', () {
+      var txn = new TextEditTransaction(original, file);
+      txn.edit(2, 4, '.');
+      txn.edit(5, 5, '|');
+      txn.edit(6, 6, '-');
+      txn.edit(6, 7, '_');
+      expect((txn.commit()..build('')).text, "01.4|5-_789abcdefghij");
+    });
+
+    test('no conflict, out of order', () {
+      var txn = new TextEditTransaction(original, file);
+      txn.edit(2, 4, '.');
+      txn.edit(5, 5, '|');
+
+      // Regresion test for issue #404: there is no conflict/overlap for edits
+      // that don't remove any of the original code.
+      txn.edit(6, 7, '_');
+      txn.edit(6, 6, '-');
+      expect((txn.commit()..build('')).text, "01.4|5-_789abcdefghij");
+
+    });
+
+    test('conflict', () {
+      var txn = new TextEditTransaction(original, file);
+      txn.edit(2, 4, '.');
+      txn.edit(3, 3, '-');
+      expect(() => txn.commit(), throwsA(predicate(
+            (e) => e.toString().contains('overlapping edits'))));
+    });
+  });
+
+  test('generated source maps', () {
+    var original =
+        "0123456789\n0*23456789\n01*3456789\nabcdefghij\nabcd*fghij\n";
+    var file = new SourceFile.text('', original);
+    var txn = new TextEditTransaction(original, file);
+    txn.edit(27, 29, '__\n    ');
+    txn.edit(34, 35, '___');
+    var printer = (txn.commit()..build(''));
+    var output = printer.text;
+    var map = parse(printer.map);
+    expect(output,
+        "0123456789\n0*23456789\n01*34__\n    789\na___cdefghij\nabcd*fghij\n");
+
+    // Line 1 and 2 are unmodified: mapping any column returns the beginning
+    // of the corresponding line:
+    expect(_span(1, 1, map, file), ":1:1: \n0123456789");
+    expect(_span(1, 5, map, file), ":1:1: \n0123456789");
+    expect(_span(2, 1, map, file), ":2:1: \n0*23456789");
+    expect(_span(2, 8, map, file), ":2:1: \n0*23456789");
+
+    // Line 3 is modified part way: mappings before the edits have the right
+    // mapping, after the edits the mapping is null.
+    expect(_span(3, 1, map, file), ":3:1: \n01*3456789");
+    expect(_span(3, 5, map, file), ":3:1: \n01*3456789");
+
+    // Start of edits map to beginning of the edit secion:
+    expect(_span(3, 6, map, file), ":3:6: \n01*3456789");
+    expect(_span(3, 7, map, file), ":3:6: \n01*3456789");
+
+    // Lines added have no mapping (they should inherit the last mapping),
+    // but the end of the edit region continues were we left off:
+    expect(_span(4, 1, map, file), isNull);
+    expect(_span(4, 5, map, file), ":3:8: \n01*3456789");
+
+    // Subsequent lines are still mapped correctly:
+    expect(_span(5, 1, map, file), ":4:1: \nabcdefghij"); // a (in a___cd...)
+    expect(_span(5, 2, map, file), ":4:2: \nabcdefghij"); // _ (in a___cd...)
+    expect(_span(5, 3, map, file), ":4:2: \nabcdefghij"); // _ (in a___cd...)
+    expect(_span(5, 4, map, file), ":4:2: \nabcdefghij"); // _ (in a___cd...)
+    expect(_span(5, 5, map, file), ":4:3: \nabcdefghij"); // c (in a___cd...)
+    expect(_span(6, 1, map, file), ":5:1: \nabcd*fghij");
+    expect(_span(6, 8, map, file), ":5:1: \nabcd*fghij");
+  });
+}
+
+String _span(int line, int column, Mapping map, SourceFile file) {
+  var span = map.spanFor(line - 1, column - 1, files: {'': file});
+  return span == null ? null : span.getLocationMessage('').trim();
+}
diff --git a/test/run.dart b/test/run.dart
index 9a19785..876cff7 100755
--- a/test/run.dart
+++ b/test/run.dart
@@ -13,6 +13,7 @@
 import 'end2end_test.dart' as end2end_test;
 import 'parser_test.dart' as parser_test;
 import 'printer_test.dart' as printer_test;
+import 'refactor_test.dart' as refactor_test;
 import 'span_test.dart' as span_test;
 import 'utils_test.dart' as utils_test;
 import 'vlq_test.dart' as vlq_test;
@@ -32,6 +33,7 @@
   addGroup('end2end_test.dart', end2end_test.main);
   addGroup('parser_test.dart', parser_test.main);
   addGroup('printer_test.dart', printer_test.main);
+  addGroup('refactor_test.dart', refactor_test.main);
   addGroup('span_test.dart', span_test.main);
   addGroup('utils_test.dart', utils_test.main);
   addGroup('vlq_test.dart', vlq_test.main);