| Index: runtime/observatory/lib/src/elements/script_inset.dart
|
| diff --git a/runtime/observatory/lib/src/elements/script_inset.dart b/runtime/observatory/lib/src/elements/script_inset.dart
|
| index aef3fe3dde57d321ee2f8a107482a897802cf8a6..124697464f8cf859ccc140afdeadbdadb1f896b6 100644
|
| --- a/runtime/observatory/lib/src/elements/script_inset.dart
|
| +++ b/runtime/observatory/lib/src/elements/script_inset.dart
|
| @@ -4,11 +4,27 @@
|
|
|
| library script_inset_element;
|
|
|
| +import 'dart:async';
|
| import 'dart:html';
|
| import 'observatory_element.dart';
|
| import 'package:observatory/service.dart';
|
| import 'package:polymer/polymer.dart';
|
|
|
| +const nbsp = "\u00A0";
|
| +
|
| +class Annotation {
|
| + int line;
|
| + int columnStart;
|
| + int columnStop;
|
| + String title;
|
| +
|
| + void applyStyleTo(element) {
|
| + element.style.color = "blue";
|
| + element.style.textDecoration = "underline";
|
| + element.title = title;
|
| + }
|
| +}
|
| +
|
| /// Box with script source code in it.
|
| @CustomTag('script-inset')
|
| class ScriptInsetElement extends ObservatoryElement {
|
| @@ -26,93 +42,100 @@ class ScriptInsetElement extends ObservatoryElement {
|
| @observable int currentCol;
|
| @observable int startLine;
|
| @observable int endLine;
|
| - @observable bool linesReady = false;
|
|
|
| - // Contents are either ScriptLine or ScriptElipsis.
|
| - @observable List lines = toObservable([]);
|
| + var annotations = [];
|
| + var annotationsCursor;
|
| +
|
| + StreamSubscription scriptChangeSubscription;
|
|
|
| String makeLineId(int line) {
|
| return 'line-$line';
|
| }
|
|
|
| - String clip(String line, int start, [int limit]) {
|
| - try {
|
| - return line.substring(start, limit);
|
| - } catch (_) {
|
| - // NOTE(turnidge): Sometimes polymer updates give us garbage
|
| - // starts and limits during page updates.
|
| - return "OOB";
|
| - }
|
| - }
|
| -
|
| - MutationObserver _observer;
|
| -
|
| void _scrollToCurrentPos() {
|
| - var line = shadowRoot.querySelector('#line-$currentLine');
|
| + var line = querySelector('#${makeLineId(currentLine)}');
|
| if (line != null) {
|
| line.scrollIntoView();
|
| }
|
| }
|
|
|
| - void _onMutation(mutations, observer) {
|
| - _scrollToCurrentPos();
|
| - }
|
| -
|
| - void attached() {
|
| - super.attached();
|
| - var table = shadowRoot.querySelector('.sourceTable');
|
| - if (table != null) {
|
| - _observer = new MutationObserver(_onMutation);
|
| - _observer.observe(table, childList:true);
|
| - }
|
| - }
|
| -
|
| void detached() {
|
| - if (_observer != null) {
|
| - _observer.disconnect();
|
| - _observer = null;
|
| + if (scriptChangeSubscription != null) {
|
| + // Don't leak. If only Dart and Javascript exposed weak references...
|
| + scriptChangeSubscription.cancel();
|
| + scriptChangeSubscription = null;
|
| }
|
| super.detached();
|
| }
|
|
|
| void currentPosChanged(oldValue) {
|
| - _updateLines();
|
| + update();
|
| _scrollToCurrentPos();
|
| }
|
|
|
| void startPosChanged(oldValue) {
|
| - _updateLines();
|
| + update();
|
| }
|
|
|
| void endPosChanged(oldValue) {
|
| - _updateLines();
|
| + update();
|
| }
|
|
|
| void scriptChanged(oldValue) {
|
| - _updateLines();
|
| + update();
|
| }
|
|
|
| - var _updateFuture;
|
| + Element a(String text) => new AnchorElement()..text = text;
|
| + Element span(String text) => new SpanElement()..text = text;
|
|
|
| - void _updateLines() {
|
| - linesReady = false;
|
| - if (_updateFuture != null) {
|
| - // Already scheduled.
|
| - return;
|
| - }
|
| + Element hitsUnknown(Element element) {
|
| + element.classes.add('hitsNone');
|
| + element.title = "";
|
| + return element;
|
| + }
|
| + Element hitsNotExecuted(Element element) {
|
| + element.classes.add('hitsNotExecuted');
|
| + element.title = "Line did not execute";
|
| + return element;
|
| + }
|
| + Element hitsExecuted(Element element) {
|
| + element.classes.add('hitsExecuted');
|
| + element.title = "Line did execute";
|
| + return element;
|
| + }
|
| +
|
| + Element container;
|
| +
|
| + void update() {
|
| if (script == null) {
|
| - // Wait for script to be assigned.
|
| + // We may have previously had a script.
|
| + if (container != null) {
|
| + container.children.clear();
|
| + }
|
| return;
|
| }
|
| if (!script.loaded) {
|
| - _updateFuture = script.load().then((_) {
|
| - if (script.loaded) {
|
| - _updateFuture = null;
|
| - _updateLines();
|
| - }
|
| - });
|
| + script.load().then((_) => update());
|
| return;
|
| }
|
| +
|
| + if (scriptChangeSubscription == null) {
|
| + scriptChangeSubscription = script.changes.listen((_) => update());
|
| + }
|
| +
|
| + computeAnnotations();
|
| +
|
| + var table = linesTable();
|
| + if (container == null) {
|
| + // Indirect to avoid deleting the style element.
|
| + container = new DivElement();
|
| + shadowRoot.append(container);
|
| + }
|
| + container.children.clear();
|
| + container.children.add(table);
|
| + }
|
| +
|
| + void computeAnnotations() {
|
| startLine = (startPos != null
|
| ? script.tokenToLine(startPos)
|
| : 1);
|
| @@ -126,11 +149,30 @@ class ScriptInsetElement extends ObservatoryElement {
|
| ? script.tokenToLine(endPos)
|
| : script.lines.length);
|
|
|
| - lines.clear();
|
| + annotations.clear();
|
| + if (currentLine != null) {
|
| + var a = new Annotation();
|
| + a.line = currentLine;
|
| + a.columnStart = currentCol;
|
| + a.columnStop = currentCol + 1;
|
| + a.title = "Point of interest";
|
| + annotations.add(a);
|
| + }
|
| +
|
| + // TODO(rmacnak): Call site data.
|
| + }
|
| +
|
| + Element linesTable() {
|
| + var table = new DivElement();
|
| + table.classes.add("sourceTable");
|
| +
|
| + annotationsCursor = 0;
|
| +
|
| int blankLineCount = 0;
|
| for (int i = (startLine - 1); i <= (endLine - 1); i++) {
|
| if (script.lines[i].isBlank) {
|
| - // Try to introduce elipses if there are 4 or more contiguous blank lines.
|
| + // Try to introduce elipses if there are 4 or more contiguous
|
| + // blank lines.
|
| blankLineCount++;
|
| } else {
|
| if (blankLineCount > 0) {
|
| @@ -139,20 +181,95 @@ class ScriptInsetElement extends ObservatoryElement {
|
| if (blankLineCount < 4) {
|
| // Too few blank lines for an elipsis.
|
| for (int j = firstBlank; j <= lastBlank; j++) {
|
| - lines.add(script.lines[j]);
|
| + table.append(lineElement(script.lines[j]));
|
| }
|
| } else {
|
| // Add an elipsis for the skipped region.
|
| - lines.add(script.lines[firstBlank]);
|
| - lines.add(null);
|
| - lines.add(script.lines[lastBlank]);
|
| + table.append(lineElement(script.lines[firstBlank]));
|
| + table.append(lineElement(null));
|
| + table.append(lineElement(script.lines[lastBlank]));
|
| }
|
| blankLineCount = 0;
|
| }
|
| - lines.add(script.lines[i]);
|
| + table.append(lineElement(script.lines[i]));
|
| }
|
| }
|
| - linesReady = true;
|
| +
|
| + return table;
|
| + }
|
| +
|
| + // Assumes annotations are sorted.
|
| + Annotation nextAnnotationOnLine(int line) {
|
| + if (annotationsCursor >= annotations.length) return null;
|
| + var annotation = annotations[annotationsCursor];
|
| + if (annotation.line != line) return null;
|
| + annotationsCursor++;
|
| + return annotation;
|
| + }
|
| +
|
| + Element lineElement(ScriptLine line) {
|
| + var e = new DivElement();
|
| + e.classes.add("sourceRow");
|
| + e.append(lineBreakpointElement(line));
|
| + e.append(lineNumberElement(line));
|
| + e.append(lineSourceElement(line));
|
| + return e;
|
| + }
|
| +
|
| + Element lineBreakpointElement(ScriptLine line) {
|
| + BreakpointToggleElement e = new Element.tag("breakpoint-toggle");
|
| + e.line = line;
|
| + return e;
|
| + }
|
| +
|
| + Element lineNumberElement(ScriptLine line) {
|
| + var lineNumber = line == null ? "..." : line.line;
|
| + var e = span("$nbsp$lineNumber$nbsp");
|
| +
|
| + if ((line == null) || (line.hits == null)) {
|
| + hitsUnknown(e);
|
| + } else if (line.hits == 0) {
|
| + hitsNotExecuted(e);
|
| + } else {
|
| + hitsExecuted(e);
|
| + }
|
| +
|
| + return e;
|
| + }
|
| +
|
| + Element lineSourceElement(ScriptLine line) {
|
| + var e = new DivElement();
|
| + e.classes.add("sourceItem");
|
| +
|
| + if (line != null) {
|
| + if (line.line == currentLine) {
|
| + e.classes.add("sourceItemCurrent");
|
| + }
|
| +
|
| + e.id = makeLineId(line.line);
|
| +
|
| + var position = 0;
|
| + consumeUntil(var stop) {
|
| + if (stop <= position) {
|
| + return; // Empty gap between annotations/boundries.
|
| + }
|
| + var chunk = line.text.substring(position, stop);
|
| + var chunkNode = span(chunk);
|
| + e.append(chunkNode);
|
| + position = stop;
|
| + return chunkNode;
|
| + }
|
| +
|
| + // TODO(rmacnak): Tolerate overlapping annotations.
|
| + var annotation;
|
| + while ((annotation = nextAnnotationOnLine(line.line)) != null) {
|
| + consumeUntil(annotation.columnStart);
|
| + annotation.applyStyleTo(consumeUntil(annotation.columnStop));
|
| + }
|
| + consumeUntil(line.text.length);
|
| + }
|
| +
|
| + return e;
|
| }
|
|
|
| ScriptInsetElement.created() : super.created();
|
|
|