Chromium Code Reviews| 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; |
| + } |
| +} |