| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library trydart.interaction_manager; | 5 library trydart.interaction_manager; |
| 6 | 6 |
| 7 import 'dart:html'; | 7 import 'dart:html'; |
| 8 | 8 |
| 9 import 'dart:convert' show | 9 import 'dart:convert' show |
| 10 JSON; | 10 JSON; |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 73 import 'shadow_root.dart' show | 73 import 'shadow_root.dart' show |
| 74 getShadowRoot, | 74 getShadowRoot, |
| 75 getText, | 75 getText, |
| 76 removeShadowRootPolyfill, | 76 removeShadowRootPolyfill, |
| 77 setShadowRoot; | 77 setShadowRoot; |
| 78 | 78 |
| 79 const String TRY_DART_NEW_DEFECT = | 79 const String TRY_DART_NEW_DEFECT = |
| 80 'https://code.google.com/p/dart/issues/entry' | 80 'https://code.google.com/p/dart/issues/entry' |
| 81 '?template=Try+Dart+Internal+Error'; | 81 '?template=Try+Dart+Internal+Error'; |
| 82 | 82 |
| 83 const Duration HEARTBEAT_INTERVAL = const Duration(milliseconds: 500); | 83 /// How frequently [InteractionManager.onHeartbeat] is called. |
| 84 const Duration HEARTBEAT_INTERVAL = const Duration(milliseconds: 50); |
| 84 | 85 |
| 86 /// Determines how frequently "project" files are saved. The time is measured |
| 87 /// from the time of last modification. |
| 85 const Duration SAVE_INTERVAL = const Duration(seconds: 5); | 88 const Duration SAVE_INTERVAL = const Duration(seconds: 5); |
| 86 | 89 |
| 90 /// Determines how frequently the compiler is invoked. The time is measured |
| 91 /// from the time of last modification. |
| 87 const Duration COMPILE_INTERVAL = const Duration(seconds: 1); | 92 const Duration COMPILE_INTERVAL = const Duration(seconds: 1); |
| 88 | 93 |
| 94 /// Determines if a compilation is slow. The time is measured from the last |
| 95 /// compilation started. If a compilation is slow, progress information is |
| 96 /// displayed to the user, but the console is untouched if the compilation |
| 97 /// finished quickly. The purpose is to reduce flicker in the UI. |
| 98 const Duration SLOW_COMPILE = const Duration(seconds: 1); |
| 99 |
| 89 /** | 100 /** |
| 90 * UI interaction manager for the entire application. | 101 * UI interaction manager for the entire application. |
| 91 */ | 102 */ |
| 92 abstract class InteractionManager { | 103 abstract class InteractionManager { |
| 93 // Design note: All UI interactions go through one instance of this | 104 // Design note: All UI interactions go through one instance of this |
| 94 // class. This is by design. | 105 // class. This is by design. |
| 95 // | 106 // |
| 96 // Simplicity in UI is in the eye of the beholder, not the implementor. Great | 107 // Simplicity in UI is in the eye of the beholder, not the implementor. Great |
| 97 // 'natural UI' is usually achieved with substantial implementation | 108 // 'natural UI' is usually achieved with substantial implementation |
| 98 // complexity that doesn't modularize well and has nasty complicated state | 109 // complexity that doesn't modularize well and has nasty complicated state |
| (...skipping 26 matching lines...) Expand all Loading... |
| 125 void onCompilationUnitChanged(CompilationUnit unit); | 136 void onCompilationUnitChanged(CompilationUnit unit); |
| 126 | 137 |
| 127 Future<List<String>> projectFileNames(); | 138 Future<List<String>> projectFileNames(); |
| 128 | 139 |
| 129 /// Called when the user selected a new project file. | 140 /// Called when the user selected a new project file. |
| 130 void onProjectFileSelected(String projectFile); | 141 void onProjectFileSelected(String projectFile); |
| 131 | 142 |
| 132 /// Called when notified about a project file changed (on the server). | 143 /// Called when notified about a project file changed (on the server). |
| 133 void onProjectFileFsEvent(MessageEvent e); | 144 void onProjectFileFsEvent(MessageEvent e); |
| 134 | 145 |
| 135 /// Called every 500ms. | 146 /// Called every [HEARTBEAT_INTERVAL]. |
| 136 void onHeartbeat(Timer timer); | 147 void onHeartbeat(Timer timer); |
| 137 | 148 |
| 138 /// Called by [:window.onMessage.listen:]. | 149 /// Called by [:window.onMessage.listen:]. |
| 139 void onMessage(MessageEvent event); | 150 void onWindowMessage(MessageEvent event); |
| 151 |
| 152 void onCompilationFailed(); |
| 140 | 153 |
| 141 void onCompilationDone(); | 154 void onCompilationDone(); |
| 142 | 155 |
| 143 /// Called when a compilation is starting, but just before sending the | 156 /// Called when a compilation is starting, but just before sending the |
| 144 /// initiating message to the compiler isolate. | 157 /// initiating message to the compiler isolate. |
| 145 void compilationStarting(); | 158 void compilationStarting(); |
| 159 |
| 160 // TODO(ahe): Remove this from InteractionManager, but not from InitialState. |
| 161 void consolePrintLine(line); |
| 162 |
| 163 void verboseCompilerMessage(String message); |
| 146 } | 164 } |
| 147 | 165 |
| 148 /** | 166 /** |
| 149 * State machine for UI interactions. | 167 * State machine for UI interactions. |
| 150 */ | 168 */ |
| 151 class InteractionContext extends InteractionManager { | 169 class InteractionContext extends InteractionManager { |
| 152 InteractionState state; | 170 InteractionState state; |
| 153 | 171 |
| 154 final Map<String, CompilationUnit> projectFiles = <String, CompilationUnit>{}; | 172 final Map<String, CompilationUnit> projectFiles = <String, CompilationUnit>{}; |
| 155 | 173 |
| 156 final Set<CompilationUnit> modifiedUnits = new Set<CompilationUnit>(); | 174 final Set<CompilationUnit> modifiedUnits = new Set<CompilationUnit>(); |
| 157 | 175 |
| 158 final Queue<CompilationUnit> unitsToSave = new Queue<CompilationUnit>(); | 176 final Queue<CompilationUnit> unitsToSave = new Queue<CompilationUnit>(); |
| 159 | 177 |
| 178 /// Tracks time since last modification of a "project" file. |
| 160 final Stopwatch saveTimer = new Stopwatch(); | 179 final Stopwatch saveTimer = new Stopwatch(); |
| 161 | 180 |
| 181 /// Tracks time since last modification. |
| 162 final Stopwatch compileTimer = new Stopwatch(); | 182 final Stopwatch compileTimer = new Stopwatch(); |
| 163 | 183 |
| 184 /// Tracks elapsed time of current compilation. |
| 185 final Stopwatch elapsedCompilationTime = new Stopwatch(); |
| 186 |
| 164 CompilationUnit currentCompilationUnit = | 187 CompilationUnit currentCompilationUnit = |
| 165 // TODO(ahe): Don't use a fake unit. | 188 // TODO(ahe): Don't use a fake unit. |
| 166 new CompilationUnit('fake', ''); | 189 new CompilationUnit('fake', ''); |
| 167 | 190 |
| 168 Timer heartbeat; | 191 Timer heartbeat; |
| 169 | 192 |
| 170 Completer<String> completeSaveOperation; | 193 Completer<String> completeSaveOperation; |
| 171 | 194 |
| 195 bool shouldClearConsole = false; |
| 196 |
| 197 Element compilerConsole; |
| 198 |
| 199 bool isFirstCompile = true; |
| 200 |
| 172 final Set<AnchorElement> oldDiagnostics = new Set<AnchorElement>(); | 201 final Set<AnchorElement> oldDiagnostics = new Set<AnchorElement>(); |
| 173 | 202 |
| 174 InteractionContext() | 203 InteractionContext() |
| 175 : super.internal() { | 204 : super.internal() { |
| 176 state = new InitialState(this); | 205 state = new InitialState(this); |
| 177 heartbeat = new Timer.periodic(HEARTBEAT_INTERVAL, onHeartbeat); | 206 heartbeat = new Timer.periodic(HEARTBEAT_INTERVAL, onHeartbeat); |
| 178 } | 207 } |
| 179 | 208 |
| 180 void onInput(Event event) => state.onInput(event); | 209 void onInput(Event event) => state.onInput(event); |
| 181 | 210 |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 222 void onProjectFileSelected(String projectFile) { | 251 void onProjectFileSelected(String projectFile) { |
| 223 return state.onProjectFileSelected(projectFile); | 252 return state.onProjectFileSelected(projectFile); |
| 224 } | 253 } |
| 225 | 254 |
| 226 void onProjectFileFsEvent(MessageEvent e) { | 255 void onProjectFileFsEvent(MessageEvent e) { |
| 227 return state.onProjectFileFsEvent(e); | 256 return state.onProjectFileFsEvent(e); |
| 228 } | 257 } |
| 229 | 258 |
| 230 void onHeartbeat(Timer timer) => state.onHeartbeat(timer); | 259 void onHeartbeat(Timer timer) => state.onHeartbeat(timer); |
| 231 | 260 |
| 232 void onMessage(MessageEvent event) => state.onMessage(event); | 261 void onWindowMessage(MessageEvent event) => state.onWindowMessage(event); |
| 262 |
| 263 void onCompilationFailed() => state.onCompilationFailed(); |
| 233 | 264 |
| 234 void onCompilationDone() => state.onCompilationDone(); | 265 void onCompilationDone() => state.onCompilationDone(); |
| 235 | 266 |
| 236 void compilationStarting() => state.compilationStarting(); | 267 void compilationStarting() => state.compilationStarting(); |
| 268 |
| 269 void consolePrintLine(line) => state.consolePrintLine(line); |
| 270 |
| 271 void verboseCompilerMessage(String message) { |
| 272 return state.verboseCompilerMessage(message); |
| 273 } |
| 237 } | 274 } |
| 238 | 275 |
| 239 abstract class InteractionState implements InteractionManager { | 276 abstract class InteractionState implements InteractionManager { |
| 240 InteractionContext get context; | 277 InteractionContext get context; |
| 241 | 278 |
| 242 // TODO(ahe): Remove this. | 279 // TODO(ahe): Remove this. |
| 243 Set<AnchorElement> get oldDiagnostics { | 280 Set<AnchorElement> get oldDiagnostics { |
| 244 throw 'Use context.oldDiagnostics instead'; | 281 throw 'Use context.oldDiagnostics instead'; |
| 245 } | 282 } |
| 246 | 283 |
| (...skipping 272 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 519 saveUnits(); | 556 saveUnits(); |
| 520 } | 557 } |
| 521 if (!settings.compilationPaused && | 558 if (!settings.compilationPaused && |
| 522 context.compileTimer.elapsed > COMPILE_INTERVAL) { | 559 context.compileTimer.elapsed > COMPILE_INTERVAL) { |
| 523 if (startCompilation()) { | 560 if (startCompilation()) { |
| 524 context.compileTimer | 561 context.compileTimer |
| 525 ..stop() | 562 ..stop() |
| 526 ..reset(); | 563 ..reset(); |
| 527 } | 564 } |
| 528 } | 565 } |
| 566 |
| 567 if (context.elapsedCompilationTime.elapsed > SLOW_COMPILE) { |
| 568 if (context.compilerConsole.parent == null) { |
| 569 outputDiv.append(context.compilerConsole); |
| 570 } |
| 571 } |
| 529 } | 572 } |
| 530 | 573 |
| 531 void saveUnits() { | 574 void saveUnits() { |
| 532 if (context.unitsToSave.isEmpty) return; | 575 if (context.unitsToSave.isEmpty) return; |
| 533 CompilationUnit unit = context.unitsToSave.removeFirst(); | 576 CompilationUnit unit = context.unitsToSave.removeFirst(); |
| 534 onError(ProgressEvent event) { | 577 onError(ProgressEvent event) { |
| 535 HttpRequest request = event.target; | 578 HttpRequest request = event.target; |
| 536 statusDiv.text = "Couldn't save '${unit.name}': ${request.responseText}"; | 579 statusDiv.text = "Couldn't save '${unit.name}': ${request.responseText}"; |
| 537 context.completeSaveOperation.complete(unit.name); | 580 context.completeSaveOperation.complete(unit.name); |
| 538 } | 581 } |
| 539 new HttpRequest() | 582 new HttpRequest() |
| 540 ..open("POST", "/project/${unit.name}") | 583 ..open("POST", "/project/${unit.name}") |
| 541 ..onError.listen(onError) | 584 ..onError.listen(onError) |
| 542 ..send(unit.content); | 585 ..send(unit.content); |
| 543 void setupCompleter() { | 586 void setupCompleter() { |
| 544 context.completeSaveOperation = new Completer<String>.sync(); | 587 context.completeSaveOperation = new Completer<String>.sync(); |
| 545 context.completeSaveOperation.future.then((String name) { | 588 context.completeSaveOperation.future.then((String name) { |
| 546 if (name == unit.name) { | 589 if (name == unit.name) { |
| 547 print("Saved source of '$name'"); | 590 print("Saved source of '$name'"); |
| 548 saveUnits(); | 591 saveUnits(); |
| 549 } else { | 592 } else { |
| 550 setupCompleter(); | 593 setupCompleter(); |
| 551 } | 594 } |
| 552 }); | 595 }); |
| 553 } | 596 } |
| 554 setupCompleter(); | 597 setupCompleter(); |
| 555 } | 598 } |
| 556 | 599 |
| 557 void onMessage(MessageEvent event) { | 600 void onWindowMessage(MessageEvent event) { |
| 558 if (event.source is! WindowBase || event.source == window) { | 601 if (event.source is! WindowBase || event.source == window) { |
| 559 return onBadMessage(event); | 602 return onBadMessage(event); |
| 560 } | 603 } |
| 561 if (event.data is List) { | 604 if (event.data is List) { |
| 562 List message = event.data; | 605 List message = event.data; |
| 563 if (message.length > 0) { | 606 if (message.length > 0) { |
| 564 switch (message[0]) { | 607 switch (message[0]) { |
| 565 case 'error': | 608 case 'error': |
| 566 return onErrorMessage(message[1]['url'], message[1]['message']); | 609 return onErrorMessage(message[1]['url'], message[1]['message']); |
| 567 case 'scrollHeight': | 610 case 'scrollHeight': |
| (...skipping 26 matching lines...) Expand all Loading... |
| 594 } | 637 } |
| 595 | 638 |
| 596 void onBadMessage(MessageEvent event) { | 639 void onBadMessage(MessageEvent event) { |
| 597 window.console | 640 window.console |
| 598 ..groupCollapsed('Bad message') | 641 ..groupCollapsed('Bad message') |
| 599 ..dir(event) | 642 ..dir(event) |
| 600 ..log(event.source.runtimeType) | 643 ..log(event.source.runtimeType) |
| 601 ..groupEnd(); | 644 ..groupEnd(); |
| 602 } | 645 } |
| 603 | 646 |
| 604 void consolePrintLine(data) { | 647 void consolePrintLine(line) { |
| 605 outputDiv.appendText('$data\n'); | 648 if (context.shouldClearConsole) { |
| 649 context.shouldClearConsole = false; |
| 650 outputDiv.nodes.clear(); |
| 651 } |
| 652 if (window.parent != window) { |
| 653 // Test support. |
| 654 // TODO(ahe): Use '/' instead of '*' when Firefox is upgraded to version |
| 655 // 30 across build bots. Support for '/' was added in version 29, and we |
| 656 // support the two most recent versions. |
| 657 window.parent.postMessage('$line\n', '*'); |
| 658 } |
| 659 outputDiv.appendText('$line\n'); |
| 660 } |
| 661 |
| 662 void onCompilationFailed() { |
| 606 } | 663 } |
| 607 | 664 |
| 608 void onCompilationDone() { | 665 void onCompilationDone() { |
| 666 context.isFirstCompile = false; |
| 667 context.elapsedCompilationTime.stop(); |
| 668 Duration compilationDuration = context.elapsedCompilationTime.elapsed; |
| 669 context.elapsedCompilationTime.reset(); |
| 670 print('Compilation took $compilationDuration.'); |
| 671 if (context.compilerConsole.parent != null) { |
| 672 context.compilerConsole.remove(); |
| 673 } |
| 609 for (AnchorElement diagnostic in context.oldDiagnostics) { | 674 for (AnchorElement diagnostic in context.oldDiagnostics) { |
| 610 if (diagnostic.parent != null) { | 675 if (diagnostic.parent != null) { |
| 611 // Problem fixed, remove the diagnostic. | 676 // Problem fixed, remove the diagnostic. |
| 612 diagnostic.replaceWith(new Text(getText(diagnostic))); | 677 diagnostic.replaceWith(new Text(getText(diagnostic))); |
| 613 } | 678 } |
| 614 } | 679 } |
| 615 context.oldDiagnostics.clear(); | 680 context.oldDiagnostics.clear(); |
| 616 observer.takeRecords(); // Discard mutations. | 681 observer.takeRecords(); // Discard mutations. |
| 617 } | 682 } |
| 618 | 683 |
| 619 void compilationStarting() { | 684 void compilationStarting() { |
| 685 var progress = new SpanElement() |
| 686 ..appendHtml('<i class="icon-spinner icon-spin"></i>') |
| 687 ..appendText(' Compiling Dart program.'); |
| 688 if (settings.verboseCompiler) { |
| 689 progress.appendText('..'); |
| 690 } |
| 691 context.compilerConsole = new SpanElement() |
| 692 ..append(progress) |
| 693 ..appendText('\n'); |
| 694 context.shouldClearConsole = true; |
| 695 context.elapsedCompilationTime |
| 696 ..start() |
| 697 ..reset(); |
| 698 if (context.isFirstCompile) { |
| 699 outputDiv.append(context.compilerConsole); |
| 700 } |
| 620 context.oldDiagnostics | 701 context.oldDiagnostics |
| 621 ..clear() | 702 ..clear() |
| 622 ..addAll(mainEditorPane.querySelectorAll('a.diagnostic')); | 703 ..addAll(mainEditorPane.querySelectorAll('a.diagnostic')); |
| 623 } | 704 } |
| 705 |
| 706 void verboseCompilerMessage(String message) { |
| 707 if (settings.verboseCompiler) { |
| 708 context.compilerConsole.appendText('$message\n'); |
| 709 } else { |
| 710 if (isCompilerStageMarker(message)) { |
| 711 Element progress = context.compilerConsole.firstChild; |
| 712 progress.appendText('.'); |
| 713 } |
| 714 } |
| 715 } |
| 624 } | 716 } |
| 625 | 717 |
| 626 Future<String> getString(uri) { | 718 Future<String> getString(uri) { |
| 627 return new Future<String>.sync(() => HttpRequest.getString('$uri')); | 719 return new Future<String>.sync(() => HttpRequest.getString('$uri')); |
| 628 } | 720 } |
| 629 | 721 |
| 630 class PendingInputState extends InitialState { | 722 class PendingInputState extends InitialState { |
| 631 PendingInputState(InteractionContext context) | 723 PendingInputState(InteractionContext context) |
| 632 : super(context); | 724 : super(context); |
| 633 | 725 |
| (...skipping 429 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1063 return text.split(new RegExp('^', multiLine: true)); | 1155 return text.split(new RegExp('^', multiLine: true)); |
| 1064 } | 1156 } |
| 1065 | 1157 |
| 1066 void removeCodeCompletion() { | 1158 void removeCodeCompletion() { |
| 1067 List<Node> highlighting = | 1159 List<Node> highlighting = |
| 1068 mainEditorPane.querySelectorAll('.dart-code-completion'); | 1160 mainEditorPane.querySelectorAll('.dart-code-completion'); |
| 1069 for (Element element in highlighting) { | 1161 for (Element element in highlighting) { |
| 1070 element.remove(); | 1162 element.remove(); |
| 1071 } | 1163 } |
| 1072 } | 1164 } |
| 1165 |
| 1166 bool isCompilerStageMarker(String message) { |
| 1167 return |
| 1168 message.startsWith('Package root is ') || |
| 1169 message.startsWith('Compiling ') || |
| 1170 message == "Resolving..." || |
| 1171 message.startsWith('Resolved ') || |
| 1172 message == "Inferring types..." || |
| 1173 message == "Compiling..." || |
| 1174 message.startsWith('Compiled '); |
| 1175 } |
| OLD | NEW |