| 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 |