Chromium Code Reviews| 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..1067f85287678418761d4ce1b1cc88eda501a07b 100644 |
| --- a/runtime/observatory/lib/src/elements/script_inset.dart |
| +++ b/runtime/observatory/lib/src/elements/script_inset.dart |
| @@ -9,6 +9,21 @@ 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 { |
| @@ -28,91 +43,94 @@ class ScriptInsetElement extends ObservatoryElement { |
| @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. |
| return; |
|
Cutch
2015/02/28 00:37:04
In the case that a script was not null and then re
rmacnak
2015/03/02 19:08:26
Done.
|
| } |
| if (!script.loaded) { |
| - _updateFuture = script.load().then((_) { |
| - if (script.loaded) { |
| - _updateFuture = null; |
| - _updateLines(); |
| - } |
| - }); |
| - return; |
| + return script.load().then((_) => update()); |
| } |
| + |
| + 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 +144,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 = "Current invocation"; |
|
Cutch
2015/02/28 00:37:04
currentPos doesn't imply invocation. It is just th
rmacnak
2015/03/02 19:08:26
We should replace use currentPos with specific ann
|
| + 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 +176,92 @@ 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; |
|
Cutch
2015/02/28 00:37:04
You no longer update linesReady- why?
rmacnak
2015/03/02 19:08:26
Unused. Removed the field.
|
| + } |
| + |
| + // 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) { |
| + return new Element.tag("breakpoint-toggle") |
| + ..line = line; |
| + } |
| + |
| + 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(); |