Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(524)

Side by Side Diff: site/try/src/interaction_manager.dart

Issue 2232273004: Delete site/try (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 library trydart.interaction_manager;
6
7 import 'dart:html';
8
9 import 'dart:convert' show
10 JSON;
11
12 import 'dart:math' show
13 max,
14 min;
15
16 import 'dart:async' show
17 Completer,
18 Future,
19 Timer;
20
21 import 'dart:collection' show
22 Queue;
23
24 import 'package:compiler/src/scanner/string_scanner.dart' show
25 StringScanner;
26
27 import 'package:compiler/src/tokens/token.dart' show
28 BeginGroupToken,
29 ErrorToken,
30 Token,
31 UnmatchedToken,
32 UnterminatedToken;
33
34 import 'package:compiler/src/tokens/token_constants.dart' show
35 EOF_TOKEN,
36 STRING_INTERPOLATION_IDENTIFIER_TOKEN,
37 STRING_INTERPOLATION_TOKEN,
38 STRING_TOKEN;
39
40 import 'package:compiler/src/io/source_file.dart' show
41 StringSourceFile;
42
43 import 'package:compiler/src/string_validator.dart' show
44 StringValidator;
45
46 import 'package:compiler/src/tree/tree.dart' show
47 StringQuoting;
48
49 import 'compilation.dart' show
50 currentSource,
51 startCompilation;
52
53 import 'ui.dart' show
54 currentTheme,
55 hackDiv,
56 mainEditorPane,
57 observer,
58 outputDiv,
59 outputFrame,
60 statusDiv;
61
62 import 'decoration.dart' show
63 CodeCompletionDecoration,
64 Decoration,
65 DiagnosticDecoration,
66 error,
67 info,
68 warning;
69
70 import 'html_to_text.dart' show
71 htmlToText;
72
73 import 'compilation_unit.dart' show
74 CompilationUnit;
75
76 import 'selection.dart' show
77 TrySelection,
78 isCollapsed;
79
80 import 'editor.dart' as editor;
81
82 import 'mock.dart' as mock;
83
84 import 'settings.dart' as settings;
85
86 import 'shadow_root.dart' show
87 getShadowRoot,
88 getText,
89 setShadowRoot,
90 containsNode;
91
92 import 'iframe_error_handler.dart' show
93 ErrorMessage;
94
95 const String TRY_DART_NEW_DEFECT =
96 'https://code.google.com/p/dart/issues/entry'
97 '?template=Try+Dart+Internal+Error';
98
99 /// How frequently [InteractionManager.onHeartbeat] is called.
100 const Duration HEARTBEAT_INTERVAL = const Duration(milliseconds: 50);
101
102 /// Determines how frequently "project" files are saved. The time is measured
103 /// from the time of last modification.
104 const Duration SAVE_INTERVAL = const Duration(seconds: 5);
105
106 /// Determines how frequently the compiler is invoked. The time is measured
107 /// from the time of last modification.
108 const Duration COMPILE_INTERVAL = const Duration(seconds: 1);
109
110 /// Determines how frequently the compiler is invoked in "live" mode. The time
111 /// is measured from the time of last modification.
112 const Duration LIVE_COMPILE_INTERVAL = const Duration(seconds: 0);
113
114 /// Determines if a compilation is slow. The time is measured from the last
115 /// compilation started. If a compilation is slow, progress information is
116 /// displayed to the user, but the console is untouched if the compilation
117 /// finished quickly. The purpose is to reduce flicker in the UI.
118 const Duration SLOW_COMPILE = const Duration(seconds: 1);
119
120 const int TAB_WIDTH = 2;
121
122 /**
123 * UI interaction manager for the entire application.
124 */
125 abstract class InteractionManager {
126 // Design note: All UI interactions go through one instance of this
127 // class. This is by design.
128 //
129 // Simplicity in UI is in the eye of the beholder, not the implementor. Great
130 // 'natural UI' is usually achieved with substantial implementation
131 // complexity that doesn't modularize well and has nasty complicated state
132 // dependencies.
133 //
134 // In rare cases, some UI components can be independent of this state
135 // machine. For example, animation and auto-save loops.
136
137 // Implementation note: The state machine is actually implemented by
138 // [InteractionContext], this class represents public event handlers.
139
140 factory InteractionManager() => new InteractionContext();
141
142 InteractionManager.internal();
143
144 // TODO(ahe): Remove this.
145 Set<AnchorElement> get oldDiagnostics;
146
147 void onInput(Event event);
148
149 // TODO(ahe): Rename to onKeyDown (as it is called in response to keydown
150 // event).
151 void onKeyUp(KeyboardEvent event);
152
153 void onMutation(List<MutationRecord> mutations, MutationObserver observer);
154
155 void onSelectionChange(Event event);
156
157 /// Called when the content of a CompilationUnit changed.
158 void onCompilationUnitChanged(CompilationUnit unit);
159
160 Future<List<String>> projectFileNames();
161
162 /// Called when the user selected a new project file.
163 void onProjectFileSelected(String projectFile);
164
165 /// Called when notified about a project file changed (on the server).
166 void onProjectFileFsEvent(MessageEvent e);
167
168 /// Called every [HEARTBEAT_INTERVAL].
169 void onHeartbeat(Timer timer);
170
171 /// Called by [:window.onMessage.listen:].
172 void onWindowMessage(MessageEvent event);
173
174 void onCompilationFailed(String firstError);
175
176 void onCompilationDone();
177
178 /// Called when a compilation is starting, but just before sending the
179 /// initiating message to the compiler isolate.
180 void compilationStarting();
181
182 // TODO(ahe): Remove this from InteractionManager, but not from InitialState.
183 void consolePrintLine(line);
184
185 /// Called just before running a freshly compiled program.
186 void aboutToRun();
187
188 /// Called when an error occurs when running user code in an iframe.
189 void onIframeError(ErrorMessage message);
190
191 void verboseCompilerMessage(String message);
192
193 /// Called if the compiler crashes.
194 void onCompilerCrash(data);
195
196 /// Called if an internal error is detected.
197 void onInternalError(message);
198 }
199
200 /**
201 * State machine for UI interactions.
202 */
203 class InteractionContext extends InteractionManager {
204 InteractionState state;
205
206 final Map<String, CompilationUnit> projectFiles = <String, CompilationUnit>{};
207
208 final Set<CompilationUnit> modifiedUnits = new Set<CompilationUnit>();
209
210 final Queue<CompilationUnit> unitsToSave = new Queue<CompilationUnit>();
211
212 /// Tracks time since last modification of a "project" file.
213 final Stopwatch saveTimer = new Stopwatch();
214
215 /// Tracks time since last modification.
216 final Stopwatch compileTimer = new Stopwatch();
217
218 /// Tracks elapsed time of current compilation.
219 final Stopwatch elapsedCompilationTime = new Stopwatch();
220
221 CompilationUnit currentCompilationUnit =
222 // TODO(ahe): Don't use a fake unit.
223 new CompilationUnit('fake', '');
224
225 Timer heartbeat;
226
227 Completer<String> completeSaveOperation;
228
229 bool shouldClearConsole = false;
230
231 Element compilerConsole;
232
233 bool isFirstCompile = true;
234
235 final Set<AnchorElement> oldDiagnostics = new Set<AnchorElement>();
236
237 final Duration compileInterval = settings.live.value
238 ? LIVE_COMPILE_INTERVAL
239 : COMPILE_INTERVAL;
240
241 InteractionContext()
242 : super.internal() {
243 state = new InitialState(this);
244 heartbeat = new Timer.periodic(HEARTBEAT_INTERVAL, onHeartbeat);
245 }
246
247 void onInput(Event event) => state.onInput(event);
248
249 void onKeyUp(KeyboardEvent event) => state.onKeyUp(event);
250
251 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
252 workAroundFirefoxBug();
253 try {
254 try {
255 return state.onMutation(mutations, observer);
256 } finally {
257 // Discard any mutations during the observer, as these can lead to
258 // infinite loop.
259 observer.takeRecords();
260 }
261 } catch (error, stackTrace) {
262 try {
263 editor.isMalformedInput = true;
264 state.onInternalError(
265 '\nError and stack trace:\n$error\n$stackTrace\n');
266 } catch (e) {
267 // Double faults ignored.
268 }
269 rethrow;
270 }
271 }
272
273 void onSelectionChange(Event event) => state.onSelectionChange(event);
274
275 void onCompilationUnitChanged(CompilationUnit unit) {
276 return state.onCompilationUnitChanged(unit);
277 }
278
279 Future<List<String>> projectFileNames() => state.projectFileNames();
280
281 void onProjectFileSelected(String projectFile) {
282 return state.onProjectFileSelected(projectFile);
283 }
284
285 void onProjectFileFsEvent(MessageEvent e) {
286 return state.onProjectFileFsEvent(e);
287 }
288
289 void onHeartbeat(Timer timer) => state.onHeartbeat(timer);
290
291 void onWindowMessage(MessageEvent event) => state.onWindowMessage(event);
292
293 void onCompilationFailed(String firstError) {
294 return state.onCompilationFailed(firstError);
295 }
296
297 void onCompilationDone() => state.onCompilationDone();
298
299 void compilationStarting() => state.compilationStarting();
300
301 void consolePrintLine(line) => state.consolePrintLine(line);
302
303 void aboutToRun() => state.aboutToRun();
304
305 void onIframeError(ErrorMessage message) => state.onIframeError(message);
306
307 void verboseCompilerMessage(String message) {
308 return state.verboseCompilerMessage(message);
309 }
310
311 void onCompilerCrash(data) => state.onCompilerCrash(data);
312
313 void onInternalError(message) => state.onInternalError(message);
314 }
315
316 abstract class InteractionState implements InteractionManager {
317 InteractionContext get context;
318
319 // TODO(ahe): Remove this.
320 Set<AnchorElement> get oldDiagnostics {
321 throw 'Use context.oldDiagnostics instead';
322 }
323
324 void set state(InteractionState newState);
325
326 void onStateChanged(InteractionState previous) {
327 }
328
329 void transitionToInitialState() {
330 state = new InitialState(context);
331 }
332 }
333
334 class InitialState extends InteractionState {
335 final InteractionContext context;
336 bool requestCodeCompletion = false;
337
338 InitialState(this.context);
339
340 void set state(InteractionState state) {
341 InteractionState previous = context.state;
342 if (previous != state) {
343 context.state = state;
344 state.onStateChanged(previous);
345 }
346 }
347
348 void onInput(Event event) {
349 state = new PendingInputState(context);
350 }
351
352 void onKeyUp(KeyboardEvent event) {
353 if (computeHasModifier(event)) {
354 onModifiedKeyUp(event);
355 } else {
356 onUnmodifiedKeyUp(event);
357 }
358 }
359
360 void onModifiedKeyUp(KeyboardEvent event) {
361 if (event.getModifierState("Shift")) return onShiftedKeyUp(event);
362 switch (event.keyCode) {
363 case KeyCode.S:
364 // Disable Ctrl-S, Cmd-S, etc. We have observed users hitting these
365 // keys often when using Try Dart and getting frustrated.
366 event.preventDefault();
367 // TODO(ahe): Consider starting a compilation.
368 break;
369 }
370 }
371
372 void onShiftedKeyUp(KeyboardEvent event) {
373 switch (event.keyCode) {
374 case KeyCode.TAB:
375 event.preventDefault();
376 break;
377 }
378 }
379
380 void onUnmodifiedKeyUp(KeyboardEvent event) {
381 switch (event.keyCode) {
382 case KeyCode.ENTER: {
383 Selection selection = window.getSelection();
384 if (isCollapsed(selection)) {
385 event.preventDefault();
386 Node node = selection.anchorNode;
387 if (node is Text) {
388 Text text = node;
389 int offset = selection.anchorOffset;
390 // If at end-of-file, insert an extra newline. The the extra
391 // newline ensures that the next line isn't empty. At least Chrome
392 // behaves as if "\n" is just a single line. "\nc" (where c is any
393 // character) is two lines, according to Chrome.
394 String newline = isAtEndOfFile(text, offset) ? '\n\n' : '\n';
395 text.insertData(offset, newline);
396 selection.collapse(text, offset + 1);
397 } else if (node is Element) {
398 node.appendText('\n\n');
399 selection.collapse(node.firstChild, 1);
400 } else {
401 window.console
402 ..error('Unexpected node')
403 ..dir(node);
404 }
405 }
406 break;
407 }
408 case KeyCode.TAB: {
409 Selection selection = window.getSelection();
410 if (isCollapsed(selection)) {
411 event.preventDefault();
412 Text text = new Text(' ' * TAB_WIDTH);
413 selection.getRangeAt(0).insertNode(text);
414 selection.collapse(text, TAB_WIDTH);
415 }
416 break;
417 }
418 }
419
420 // This is a hack to get Safari (iOS) to send mutation events on
421 // contenteditable.
422 // TODO(ahe): Move to onInput?
423 var newDiv = new DivElement();
424 hackDiv.replaceWith(newDiv);
425 hackDiv = newDiv;
426 }
427
428 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
429 removeCodeCompletion();
430
431 Selection selection = window.getSelection();
432 TrySelection trySelection = new TrySelection(mainEditorPane, selection);
433
434 Set<Node> normalizedNodes = new Set<Node>();
435 for (MutationRecord record in mutations) {
436 normalizeMutationRecord(record, trySelection, normalizedNodes);
437 }
438
439 if (normalizedNodes.length == 1) {
440 Node node = normalizedNodes.single;
441 if (node is Element && node.classes.contains('lineNumber')) {
442 print('Single line change: ${node.outerHtml}');
443
444 updateHighlighting(node, selection, trySelection, mainEditorPane);
445 return;
446 }
447 }
448
449 updateHighlighting(mainEditorPane, selection, trySelection);
450 }
451
452 void updateHighlighting(
453 Element node,
454 Selection selection,
455 TrySelection trySelection,
456 [Element root]) {
457 String state = '';
458 String currentText = getText(node);
459 if (root != null) {
460 // Single line change.
461 trySelection = trySelection.copyWithRoot(node);
462 Element previousLine = node.previousElementSibling;
463 if (previousLine != null) {
464 state = previousLine.getAttribute('dart-state');
465 }
466
467 node.parentNode.insertAllBefore(
468 createHighlightedNodes(trySelection, currentText, state),
469 node);
470 node.remove();
471 } else {
472 root = node;
473 editor.seenIdentifiers = new Set<String>.from(mock.identifiers);
474
475 // Fail safe: new [nodes] are computed before clearing old nodes.
476 List<Node> nodes =
477 createHighlightedNodes(trySelection, currentText, state);
478
479 node.nodes
480 ..clear()
481 ..addAll(nodes);
482 }
483
484 if (containsNode(mainEditorPane, trySelection.anchorNode)) {
485 // Sometimes the anchor node is removed by the above call. This has
486 // only been observed in Firefox, and is hard to reproduce.
487 trySelection.adjust(selection);
488 }
489
490 // TODO(ahe): We know almost exactly what has changed. It could be
491 // more efficient to only communicate what changed.
492 context.currentCompilationUnit.content = getText(root);
493
494 // Discard highlighting mutations.
495 observer.takeRecords();
496 }
497
498 List<Node> createHighlightedNodes(
499 TrySelection trySelection,
500 String currentText,
501 String state) {
502 trySelection.updateText(currentText);
503
504 editor.isMalformedInput = false;
505 int offset = 0;
506 List<Node> nodes = <Node>[];
507
508 for (String line in splitLines(currentText)) {
509 List<Node> lineNodes = <Node>[];
510 state =
511 tokenizeAndHighlight(line, state, offset, trySelection, lineNodes);
512 offset += line.length;
513 nodes.add(makeLine(lineNodes, state));
514 }
515
516 return nodes;
517 }
518
519 void onSelectionChange(Event event) {
520 }
521
522 void onStateChanged(InteractionState previous) {
523 super.onStateChanged(previous);
524 context.compileTimer
525 ..start()
526 ..reset();
527 }
528
529 void onCompilationUnitChanged(CompilationUnit unit) {
530 if (unit == context.currentCompilationUnit) {
531 currentSource = unit.content;
532 if (context.projectFiles.containsKey(unit.name)) {
533 postProjectFileUpdate(unit);
534 }
535 context.compileTimer.start();
536 } else {
537 print("Unexpected change to compilation unit '${unit.name}'.");
538 }
539 }
540
541 void postProjectFileUpdate(CompilationUnit unit) {
542 context.modifiedUnits.add(unit);
543 context.saveTimer.start();
544 }
545
546 Future<List<String>> projectFileNames() {
547 return getString('project?list').then((String response) {
548 WebSocket socket = new WebSocket('ws://127.0.0.1:9090/ws/watch');
549 socket.onMessage.listen(context.onProjectFileFsEvent);
550 return new List<String>.from(JSON.decode(response));
551 });
552 }
553
554 void onProjectFileSelected(String projectFile) {
555 // Disable editing whilst fetching data.
556 mainEditorPane.contentEditable = 'false';
557
558 CompilationUnit unit = context.projectFiles[projectFile];
559 Future<CompilationUnit> future;
560 if (unit != null) {
561 // This project file had been fetched already.
562 future = new Future<CompilationUnit>.value(unit);
563
564 // TODO(ahe): Probably better to fetch the sources again.
565 } else {
566 // This project file has to be fetched.
567 future = getString('project/$projectFile').then((String text) {
568 CompilationUnit unit = context.projectFiles[projectFile];
569 if (unit == null) {
570 // Only create a new unit if the value hadn't arrived already.
571 unit = new CompilationUnit(projectFile, text);
572 context.projectFiles[projectFile] = unit;
573 } else {
574 // TODO(ahe): Probably better to overwrite sources. Create a new
575 // unit?
576 // The server should push updates to the client.
577 }
578 return unit;
579 });
580 }
581 future.then((CompilationUnit unit) {
582 mainEditorPane
583 ..contentEditable = 'true'
584 ..nodes.clear();
585 observer.takeRecords(); // Discard mutations.
586
587 transitionToInitialState();
588 context.currentCompilationUnit = unit;
589
590 // Install the code, which will trigger a call to onMutation.
591 mainEditorPane.appendText(unit.content);
592 });
593 }
594
595 void transitionToInitialState() {}
596
597 void onProjectFileFsEvent(MessageEvent e) {
598 Map map = JSON.decode(e.data);
599 List modified = map['modify'];
600 if (modified == null) return;
601 for (String name in modified) {
602 Completer completer = context.completeSaveOperation;
603 if (completer != null && !completer.isCompleted) {
604 completer.complete(name);
605 } else {
606 onUnexpectedServerModification(name);
607 }
608 }
609 }
610
611 void onUnexpectedServerModification(String name) {
612 if (context.currentCompilationUnit.name == name) {
613 mainEditorPane.contentEditable = 'false';
614 statusDiv.text = 'Modified on disk';
615 }
616 }
617
618 void onHeartbeat(Timer timer) {
619 if (context.unitsToSave.isEmpty &&
620 context.saveTimer.elapsed > SAVE_INTERVAL) {
621 context.saveTimer
622 ..stop()
623 ..reset();
624 context.unitsToSave.addAll(context.modifiedUnits);
625 context.modifiedUnits.clear();
626 saveUnits();
627 }
628 if (!settings.compilationPaused &&
629 context.compileTimer.elapsed > context.compileInterval) {
630 if (startCompilation()) {
631 context.compileTimer
632 ..stop()
633 ..reset();
634 }
635 }
636
637 if (context.elapsedCompilationTime.elapsed > SLOW_COMPILE) {
638 if (context.compilerConsole.parent == null) {
639 outputDiv.append(context.compilerConsole);
640 }
641 }
642 }
643
644 void saveUnits() {
645 if (context.unitsToSave.isEmpty) return;
646 CompilationUnit unit = context.unitsToSave.removeFirst();
647 onError(ProgressEvent event) {
648 HttpRequest request = event.target;
649 statusDiv.text = "Couldn't save '${unit.name}': ${request.responseText}";
650 context.completeSaveOperation.complete(unit.name);
651 }
652 new HttpRequest()
653 ..open("POST", "/project/${unit.name}")
654 ..onError.listen(onError)
655 ..send(unit.content);
656 void setupCompleter() {
657 context.completeSaveOperation = new Completer<String>.sync();
658 context.completeSaveOperation.future.then((String name) {
659 if (name == unit.name) {
660 print("Saved source of '$name'");
661 saveUnits();
662 } else {
663 setupCompleter();
664 }
665 });
666 }
667 setupCompleter();
668 }
669
670 void onWindowMessage(MessageEvent event) {
671 if (event.source is! WindowBase || event.source == window) {
672 return onBadMessage(event);
673 }
674 if (event.data is List) {
675 List message = event.data;
676 if (message.length > 0) {
677 switch (message[0]) {
678 case 'scrollHeight':
679 return onScrollHeightMessage(message[1]);
680 }
681 }
682 return onBadMessage(event);
683 } else {
684 return consolePrintLine(event.data);
685 }
686 }
687
688 /// Called when an iframe is modified.
689 void onScrollHeightMessage(int scrollHeight) {
690 window.console.log('scrollHeight = $scrollHeight');
691 if (scrollHeight > 8) {
692 outputFrame.style
693 ..height = '${scrollHeight}px'
694 ..visibility = ''
695 ..position = '';
696 while (outputFrame.nextNode is IFrameElement) {
697 outputFrame.nextNode.remove();
698 }
699 }
700 }
701
702 void onBadMessage(MessageEvent event) {
703 window.console
704 ..groupCollapsed('Bad message')
705 ..dir(event)
706 ..log(event.source.runtimeType)
707 ..groupEnd();
708 }
709
710 void consolePrintLine(line) {
711 if (context.shouldClearConsole) {
712 context.shouldClearConsole = false;
713 outputDiv.nodes.clear();
714 }
715 if (window.parent != window) {
716 // Test support.
717 // TODO(ahe): Use '/' instead of '*' when Firefox is upgraded to version
718 // 30 across build bots. Support for '/' was added in version 29, and we
719 // support the two most recent versions.
720 window.parent.postMessage('$line\n', '*');
721 }
722 outputDiv.appendText('$line\n');
723 }
724
725 void onCompilationFailed(String firstError) {
726 if (firstError == null) {
727 consolePrintLine('Compilation failed.');
728 } else {
729 consolePrintLine('Compilation failed: $firstError');
730 }
731 }
732
733 void onCompilationDone() {
734 context.isFirstCompile = false;
735 context.elapsedCompilationTime.stop();
736 Duration compilationDuration = context.elapsedCompilationTime.elapsed;
737 context.elapsedCompilationTime.reset();
738 print('Compilation took $compilationDuration.');
739 if (context.compilerConsole.parent != null) {
740 context.compilerConsole.remove();
741 }
742 for (AnchorElement diagnostic in context.oldDiagnostics) {
743 if (diagnostic.parent != null) {
744 // Problem fixed, remove the diagnostic.
745 diagnostic.replaceWith(new Text(getText(diagnostic)));
746 }
747 }
748 context.oldDiagnostics.clear();
749 observer.takeRecords(); // Discard mutations.
750 }
751
752 void compilationStarting() {
753 var progress = new SpanElement()
754 ..appendHtml('<i class="icon-spinner icon-spin"></i>')
755 ..appendText(' Compiling Dart program.');
756 if (settings.verboseCompiler) {
757 progress.appendText('..');
758 }
759 context.compilerConsole = new SpanElement()
760 ..append(progress)
761 ..appendText('\n');
762 context.shouldClearConsole = true;
763 context.elapsedCompilationTime
764 ..start()
765 ..reset();
766 if (context.isFirstCompile) {
767 outputDiv.append(context.compilerConsole);
768 }
769 var diagnostics = mainEditorPane.querySelectorAll('a.diagnostic');
770 context.oldDiagnostics
771 ..clear()
772 ..addAll(diagnostics);
773 }
774
775 void aboutToRun() {
776 context.shouldClearConsole = true;
777 }
778
779 void onIframeError(ErrorMessage message) {
780 // TODO(ahe): Consider replacing object URLs with something like <a
781 // href='...'>out.js</a>.
782 // TODO(ahe): Use source maps to translate stack traces.
783 consolePrintLine(message);
784 }
785
786 void verboseCompilerMessage(String message) {
787 if (settings.verboseCompiler) {
788 context.compilerConsole.appendText('$message\n');
789 } else {
790 if (isCompilerStageMarker(message)) {
791 Element progress = context.compilerConsole.firstChild;
792 progress.appendText('.');
793 }
794 }
795 }
796
797 void onCompilerCrash(data) {
798 onInternalError('Error and stack trace:\n$data');
799 }
800
801 void onInternalError(message) {
802 outputDiv
803 ..nodes.clear()
804 ..append(new HeadingElement.h1()..appendText('Internal Error'))
805 ..appendText('We would appreciate if you take a moment to report '
806 'this at ')
807 ..append(
808 new AnchorElement(href: TRY_DART_NEW_DEFECT)
809 ..target = '_blank'
810 ..appendText(TRY_DART_NEW_DEFECT))
811 ..appendText('$message');
812 if (window.parent != window) {
813 // Test support.
814 // TODO(ahe): Use '/' instead of '*' when Firefox is upgraded to version
815 // 30 across build bots. Support for '/' was added in version 29, and we
816 // support the two most recent versions.
817 window.parent.postMessage('$message\n', '*');
818 }
819 }
820 }
821
822 Future<String> getString(uri) {
823 return new Future<String>.sync(() => HttpRequest.getString('$uri'));
824 }
825
826 class PendingInputState extends InitialState {
827 PendingInputState(InteractionContext context)
828 : super(context);
829
830 void onInput(Event event) {
831 // Do nothing.
832 }
833
834 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
835 super.onMutation(mutations, observer);
836
837 InteractionState nextState = new InitialState(context);
838 if (settings.enableCodeCompletion.value) {
839 Element parent = editor.getElementAtSelection();
840 Element ui;
841 if (parent != null) {
842 ui = parent.querySelector('.dart-code-completion');
843 if (ui != null) {
844 nextState = new CodeCompletionState(context, parent, ui);
845 }
846 }
847 }
848 state = nextState;
849 }
850 }
851
852 class CodeCompletionState extends InitialState {
853 final Element activeCompletion;
854 final Element ui;
855 int minWidth = 0;
856 DivElement staticResults;
857 SpanElement inline;
858 DivElement serverResults;
859 String inlineSuggestion;
860
861 CodeCompletionState(InteractionContext context,
862 this.activeCompletion,
863 this.ui)
864 : super(context);
865
866 void onInput(Event event) {
867 // Do nothing.
868 }
869
870 void onModifiedKeyUp(KeyboardEvent event) {
871 // TODO(ahe): Handle DOWN (jump to server results).
872 }
873
874 void onUnmodifiedKeyUp(KeyboardEvent event) {
875 switch (event.keyCode) {
876 case KeyCode.DOWN:
877 return moveDown(event);
878
879 case KeyCode.UP:
880 return moveUp(event);
881
882 case KeyCode.ESC:
883 event.preventDefault();
884 return endCompletion();
885
886 case KeyCode.TAB:
887 case KeyCode.RIGHT:
888 case KeyCode.ENTER:
889 event.preventDefault();
890 return endCompletion(acceptSuggestion: true);
891
892 case KeyCode.SPACE:
893 return endCompletion();
894 }
895 }
896
897 void moveDown(Event event) {
898 event.preventDefault();
899 move(1);
900 }
901
902 void moveUp(Event event) {
903 event.preventDefault();
904 move(-1);
905 }
906
907 void move(int direction) {
908 Element element = editor.moveActive(direction, ui);
909 if (element == null) return;
910 var text = activeCompletion.firstChild;
911 String prefix = "";
912 if (text is Text) prefix = text.data.trim();
913 updateInlineSuggestion(prefix, element.text);
914 }
915
916 void endCompletion({bool acceptSuggestion: false}) {
917 if (acceptSuggestion) {
918 suggestionAccepted();
919 }
920 activeCompletion.classes.remove('active');
921 mainEditorPane.querySelectorAll('.hazed-suggestion')
922 .forEach((e) => e.remove());
923 // The above changes create mutation records. This implicitly fire mutation
924 // events that result in saving the source code in local storage.
925 // TODO(ahe): Consider making this more explicit.
926 state = new InitialState(context);
927 }
928
929 void suggestionAccepted() {
930 if (inlineSuggestion != null) {
931 Text text = new Text(inlineSuggestion);
932 activeCompletion.replaceWith(text);
933 window.getSelection().collapse(text, inlineSuggestion.length);
934 }
935 }
936
937 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
938 for (MutationRecord record in mutations) {
939 if (!activeCompletion.contains(record.target)) {
940 endCompletion();
941 return super.onMutation(mutations, observer);
942 }
943 }
944
945 var text = activeCompletion.firstChild;
946 if (text is! Text) return endCompletion();
947 updateSuggestions(text.data.trim());
948 }
949
950 void onStateChanged(InteractionState previous) {
951 super.onStateChanged(previous);
952 displayCodeCompletion();
953 }
954
955 void displayCodeCompletion() {
956 Selection selection = window.getSelection();
957 if (selection.anchorNode is! Text) {
958 return endCompletion();
959 }
960 Text text = selection.anchorNode;
961 if (!activeCompletion.contains(text)) {
962 return endCompletion();
963 }
964
965 int anchorOffset = selection.anchorOffset;
966
967 String prefix = text.data.substring(0, anchorOffset).trim();
968 if (prefix.isEmpty) {
969 return endCompletion();
970 }
971
972 num height = activeCompletion.getBoundingClientRect().height;
973 activeCompletion.classes.add('active');
974 Node root = getShadowRoot(ui);
975
976 inline = new SpanElement()
977 ..classes.add('hazed-suggestion');
978 Text rest = text.splitText(anchorOffset);
979 text.parentNode.insertBefore(inline, text.nextNode);
980 activeCompletion.parentNode.insertBefore(
981 rest, activeCompletion.nextNode);
982
983 staticResults = new DivElement()
984 ..classes.addAll(['dart-static', 'dart-limited-height']);
985 serverResults = new DivElement()
986 ..style.display = 'none'
987 ..classes.add('dart-server');
988 root.nodes.addAll([staticResults, serverResults]);
989 ui.style.top = '${height}px';
990
991 staticResults.nodes.add(buildCompletionEntry(prefix));
992
993 updateSuggestions(prefix);
994 }
995
996 void updateInlineSuggestion(String prefix, String suggestion) {
997 inlineSuggestion = suggestion;
998
999 minWidth = max(minWidth, activeCompletion.getBoundingClientRect().width);
1000
1001 activeCompletion.style
1002 ..display = 'inline-block'
1003 ..minWidth = '${minWidth}px';
1004
1005 setShadowRoot(inline, suggestion.substring(prefix.length));
1006 inline.style.display = '';
1007
1008 observer.takeRecords(); // Discard mutations.
1009 }
1010
1011 void updateSuggestions(String prefix) {
1012 if (prefix.isEmpty) {
1013 return endCompletion();
1014 }
1015
1016 Token first = tokenize(prefix);
1017 for (Token token = first; token.kind != EOF_TOKEN; token = token.next) {
1018 String tokenInfo = token.info.value;
1019 if (token != first ||
1020 tokenInfo != 'identifier' &&
1021 tokenInfo != 'keyword') {
1022 return endCompletion();
1023 }
1024 }
1025
1026 var borderHeight = 2; // 1 pixel border top & bottom.
1027 num height = ui.getBoundingClientRect().height - borderHeight;
1028 ui.style.minHeight = '${height}px';
1029
1030 minWidth =
1031 max(minWidth, activeCompletion.getBoundingClientRect().width);
1032
1033 staticResults.nodes.clear();
1034 serverResults.nodes.clear();
1035
1036 if (inlineSuggestion != null && inlineSuggestion.startsWith(prefix)) {
1037 setShadowRoot(inline, inlineSuggestion.substring(prefix.length));
1038 }
1039
1040 List<String> results = editor.seenIdentifiers.where(
1041 (String identifier) {
1042 return identifier != prefix && identifier.startsWith(prefix);
1043 }).toList(growable: false);
1044 results.sort();
1045 if (results.isEmpty) results = <String>[prefix];
1046
1047 results.forEach((String completion) {
1048 staticResults.nodes.add(buildCompletionEntry(completion));
1049 });
1050
1051 if (settings.enableDartMind) {
1052 // TODO(ahe): Move this code to its own function or class.
1053 String encodedArg0 = Uri.encodeComponent('"$prefix"');
1054 String mindQuery =
1055 'http://dart-mind.appspot.com/rpc'
1056 '?action=GetExportingPubCompletions'
1057 '&arg0=$encodedArg0';
1058 try {
1059 var serverWatch = new Stopwatch()..start();
1060 HttpRequest.getString(mindQuery).then((String responseText) {
1061 serverWatch.stop();
1062 List<String> serverSuggestions = JSON.decode(responseText);
1063 if (!serverSuggestions.isEmpty) {
1064 updateInlineSuggestion(prefix, serverSuggestions.first);
1065 }
1066 var root = getShadowRoot(ui);
1067 for (int i = 1; i < serverSuggestions.length; i++) {
1068 String completion = serverSuggestions[i];
1069 DivElement where = staticResults;
1070 int index = results.indexOf(completion);
1071 if (index != -1) {
1072 List<Element> entries = root.querySelectorAll(
1073 '.dart-static>.dart-entry');
1074 entries[index].classes.add('doubleplusgood');
1075 } else {
1076 if (results.length > 3) {
1077 serverResults.style.display = 'block';
1078 where = serverResults;
1079 }
1080 Element entry = buildCompletionEntry(completion);
1081 entry.classes.add('doubleplusgood');
1082 where.nodes.add(entry);
1083 }
1084 }
1085 serverResults.appendHtml(
1086 '<div>${serverWatch.elapsedMilliseconds}ms</div>');
1087 // Discard mutations.
1088 observer.takeRecords();
1089 }).catchError((error, stack) {
1090 window.console.dir(error);
1091 window.console.error('$stack');
1092 });
1093 } catch (error, stack) {
1094 window.console.dir(error);
1095 window.console.error('$stack');
1096 }
1097 }
1098 // Discard mutations.
1099 observer.takeRecords();
1100 }
1101
1102 Element buildCompletionEntry(String completion) {
1103 return new DivElement()
1104 ..classes.add('dart-entry')
1105 ..appendText(completion);
1106 }
1107
1108 void transitionToInitialState() {
1109 endCompletion();
1110 }
1111 }
1112
1113 Token tokenize(String text) {
1114 var file = new StringSourceFile.fromName('', text);
1115 return new StringScanner(file, includeComments: true).tokenize();
1116 }
1117
1118 bool computeHasModifier(KeyboardEvent event) {
1119 return
1120 event.getModifierState("Alt") ||
1121 event.getModifierState("AltGraph") ||
1122 event.getModifierState("CapsLock") ||
1123 event.getModifierState("Control") ||
1124 event.getModifierState("Fn") ||
1125 event.getModifierState("Meta") ||
1126 event.getModifierState("NumLock") ||
1127 event.getModifierState("ScrollLock") ||
1128 event.getModifierState("Scroll") ||
1129 event.getModifierState("Win") ||
1130 event.getModifierState("Shift") ||
1131 event.getModifierState("SymbolLock") ||
1132 event.getModifierState("OS");
1133 }
1134
1135 String tokenizeAndHighlight(String line,
1136 String state,
1137 int start,
1138 TrySelection trySelection,
1139 List<Node> nodes) {
1140 String newState = '';
1141 int offset = state.length;
1142 int adjustedStart = start - state.length;
1143
1144 // + offset + charOffset + globalOffset + (charOffset + charCount)
1145 // v v v v
1146 // do identifier_abcdefghijklmnopqrst
1147 for (Token token = tokenize('$state$line');
1148 token.kind != EOF_TOKEN;
1149 token = token.next) {
1150 int charOffset = token.charOffset;
1151 int charCount = token.charCount;
1152
1153 Token tokenToDecorate = token;
1154 if (token is UnterminatedToken && isUnterminatedMultiLineToken(token)) {
1155 newState += '${token.start}';
1156 continue; // This might not be an error.
1157 } else {
1158 Token follow = token.next;
1159 if (token is BeginGroupToken && token.endGroup != null) {
1160 follow = token.endGroup.next;
1161 }
1162 if (token.kind == STRING_TOKEN) {
1163 follow = followString(follow);
1164 if (follow is UnmatchedToken) {
1165 if ('${follow.begin.value}' == r'${') {
1166 newState += '${extractQuote(token.value)}';
1167 }
1168 }
1169 }
1170 if (follow is ErrorToken && follow.charOffset == token.charOffset) {
1171 if (follow is UnmatchedToken) {
1172 newState += '${follow.begin.value}';
1173 } else {
1174 tokenToDecorate = follow;
1175 }
1176 }
1177 }
1178
1179 if (charOffset < offset) {
1180 // Happens for scanner errors, or for the [state] prefix.
1181 continue;
1182 }
1183
1184 Decoration decoration;
1185 if (charOffset - state.length == line.length - 1 && line.endsWith('\n')) {
1186 // Don't add decorations to trailing newline.
1187 decoration = null;
1188 } else {
1189 decoration = editor.getDecoration(tokenToDecorate);
1190 }
1191
1192 if (decoration == null) continue;
1193
1194 // Add a node for text before current token.
1195 trySelection.addNodeFromSubstring(
1196 adjustedStart + offset, adjustedStart + charOffset, nodes);
1197
1198 // Add a node for current token.
1199 trySelection.addNodeFromSubstring(
1200 adjustedStart + charOffset,
1201 adjustedStart + charOffset + charCount, nodes, decoration);
1202
1203 offset = charOffset + charCount;
1204 }
1205
1206 // Add a node for anything after the last (decorated) token.
1207 trySelection.addNodeFromSubstring(
1208 adjustedStart + offset, start + line.length, nodes);
1209
1210 return newState;
1211 }
1212
1213 bool isUnterminatedMultiLineToken(UnterminatedToken token) {
1214 return
1215 token.start == '/*' ||
1216 token.start == "'''" ||
1217 token.start == '"""' ||
1218 token.start == "r'''" ||
1219 token.start == 'r"""';
1220 }
1221
1222 void normalizeMutationRecord(MutationRecord record,
1223 TrySelection selection,
1224 Set<Node> normalizedNodes) {
1225 for (Node node in record.addedNodes) {
1226 if (node.parentNode == null) continue;
1227 normalizedNodes.add(findLine(node));
1228 if (node is Text) continue;
1229 StringBuffer buffer = new StringBuffer();
1230 int selectionOffset = htmlToText(node, buffer, selection);
1231 Text newNode = new Text('$buffer');
1232 node.replaceWith(newNode);
1233 if (selectionOffset != -1) {
1234 selection.anchorNode = newNode;
1235 selection.anchorOffset = selectionOffset;
1236 }
1237 }
1238 if (!record.removedNodes.isEmpty) {
1239 var first = record.removedNodes.first;
1240 var line = findLine(record.target);
1241
1242 if (first is Text && line.nextNode != null) {
1243 normalizedNodes.add(line.nextNode);
1244 }
1245 normalizedNodes.add(line);
1246 }
1247 if (record.type == "characterData" && record.target.parentNode != null) {
1248 // At least Firefox sends a "characterData" record whose target is the
1249 // deleted text node. It also sends a record where "removedNodes" isn't
1250 // empty whose target is the parent (which we are interested in).
1251 normalizedNodes.add(findLine(record.target));
1252 }
1253 }
1254
1255 // Finds the line of [node] (a parent node with CSS class 'lineNumber').
1256 // If no such parent exists, return mainEditorPane if it is a parent.
1257 // Otherwise return [node].
1258 Node findLine(Node node) {
1259 for (Node n = node; n != null; n = n.parentNode) {
1260 if (n is Element && n.classes.contains('lineNumber')) return n;
1261 if (n == mainEditorPane) return n;
1262 }
1263 return node;
1264 }
1265
1266 Element makeLine(List<Node> lineNodes, String state) {
1267 return new SpanElement()
1268 ..setAttribute('dart-state', state)
1269 ..nodes.addAll(lineNodes)
1270 ..classes.add('lineNumber');
1271 }
1272
1273 bool isAtEndOfFile(Text text, int offset) {
1274 Node line = findLine(text);
1275 return
1276 line.nextNode == null &&
1277 text.parentNode.nextNode == null &&
1278 offset == text.length;
1279 }
1280
1281 List<String> splitLines(String text) {
1282 return text.split(new RegExp('^', multiLine: true));
1283 }
1284
1285 void removeCodeCompletion() {
1286 List<Node> highlighting =
1287 mainEditorPane.querySelectorAll('.dart-code-completion');
1288 for (Element element in highlighting) {
1289 element.remove();
1290 }
1291 }
1292
1293 bool isCompilerStageMarker(String message) {
1294 return
1295 message.startsWith('Package root is ') ||
1296 message.startsWith('Compiling ') ||
1297 message == "Resolving..." ||
1298 message.startsWith('Resolved ') ||
1299 message == "Inferring types..." ||
1300 message == "Compiling..." ||
1301 message.startsWith('Compiled ');
1302 }
1303
1304 void workAroundFirefoxBug() {
1305 Selection selection = window.getSelection();
1306 if (!isCollapsed(selection)) return;
1307 Node node = selection.anchorNode;
1308 int offset = selection.anchorOffset;
1309 if (node is Element && offset != 0) {
1310 // In some cases, Firefox reports the wrong anchorOffset (always seems to
1311 // be 6) when anchorNode is an Element. Moving the cursor back and forth
1312 // adjusts the anchorOffset.
1313 // Safari can also reach this code, but the offset isn't wrong, just
1314 // inconsistent. After moving the cursor back and forth, Safari will make
1315 // the offset relative to a text node.
1316 if (settings.hasSelectionModify.value) {
1317 // IE doesn't support selection.modify, but it's okay since the code
1318 // above is for Firefox, IE doesn't have problems with anchorOffset.
1319 selection
1320 ..modify('move', 'backward', 'character')
1321 ..modify('move', 'forward', 'character');
1322 print('Selection adjusted $node@$offset -> '
1323 '${selection.anchorNode}@${selection.anchorOffset}.');
1324 }
1325 }
1326 }
1327
1328 /// Compute the token following a string. Compare to parseSingleLiteralString
1329 /// in parser.dart.
1330 Token followString(Token token) {
1331 // TODO(ahe): I should be able to get rid of this if I change the scanner to
1332 // create BeginGroupToken for strings.
1333 int kind = token.kind;
1334 while (kind != EOF_TOKEN) {
1335 if (kind == STRING_INTERPOLATION_TOKEN) {
1336 // Looking at ${expression}.
1337 BeginGroupToken begin = token;
1338 token = begin.endGroup.next;
1339 } else if (kind == STRING_INTERPOLATION_IDENTIFIER_TOKEN) {
1340 // Looking at $identifier.
1341 token = token.next.next;
1342 } else {
1343 return token;
1344 }
1345 kind = token.kind;
1346 if (kind != STRING_TOKEN) return token;
1347 token = token.next;
1348 kind = token.kind;
1349 }
1350 return token;
1351 }
1352
1353 String extractQuote(String string) {
1354 StringQuoting q = StringValidator.quotingFromString(string);
1355 return (q.raw ? 'r' : '') + (q.quoteChar * q.leftQuoteLength);
1356 }
OLDNEW
« dart.gyp ('K') | « site/try/src/iframe_error_handler.dart ('k') | site/try/src/leap.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698