Index: bin/inference/client.dart |
diff --git a/bin/inference/client.dart b/bin/inference/client.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3851c3c1c78ac29c79237f451c04657c6d6aed1f |
--- /dev/null |
+++ b/bin/inference/client.dart |
@@ -0,0 +1,236 @@ |
+// Copyright (c) 2015, 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. |
+ |
+/// Client component to display [GlobalResult]s as a web app. |
+library dart2js_info.bin.inference.client; |
+ |
+import 'dart:html' hide Entry; |
+import 'dart:convert'; |
+import 'package:dart2js_info/info.dart'; |
+import 'package:charcode/charcode.dart'; |
+ |
+AllInfo data; |
+main() async { |
+ data = AllInfo.parseFromJson( |
+ JSON.decode(await HttpRequest.getString('/data'))); |
+ |
+ routeByHash(); |
+ window.onHashChange.listen((_) => routeByHash()); |
+} |
+ |
+/// Does basic routing for the client UI. |
+routeByHash() { |
+ var hash = window.location.hash; |
+ if (hash.isEmpty || hash == '#' || hash == '#!') { |
+ handleHomePage(); |
+ } else if (hash.startsWith('#!')) { |
+ handleFileView(hash.substring(2)); |
+ } |
+} |
+ |
+/// Renders the home screen: a list of files with results. |
+handleHomePage() { |
+ var files = UrlRetriever.run(data); |
+ var html = new StringBuffer()..write('<ul>'); |
+ for (var file in files) { |
+ html.write('<li> <a href="#!$file">$file</a></li>'); |
+ } |
+ html.write('</ul>'); |
+ document.body.setInnerHtml('$html', treeSanitizer: NodeTreeSanitizer.trusted); |
+} |
+ |
+/// Renders the results of a single file: the code with highlighting for each |
+/// send. |
+handleFileView(String path) async { |
+ var contents = await HttpRequest.getString('file/$path'); |
+ var visitor = new SendHighlighter(path, contents); |
+ data.accept(visitor); |
+ var code = '${visitor.code}'; |
+ document.body.setInnerHtml(''' |
+ <div class="grid"> |
+ <div class="main code">$code</div> |
+ <div id="selections" class="right code"></div> |
+ </div> |
+ ''', |
+ treeSanitizer: NodeTreeSanitizer.trusted); |
+ |
+ var div = document.querySelector('#selections'); |
+ visitAllMetrics((metric, _) { |
+ if (metric is GroupedMetric || metric.name == 'reachable functions') return; |
+ var cssClassName = _classNameForMetric(metric); |
+ var node = new Element.html('<div>' |
+ '<span class="send $cssClassName inactive">${metric.name}</span>' |
+ '</div>'); |
+ node.children[0].onClick.listen((_) { |
+ document.querySelectorAll('.$cssClassName').classes.toggle('inactive'); |
+ }); |
+ div.append(node); |
+ }); |
+} |
+ |
+/// Extracts urls for all files mentioned in the results. |
+class UrlRetriever extends RecursiveInfoVisitor { |
+ List<String> _paths = []; |
+ |
+ static List<String> run(AllInfo results) { |
+ var visitor = new UrlRetriever(); |
+ results.accept(visitor); |
+ return visitor._paths; |
+ } |
+ |
+ @override |
+ visitLibrary(LibraryInfo info) { |
+ _paths.add(info.uri.path); |
+ super.visitLibrary(info); |
+ } |
+ |
+ @override |
+ visitFunction(FunctionInfo info) { |
+ var path = info.measurements?.uri?.path; |
+ if (path != null) _paths.add(path); |
+ } |
+} |
+ |
+/// Visitors that highlights every send in the text of a file using HTML |
+/// `<span>` tags. |
+class SendHighlighter extends RecursiveInfoVisitor { |
+ final String path; |
+ final StringEditBuffer code; |
+ |
+ SendHighlighter(this.path, String contents) |
+ : code = new StringEditBuffer(contents) { |
+ code.insert(0, '<span class="line">'); |
+ for (int i = 0; i < contents.length; i++) { |
+ if (contents.codeUnitAt(i) == $lt) { |
+ code.replace(i, i + 1, '<'); |
+ } else if (contents.codeUnitAt(i) == $gt) { |
+ code.replace(i, i + 1, '>'); |
+ } else if (contents.codeUnitAt(i) == $lf) { |
+ code.insert(i + 1, '</span><span class="line">'); |
+ } |
+ } |
+ code.insert(contents.length, '</span>'); |
+ } |
+ |
+ @override |
+ visitFunction(FunctionInfo function) { |
+ if (function.measurements?.uri?.path != path) return; |
+ var entries = function.measurements.entries; |
+ for (var metric in entries.keys) { |
+ if (metric is GroupedMetric) continue; |
+ var cssClassName = _classNameForMetric(metric); |
+ for (var entry in entries[metric]) { |
+ code.insert(entry.begin, |
+ '<span class="send ${cssClassName} inactive">', -entry.end); |
+ code.insert(entry.end, '</span>'); |
+ } |
+ } |
+ } |
+} |
+ |
+_classNameForMetric(Metric metric) => metric.name.replaceAll(' ', '-'); |
+ |
+/// A buffer meant to apply edits on a string (rather than building a string |
+/// from scratch). Each change is described using the location information on |
+/// the original string. Internally this buffer keeps track of how a |
+/// modification in one portion can offset a modification further down the |
+/// string. |
+class StringEditBuffer { |
Johnni Winther
2015/10/02 10:09:49
This should have its own package!
Siggi Cherem (dart-lang)
2015/10/02 17:16:17
:) - I moved it to it's own library for now and ad
|
+ final String original; |
+ final _edits = <_StringEdit>[]; |
+ |
+ StringEditBuffer(this.original); |
+ |
+ bool get hasEdits => _edits.length > 0; |
+ |
+ /// Edit the original text, replacing text on the range [begin] and |
+ /// exclusive [end] with the [replacement] string. |
+ void replace(int begin, int end, String replacement, [int sortId]) { |
+ _edits.add(new _StringEdit(begin, end, replacement, sortId)); |
+ } |
+ |
+ /// Insert [string] at [offset]. |
+ /// Equivalent to `replace(offset, offset, string)`. |
+ void insert(int offset, String string, [sortId]) => |
+ replace(offset, offset, string, sortId); |
+ |
+ /// Remove text from the range [begin] to exclusive [end]. |
+ /// Equivalent to `replace(begin, end, '')`. |
+ void remove(int begin, int end) => replace(begin, end, ''); |
+ |
+ /// Applies all pending [edit]s and returns a new string. |
+ /// |
+ /// This method is non-destructive: it does not discard existing edits or |
+ /// change the [original] string. Further edits can be added and this method |
+ /// can be called again. |
+ /// |
+ /// Throws [UnsupportedError] if the edits were overlapping. If no edits were |
+ /// made, the original string will be returned. |
+ String toString() { |
+ var sb = new StringBuffer(); |
+ if (_edits.length == 0) return original; |
+ |
+ // Sort edits by start location. |
+ _edits.sort(); |
+ |
+ int consumed = 0; |
+ for (var edit in _edits) { |
+ if (consumed > edit.begin) { |
+ sb = new StringBuffer(); |
+ sb.write('overlapping edits. Insert at offset '); |
+ sb.write(edit.begin); |
+ sb.write(' but have consumed '); |
+ sb.write(consumed); |
+ sb.write(' input characters. List of edits:'); |
+ for (var e in _edits) { |
+ sb.write('\n '); |
+ sb.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); |
+ sb.write(betweenEdits); |
+ sb.write(edit.string); |
+ consumed = edit.end; |
+ } |
+ |
+ // Add any text from the end of the original string that was not replaced. |
+ sb.write(original.substring(consumed)); |
+ return sb.toString(); |
+ } |
+} |
+ |
+/// A single edit in a [StringEditBuffer]. |
+class _StringEdit implements Comparable<_StringEdit> { |
+ // Offset where edit begins |
+ final int begin; |
+ |
+ // Offset where edit ends |
+ final int end; |
+ |
+ // Sort index as a tie-breaker for edits that have the same location. |
+ final int sortId; |
+ |
+ // String to insert |
+ final String string; |
+ |
+ _StringEdit(int begin, this.end, this.string, [int sortId]) |
+ : begin = begin, sortId = sortId == null ? begin : sortId; |
+ |
+ int get length => end - begin; |
+ |
+ String toString() => '(Edit @ $begin,$end: "$string")'; |
+ |
+ int compareTo(_StringEdit other) { |
+ int diff = begin - other.begin; |
+ if (diff != 0) return diff; |
+ diff = end - other.end; |
+ if (diff != 0) return diff; |
+ // use edit order as a tie breaker |
+ return sortId - other.sortId; |
+ } |
+} |