OLD | NEW |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library trydart.interaction_manager; | 5 library trydart.interaction_manager; |
6 | 6 |
7 import 'dart:html'; | 7 import 'dart:html'; |
8 | 8 |
9 import 'dart:convert' show | 9 import 'dart:convert' show |
10 JSON; | 10 JSON; |
(...skipping 218 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
229 : super.internal() { | 229 : super.internal() { |
230 state = new InitialState(this); | 230 state = new InitialState(this); |
231 heartbeat = new Timer.periodic(HEARTBEAT_INTERVAL, onHeartbeat); | 231 heartbeat = new Timer.periodic(HEARTBEAT_INTERVAL, onHeartbeat); |
232 } | 232 } |
233 | 233 |
234 void onInput(Event event) => state.onInput(event); | 234 void onInput(Event event) => state.onInput(event); |
235 | 235 |
236 void onKeyUp(KeyboardEvent event) => state.onKeyUp(event); | 236 void onKeyUp(KeyboardEvent event) => state.onKeyUp(event); |
237 | 237 |
238 void onMutation(List<MutationRecord> mutations, MutationObserver observer) { | 238 void onMutation(List<MutationRecord> mutations, MutationObserver observer) { |
| 239 workAroundFirefoxBug(); |
239 try { | 240 try { |
240 try { | 241 try { |
241 return state.onMutation(mutations, observer); | 242 return state.onMutation(mutations, observer); |
242 } finally { | 243 } finally { |
243 // Discard any mutations during the observer, as these can lead to | 244 // Discard any mutations during the observer, as these can lead to |
244 // infinite loop. | 245 // infinite loop. |
245 observer.takeRecords(); | 246 observer.takeRecords(); |
246 } | 247 } |
247 } catch (error, stackTrace) { | 248 } catch (error, stackTrace) { |
248 try { | 249 try { |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
301 InteractionContext get context; | 302 InteractionContext get context; |
302 | 303 |
303 // TODO(ahe): Remove this. | 304 // TODO(ahe): Remove this. |
304 Set<AnchorElement> get oldDiagnostics { | 305 Set<AnchorElement> get oldDiagnostics { |
305 throw 'Use context.oldDiagnostics instead'; | 306 throw 'Use context.oldDiagnostics instead'; |
306 } | 307 } |
307 | 308 |
308 void set state(InteractionState newState); | 309 void set state(InteractionState newState); |
309 | 310 |
310 void onStateChanged(InteractionState previous) { | 311 void onStateChanged(InteractionState previous) { |
311 print('State change ${previous.runtimeType} -> ${runtimeType}.'); | |
312 } | 312 } |
313 | 313 |
314 void transitionToInitialState() { | 314 void transitionToInitialState() { |
315 state = new InitialState(context); | 315 state = new InitialState(context); |
316 } | 316 } |
317 } | 317 } |
318 | 318 |
319 class InitialState extends InteractionState { | 319 class InitialState extends InteractionState { |
320 final InteractionContext context; | 320 final InteractionContext context; |
321 bool requestCodeCompletion = false; | 321 bool requestCodeCompletion = false; |
322 | 322 |
323 InitialState(this.context); | 323 InitialState(this.context); |
324 | 324 |
325 void set state(InteractionState state) { | 325 void set state(InteractionState state) { |
326 InteractionState previous = context.state; | 326 InteractionState previous = context.state; |
327 if (previous != state) { | 327 if (previous != state) { |
328 context.state = state; | 328 context.state = state; |
329 state.onStateChanged(previous); | 329 state.onStateChanged(previous); |
330 } | 330 } |
331 } | 331 } |
332 | 332 |
333 void onInput(Event event) { | 333 void onInput(Event event) { |
334 state = new PendingInputState(context); | 334 state = new PendingInputState(context); |
335 } | 335 } |
336 | 336 |
337 void onKeyUp(KeyboardEvent event) { | 337 void onKeyUp(KeyboardEvent event) { |
338 if (computeHasModifier(event)) { | 338 if (computeHasModifier(event)) { |
339 print('onKeyUp (modified)'); | |
340 onModifiedKeyUp(event); | 339 onModifiedKeyUp(event); |
341 } else { | 340 } else { |
342 print('onKeyUp (unmodified)'); | |
343 onUnmodifiedKeyUp(event); | 341 onUnmodifiedKeyUp(event); |
344 } | 342 } |
345 } | 343 } |
346 | 344 |
347 void onModifiedKeyUp(KeyboardEvent event) { | 345 void onModifiedKeyUp(KeyboardEvent event) { |
348 if (event.getModifierState("Shift")) return onShiftedKeyUp(event); | 346 if (event.getModifierState("Shift")) return onShiftedKeyUp(event); |
349 switch (event.keyCode) { | 347 switch (event.keyCode) { |
350 case KeyCode.S: | 348 case KeyCode.S: |
351 // Disable Ctrl-S, Cmd-S, etc. We have observed users hitting these | 349 // Disable Ctrl-S, Cmd-S, etc. We have observed users hitting these |
352 // keys often when using Try Dart and getting frustrated. | 350 // keys often when using Try Dart and getting frustrated. |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
406 | 404 |
407 // This is a hack to get Safari (iOS) to send mutation events on | 405 // This is a hack to get Safari (iOS) to send mutation events on |
408 // contenteditable. | 406 // contenteditable. |
409 // TODO(ahe): Move to onInput? | 407 // TODO(ahe): Move to onInput? |
410 var newDiv = new DivElement(); | 408 var newDiv = new DivElement(); |
411 hackDiv.replaceWith(newDiv); | 409 hackDiv.replaceWith(newDiv); |
412 hackDiv = newDiv; | 410 hackDiv = newDiv; |
413 } | 411 } |
414 | 412 |
415 void onMutation(List<MutationRecord> mutations, MutationObserver observer) { | 413 void onMutation(List<MutationRecord> mutations, MutationObserver observer) { |
416 print('onMutation'); | |
417 | |
418 removeCodeCompletion(); | 414 removeCodeCompletion(); |
419 | 415 |
420 Selection selection = window.getSelection(); | 416 Selection selection = window.getSelection(); |
421 TrySelection trySelection = new TrySelection(mainEditorPane, selection); | 417 TrySelection trySelection = new TrySelection(mainEditorPane, selection); |
422 | 418 |
423 Set<Node> normalizedNodes = new Set<Node>(); | 419 Set<Node> normalizedNodes = new Set<Node>(); |
424 for (MutationRecord record in mutations) { | 420 for (MutationRecord record in mutations) { |
425 normalizeMutationRecord(record, trySelection, normalizedNodes); | 421 normalizeMutationRecord(record, trySelection, normalizedNodes); |
426 } | 422 } |
427 | 423 |
(...skipping 21 matching lines...) Expand all Loading... |
449 for (String line in splitLines(currentText)) { | 445 for (String line in splitLines(currentText)) { |
450 List<Node> lineNodes = <Node>[]; | 446 List<Node> lineNodes = <Node>[]; |
451 state = tokenizeAndHighlight( | 447 state = tokenizeAndHighlight( |
452 line, state, offset, trySelection, lineNodes); | 448 line, state, offset, trySelection, lineNodes); |
453 offset += line.length; | 449 offset += line.length; |
454 nodes.add(makeLine(lineNodes, state)); | 450 nodes.add(makeLine(lineNodes, state)); |
455 } | 451 } |
456 | 452 |
457 node.parent.insertAllBefore(nodes, node); | 453 node.parent.insertAllBefore(nodes, node); |
458 node.remove(); | 454 node.remove(); |
459 trySelection.adjust(selection); | 455 if (mainEditorPane.contains(trySelection.anchorNode)) { |
| 456 // Sometimes the anchor node is removed by the above call. This has |
| 457 // only been observed in Firefox, and is hard to reproduce. |
| 458 trySelection.adjust(selection); |
| 459 } |
460 | 460 |
461 // TODO(ahe): We know almost exactly what has changed. It could be | 461 // TODO(ahe): We know almost exactly what has changed. It could be |
462 // more efficient to only communicate what changed. | 462 // more efficient to only communicate what changed. |
463 context.currentCompilationUnit.content = getText(mainEditorPane); | 463 context.currentCompilationUnit.content = getText(mainEditorPane); |
464 | 464 |
465 // Discard highlighting mutations. | 465 // Discard highlighting mutations. |
466 observer.takeRecords(); | 466 observer.takeRecords(); |
467 return; | 467 return; |
468 } | 468 } |
469 } | 469 } |
(...skipping 729 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1199 } | 1199 } |
1200 if (!record.removedNodes.isEmpty) { | 1200 if (!record.removedNodes.isEmpty) { |
1201 var first = record.removedNodes.first; | 1201 var first = record.removedNodes.first; |
1202 var line = findLine(record.target); | 1202 var line = findLine(record.target); |
1203 | 1203 |
1204 if (first is Text && first.data=="\n" && line.nextNode != null) { | 1204 if (first is Text && first.data=="\n" && line.nextNode != null) { |
1205 normalizedNodes.add(line.nextNode); | 1205 normalizedNodes.add(line.nextNode); |
1206 } | 1206 } |
1207 normalizedNodes.add(line); | 1207 normalizedNodes.add(line); |
1208 } | 1208 } |
1209 if (record.type == "characterData") { | 1209 if (record.type == "characterData" && record.target.parent != null) { |
| 1210 // At least Firefox sends a "characterData" record whose target is the |
| 1211 // deleted text node. It also sends a record where "removedNodes" isn't |
| 1212 // empty whose target is the parent (which we are interested in). |
1210 normalizedNodes.add(findLine(record.target)); | 1213 normalizedNodes.add(findLine(record.target)); |
1211 } | 1214 } |
1212 } | 1215 } |
1213 | 1216 |
1214 // Finds the line of [node] (a parent node with CSS class 'lineNumber'). | 1217 // Finds the line of [node] (a parent node with CSS class 'lineNumber'). |
1215 // If no such parent exists, return mainEditorPane if it is a parent. | 1218 // If no such parent exists, return mainEditorPane if it is a parent. |
1216 // Otherwise return [node]. | 1219 // Otherwise return [node]. |
1217 Node findLine(Node node) { | 1220 Node findLine(Node node) { |
1218 for (Node n = node; n != null; n = n.parent) { | 1221 for (Node n = node; n != null; n = n.parent) { |
1219 if (n is Element && n.classes.contains('lineNumber')) return n; | 1222 if (n is Element && n.classes.contains('lineNumber')) return n; |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1252 bool isCompilerStageMarker(String message) { | 1255 bool isCompilerStageMarker(String message) { |
1253 return | 1256 return |
1254 message.startsWith('Package root is ') || | 1257 message.startsWith('Package root is ') || |
1255 message.startsWith('Compiling ') || | 1258 message.startsWith('Compiling ') || |
1256 message == "Resolving..." || | 1259 message == "Resolving..." || |
1257 message.startsWith('Resolved ') || | 1260 message.startsWith('Resolved ') || |
1258 message == "Inferring types..." || | 1261 message == "Inferring types..." || |
1259 message == "Compiling..." || | 1262 message == "Compiling..." || |
1260 message.startsWith('Compiled '); | 1263 message.startsWith('Compiled '); |
1261 } | 1264 } |
| 1265 |
| 1266 void workAroundFirefoxBug() { |
| 1267 Selection selection = window.getSelection(); |
| 1268 if (!isCollapsed(selection)) return; |
| 1269 Node node = selection.anchorNode; |
| 1270 int offset = selection.anchorOffset; |
| 1271 if (selection.anchorNode is Element && selection.anchorOffset != 0) { |
| 1272 // In some cases, Firefox reports the wrong anchorOffset (always seems to |
| 1273 // be 6) when anchorNode is an Element. Moving the cursor back and forth |
| 1274 // adjusts the anchorOffset. |
| 1275 // Safari can also reach this code, but the offset isn't wrong, just |
| 1276 // inconsistent. After moving the cursor back and forth, Safari will make |
| 1277 // the offset relative to a text node. |
| 1278 selection |
| 1279 ..modify('move', 'backward', 'character') |
| 1280 ..modify('move', 'forward', 'character'); |
| 1281 print('Selection adjusted $node@$offset -> ' |
| 1282 '${selection.anchorNode}@${selection.anchorOffset}.'); |
| 1283 } |
| 1284 } |
OLD | NEW |