Initial support of getStack (#435)

* Initial support of getStack
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index e277380..a69df8c 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.3
+
+- Add support for `getScript` for paused isolates.
+
 ## 0.3.2
 
 - Add support for `scope` in `evaluate` calls.
diff --git a/dwds/example/hello_world/main.dart b/dwds/example/hello_world/main.dart
index 6583235..9c7a2b7 100644
--- a/dwds/example/hello_world/main.dart
+++ b/dwds/example/hello_world/main.dart
@@ -41,6 +41,10 @@
     });
   };
 
+  Timer.periodic(Duration(seconds: 1), (_) {
+    printCount();
+  });
+
   // Register one up front before the proxy connects, the isolate should still
   // recognize this as an available extension.
   registerExtension('ext.hello_world.existing', (_, __) => null);
@@ -48,6 +52,13 @@
   window.console.debug('Page Ready');
 }
 
+var count = 0;
+
+// An easy location to add a breakpoint.
+void printCount() {
+  print('The count is ${++count}');
+}
+
 String helloString(String response) => response;
 bool helloBool(bool response) => response;
 num helloNum(num response) => response;
diff --git a/dwds/lib/src/chrome_proxy_service.dart b/dwds/lib/src/chrome_proxy_service.dart
index 6b3d1bd..b61e198 100644
--- a/dwds/lib/src/chrome_proxy_service.dart
+++ b/dwds/lib/src/chrome_proxy_service.dart
@@ -14,6 +14,7 @@
 import 'dart_uri.dart';
 import 'debugger.dart';
 import 'helpers.dart';
+import 'location.dart';
 
 /// A proxy from the chrome debug protocol to the dart vm service protocol.
 class ChromeProxyService implements VmServiceInterface {
@@ -43,11 +44,19 @@
   /// Provides debugger-related functionality.
   Debugger debugger;
 
+  /// The current Dart stack for the paused [_isolate].
+  ///
+  /// This is null if the [_isolate] is not paused.
+  Stack _pausedStack;
+
   /// Fields that are specific to the current [_isolate].
   ///
   /// These need to get cleared whenever [destroyIsolate] is called.
+  // TODO(grouma) - Subclass Isolate to group these together. This will ensure
+  // that they are properly cleaned up and documented.
   final _classes = <String, Class>{};
   final _scriptRefs = <String, ScriptRef>{};
+  final _serverPathToScriptRef = <String, ScriptRef>{};
   final _libraries = <String, Library>{};
   final _libraryRefs = <String, LibraryRef>{};
   StreamSubscription<ConsoleAPIEvent> _consoleSubscription;
@@ -183,8 +192,10 @@
     _vm.isolates.removeWhere((ref) => ref.id == _isolate.id);
 
     _isolate = null;
+    _pausedStack = null;
     _classes.clear();
     _scriptRefs.clear();
+    _serverPathToScriptRef.clear();
     _libraries.clear();
     _libraryRefs.clear();
     _consoleSubscription.cancel();
@@ -474,6 +485,8 @@
       ..id = createId();
 
     _scriptRefs[scriptRef.id] = scriptRef;
+    _serverPathToScriptRef[DartUri(libraryRef.id, scriptRef.uri).serverPath] =
+        scriptRef;
 
     return Library()
       ..id = libraryRef.id
@@ -523,6 +536,9 @@
     return ScriptList()..scripts = scripts;
   }
 
+  /// Returns the [ScriptRef] for the provided Dart server path [uri].
+  ScriptRef scriptRefFor(String uri) => _serverPathToScriptRef[uri];
+
   Future<Script> _getScript(String isolateId, ScriptRef scriptRef) async {
     var libraryId = scriptRef.uri;
     // TODO(401): Remove uri parameter.
@@ -560,10 +576,12 @@
     throw UnimplementedError();
   }
 
+  /// Returns the current stack.
+  ///
+  /// Returns null if the corresponding isolate is not paused.
   @override
-  Future<Stack> getStack(String isolateId) {
-    throw UnimplementedError();
-  }
+  Future<Stack> getStack(String isolateId) async =>
+      isolateId == _isolate?.id ? _pausedStack : null;
 
   @override
   Future<VM> getVM() => Future.value(_vm);
@@ -771,6 +789,28 @@
     return controller;
   }
 
