OLD | NEW |
| (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 } | |
OLD | NEW |