+  List<Frame> _dartFramesFor(DebuggerPausedEvent e) {
+    var dartFrames = <Frame>[];
+    var index = 0;
+    for (var frame in e.params['callFrames']) {
+      var location = frame['location'];
+      // TODO(grouma) - This function name is JS based. Add logic to
+      // translate this to a Dart function name.
+      var functionName = frame['functionName'] as String ?? '';
+      // Chrome is 0 based. Account for this.
+      var jsLocation = JsLocation.fromZeroBased(location['scriptId'] as String,
+          location['lineNumber'] as int, location['columnNumber'] as int);
+      var dartFrame = debugger.frameFor(jsLocation);
+      if (dartFrame != null) {
+        dartFrame.code.name =
+            functionName.isEmpty ? '(anonymous)' : functionName;
+        dartFrame.index = index++;
+        dartFrames.add(dartFrame);
+      }
+    }
+    return dartFrames;
+  }
+
   /// Listens to the `debugger` events from chrome and translates those to
   /// the `Debug` stream events for the vm service protocol.
   ///
@@ -792,10 +832,14 @@
         } else {
           event.kind = EventKind.kPauseInterrupted;
         }
+        _pausedStack = Stack()
+          ..frames = _dartFramesFor(e)
+          ..messages = [];
         streamNotify('Debug', event);
       });
       resumeSubscription = tabConnection.debugger.onResumed.listen((e) {
         if (_isolate == null) return;
+        _pausedStack = null;
         streamNotify(
             'Debug',
             Event()
diff --git a/dwds/lib/src/debugger.dart b/dwds/lib/src/debugger.dart
index a14342e..6f0bf9f 100644
--- a/dwds/lib/src/debugger.dart
+++ b/dwds/lib/src/debugger.dart
@@ -67,7 +67,7 @@
     // TODO(401): Remove the additional parameter.
     var dartUri = DartUri(
         dartScript.uri, '${Uri.parse(mainProxy.uri).path}/garbage.dart');
-    var location = locationFor(dartUri, line);
+    var location = locationForDart(dartUri, line);
     // TODO: Handle cases where a breakpoint can't be set exactly at that line.
     if (location == null) return null;
     var jsBreakpointId = await _setBreakpoint(location);
@@ -109,8 +109,8 @@
     var response =
         await chromeDebugger.sendCommand('Debugger.setBreakpoint', params: {
       'location': {
-        'scriptId': location.jsScriptId,
-        'lineNumber': location.jsLine - 1,
+        'scriptId': location.jsLocation.scriptId,
+        'lineNumber': location.jsLocation.line - 1,
       }
     });
     handleErrorIfPresent(response);
@@ -127,9 +127,43 @@
   /// Find the [Location] for the given Dart source position.
   ///
   /// The [line] number is 1-based.
-  Location locationFor(DartUri uri, int line) => sources
-      .locationsFor(uri.serverPath)
-      .firstWhere((location) => location.dartLine == line, orElse: () => null);
+  Location locationForDart(DartUri uri, int line) => sources
+      .locationsForDart(uri.serverPath)
+      .firstWhere((location) => location.dartLocation.line == line,
+          orElse: () => null);
+
+  /// Returns the closest [Location] for the given Dart source position.
+  ///
+  /// The [line] and [column] are 1-based.
+  ///
+  /// Can return null if no suitable [Location] is found.
+  Location _bestLocationForJs(String scriptId, int line, int column) {
+    Location result;
+    for (var location in sources.locationsForJs(scriptId)) {
+      if (location.jsLocation.line == line) {
+        result ??= location;
+        if ((location.jsLocation.column - column).abs() <
+            (result.jsLocation.column - column).abs()) {
+          result = location;
+        }
+      }
+    }
+    return result;
+  }
+
+  /// Returns a Dart [Frame] for a [JsLocation].
+  Frame frameFor(JsLocation jsLocation) {
+    var location = _bestLocationForJs(
+        jsLocation.scriptId, jsLocation.line, jsLocation.column);
+    if (location == null) return null;
+    var script = mainProxy.scriptRefFor(location.dartLocation.uri.serverPath);
+    return Frame()
+      ..code = (CodeRef()..kind = CodeKind.kDart)
+      ..location = (SourceLocation()
+        ..tokenPos = location.tokenPos
+        ..script = script)
+      ..kind = FrameKind.kRegular;
+  }
 }
 
 /// Keeps track of the Dart and JS breakpoint Ids that correspond.
diff --git a/dwds/lib/src/location.dart b/dwds/lib/src/location.dart
index 9b97676..2dc2615 100644
--- a/dwds/lib/src/location.dart
+++ b/dwds/lib/src/location.dart
@@ -4,47 +4,29 @@
 
 import 'package:source_maps/parser.dart';
 
+import 'dart_uri.dart';
+
 var _startTokenId = 1337;
 
 /// A source location, with both Dart and JS information.
-///
-/// Note that line and column numbers here are always 1-based. The  Dart VM
-/// Service protocol line/column numbers are one-based, but in JS source maps and
-/// the Chrome protocol are zero-based, so they require translation.
 class Location {
-  final String jsScriptId;
+  final JsLocation jsLocation;
 
-  /// 1 based row offset within the JS source code.
-  final int jsLine;
-
-  /// 1 based column offset within the JS source code.
-  final int jsColumn;
-
-  final String dartUri;
-
-  /// 1 based row offset within the Dart source code.
-  final int dartLine;
-
-  /// 1 based column offset within the Dart source code.
-  final int dartColumn;
+  final DartLocation dartLocation;
 
   /// An arbitrary integer value used to represent this location.
   final int tokenPos;
 
   Location._(
-    this.jsScriptId,
-    this.jsLine,
-    this.jsColumn,
-    this.dartUri,
-    this.dartLine,
-    this.dartColumn,
+    this.jsLocation,
+    this.dartLocation,
   ) : tokenPos = _startTokenId++;
 
   static Location from(
     String scriptId,
     TargetLineEntry lineEntry,
     TargetEntry entry,
-    String dartUrl,
+    DartUri dartUri,
   ) {
     var dartLine = entry.sourceLine;
     var dartColumn = entry.sourceColumn;
@@ -52,7 +34,54 @@
     var jsColumn = entry.column;
     // lineEntry data is 0 based according to:
     // https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k
-    return Location._(scriptId, jsLine + 1, jsColumn + 1, dartUrl, dartLine + 1,
-        dartColumn + 1);
+    return Location._(JsLocation.fromZeroBased(scriptId, jsLine, jsColumn),
+        DartLocation.fromZeroBased(dartUri, dartLine, dartColumn));
   }
 }
+
+/// Location information for a Dart source.
+class DartLocation {
+  final DartUri uri;
+
+  /// 1 based row offset within the Dart source code.
+  final int line;
+
+  /// 1 based column offset within the Dart source code.
+  final int column;
+
+  DartLocation._(
+    this.uri,
+    this.line,
+    this.column,
+  );
+
+  static DartLocation fromZeroBased(DartUri uri, int line, int column) =>
+      DartLocation._(uri, line + 1, column + 1);
+
+  static DartLocation fromOneBased(DartUri uri, int line, int column) =>
+      DartLocation._(uri, line, column);
+}
+
+/// Location information for a JS source.
+class JsLocation {
+  /// The script ID as provided by Chrome.
+  final String scriptId;
+
+  /// 1 based row offset within the JS source code.
+  final int line;
+
+  /// 1 based column offset within the JS source code.
+  final int column;
+
+  JsLocation._(
+    this.scriptId,
+    this.line,
+    this.column,
+  );
+
+  static JsLocation fromZeroBased(String scriptId, int line, int column) =>
+      JsLocation._(scriptId, line + 1, column + 1);
+
+  static JsLocation fromOneBased(String scriptId, int line, int column) =>
+      JsLocation._(scriptId, line, column);
+}
diff --git a/dwds/lib/src/sources.dart b/dwds/lib/src/sources.dart
index 481b0e2..2a402a5 100644
--- a/dwds/lib/src/sources.dart
+++ b/dwds/lib/src/sources.dart
@@ -30,15 +30,19 @@
   /// Map from Dart server path to all corresponding [Location] data.
   final _sourceToLocation = <String, Set<Location>>{};
 
-  /// Map from Dart URL to server path.
-  final _canonicalPaths = <String, String>{};
+  /// Map from JS scriptId to all corresponding [Location] data.
+  final _scriptIdToLocation = <String, Set<Location>>{};
 
   Sources(this._mainProxy);
 
   /// Returns all [Location] data for a provided Dart source.
-  Set<Location> locationsFor(String serverPath) =>
+  Set<Location> locationsForDart(String serverPath) =>
       _sourceToLocation[serverPath] ?? {};
 
+  /// Returns all [Location] data for a provided JS scriptId.
+  Set<Location> locationsForJs(String scriptId) =>
+      _scriptIdToLocation[scriptId] ?? {};
+
   /// Called to handle the event that a script has been parsed
   /// and add its sourcemap information.
   Future<Null> scriptParsed(ScriptParsedEvent e) async {
@@ -53,19 +57,18 @@
         for (var entry in lineEntry.entries) {
           var index = entry.sourceUrlId;
           if (index == null) continue;
-          var dartUrl = mapping.urls[index];
+          var dartUri = DartUri(mapping.urls[index], script.url);
           var location = Location.from(
             script.scriptId,
             lineEntry,
             entry,
-            dartUrl,
+            dartUri,
           );
-          _canonicalPaths.putIfAbsent(
-              dartUrl,
-              // TODO(401): Remove the additional parameter after D24 is stable.
-              () => DartUri(dartUrl, script.url).serverPath);
           _sourceToLocation
-              .putIfAbsent(_canonicalPaths[dartUrl], () => Set())
+              .putIfAbsent(dartUri.serverPath, () => Set())
+              .add(location);
+          _scriptIdToLocation
+              .putIfAbsent(script.scriptId, () => Set())
               .add(location);
         }
       }
@@ -90,7 +93,7 @@
     var lineNumberToLocation = <int, Set<Location>>{};
     for (var location in locations) {
       lineNumberToLocation
-          .putIfAbsent(location.dartLine, () => Set())
+          .putIfAbsent(location.dartLocation.line, () => Set())
           .add(location);
     }
     for (var lineNumber in lineNumberToLocation.keys) {
@@ -98,7 +101,7 @@
         lineNumber,
         for (var location in lineNumberToLocation[lineNumber]) ...[
           location.tokenPos,
-          location.dartColumn
+          location.dartLocation.column
         ]
       ]);
     }
diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml
index 2c51183..cd10a32 100644
--- a/dwds/pubspec.yaml
+++ b/dwds/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dwds
-version: 0.3.2
+version: 0.3.3-dev
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/webdev/tree/master/dwds
 description: >-
diff --git a/dwds/test/chrome_proxy_service_test.dart b/dwds/test/chrome_proxy_service_test.dart
index cbdee3e..749f596 100644
--- a/dwds/test/chrome_proxy_service_test.dart
+++ b/dwds/test/chrome_proxy_service_test.dart
@@ -35,7 +35,6 @@
     Isolate isolate;
     ScriptList scripts;
     ScriptRef mainScript;
-    var breakpoints = <Breakpoint>[];
 
     setUp(() async {
       vm = await service.getVM();
@@ -46,19 +45,12 @@
       await service.debugger.sources.waitForSourceMap('hello_world/main.dart');
     });
 
-    tearDown(() async {
-      for (var breakpoint in breakpoints) {
-        await service.removeBreakpointInternal(breakpoint.id);
-      }
-      breakpoints = [];
-    });
-
     test('addBreakpoint', () async {
       // TODO: Much more testing.
       var bp = await service.addBreakpoint(isolate.id, mainScript.id, 21);
-      breakpoints = isolate.breakpoints;
-      breakpoints = [bp];
-      expect(breakpoints.any((b) => b.location.tokenPos == 42), isNotNull);
+      // Remove breakpoint so it doesn't impact other tests.
+      await service.removeBreakpointInternal(bp.id);
+      expect(bp.id, '1');
     });
 
     test('addBreakpointAtEntry', () {
@@ -351,8 +343,55 @@
     expect(() => service.getSourceReport(null, null), throwsUnimplementedError);
   });
 
-  test('getStack', () {
-    expect(() => service.getStack(null), throwsUnimplementedError);
+  group('getStack', () {
+    String isolateId;
+    Stream<Event> stream;
+    ScriptList scripts;
+    ScriptRef mainScript;
+
+    setUp(() async {
+      var vm = await service.getVM();
+      isolateId = vm.isolates.first.id;
+      scripts = await service.getScripts(isolateId);
+      await service.streamListen('Debug');
+      stream = service.onEvent('Debug');
+      mainScript =
+          scripts.scripts.firstWhere((each) => each.uri.contains('main.dart'));
+      await service.debugger.sources.waitForSourceMap('hello_world/main.dart');
+    });
+
+    test('returns null if not paused', () async {
+      expect(await service.getStack(isolateId), isNull);
+    });
+
+    test('returns stack when broken', () async {
+      var bp = await service.addBreakpoint(isolateId, mainScript.id, 59);
+      // Wait for breakpoint to trigger.
+      await stream
+          .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+      // Remove breakpoint so it doesn't impact other tests.
+      await service.removeBreakpointInternal(bp.id);
+      var stack = await service.getStack(isolateId);
+      // Resume as to not impact other tests.
+      await service.resume(isolateId);
+      expect(stack, isNotNull);
+      expect(stack.frames.length, 2);
+      var first = stack.frames.first;
+      expect(first.kind, 'Regular');
+      expect(first.code.kind, 'Dart');
+      // TODO(grouma) - Expect a Dart name.
+      expect(first.code.name, 'main.printCount');
+    });
+
+    test('returns non-empty stack when paused', () async {
+      await service.pause(isolateId);
+      // Wait for pausing to actually propagate.
+      await stream
+          .firstWhere((event) => event.kind == EventKind.kPauseInterrupted);
+      expect(await service.getStack(isolateId), isNotNull);
+      // Resume the isolate to not impact other tests.
+      await service.resume(isolateId);
+    });
   });
 
   test('getVM', () async {