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 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
85 | 85 |
86 // Implementation note: The state machine is actually implemented by | 86 // Implementation note: The state machine is actually implemented by |
87 // [InteractionContext], this class represents public event handlers. | 87 // [InteractionContext], this class represents public event handlers. |
88 | 88 |
89 factory InteractionManager() => new InteractionContext(); | 89 factory InteractionManager() => new InteractionContext(); |
90 | 90 |
91 InteractionManager.internal(); | 91 InteractionManager.internal(); |
92 | 92 |
93 void onInput(Event event); | 93 void onInput(Event event); |
94 | 94 |
| 95 // TODO(ahe): Rename to onKeyDown (as it is called in response to keydown |
| 96 // event). |
95 void onKeyUp(KeyboardEvent event); | 97 void onKeyUp(KeyboardEvent event); |
96 | 98 |
97 void onMutation(List<MutationRecord> mutations, MutationObserver observer); | 99 void onMutation(List<MutationRecord> mutations, MutationObserver observer); |
98 | 100 |
99 void onSelectionChange(Event event); | 101 void onSelectionChange(Event event); |
100 | 102 |
101 /// Called when the content of a CompilationUnit changed. | 103 /// Called when the content of a CompilationUnit changed. |
102 void onCompilationUnitChanged(CompilationUnit unit); | 104 void onCompilationUnitChanged(CompilationUnit unit); |
103 | 105 |
104 Future<List<String>> projectFileNames(); | 106 Future<List<String>> projectFileNames(); |
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
221 onUnmodifiedKeyUp(event); | 223 onUnmodifiedKeyUp(event); |
222 } | 224 } |
223 } | 225 } |
224 | 226 |
225 void onModifiedKeyUp(KeyboardEvent event) { | 227 void onModifiedKeyUp(KeyboardEvent event) { |
226 } | 228 } |
227 | 229 |
228 void onUnmodifiedKeyUp(KeyboardEvent event) { | 230 void onUnmodifiedKeyUp(KeyboardEvent event) { |
229 switch (event.keyCode) { | 231 switch (event.keyCode) { |
230 case KeyCode.ENTER: { | 232 case KeyCode.ENTER: { |
231 event.preventDefault(); | |
232 Selection selection = window.getSelection(); | 233 Selection selection = window.getSelection(); |
233 if (isCollapsed(selection) && selection.anchorNode is Text) { | 234 if (isCollapsed(selection)) { |
234 Text text = selection.anchorNode; | 235 event.preventDefault(); |
235 int offset = selection.anchorOffset; | 236 Node node = selection.anchorNode; |
236 text.insertData(offset, '\n'); | 237 if (node is Text) { |
237 selection.collapse(text, offset + 1); | 238 Text text = node; |
| 239 int offset = selection.anchorOffset; |
| 240 // If at end-of-file, insert an extra newline. The the extra |
| 241 // newline ensures that the next line isn't empty. At least Chrome |
| 242 // behaves as if "\n" is just a single line. "\nc" (where c is any |
| 243 // character) is two lines, according to Chrome. |
| 244 String newline = isAtEndOfFile(text, offset) ? '\n\n' : '\n'; |
| 245 text.insertData(offset, newline); |
| 246 selection.collapse(text, offset + 1); |
| 247 } else if (node is Element) { |
| 248 node.appendText('\n\n'); |
| 249 selection.collapse(node.firstChild, 1); |
| 250 } else { |
| 251 window.console |
| 252 ..error('Unexpected node') |
| 253 ..dir(node); |
| 254 } |
238 } | 255 } |
239 break; | 256 break; |
240 } | 257 } |
241 } | 258 } |
242 | 259 |
243 // editor.scheduleRemoveCodeCompletion(); | 260 // editor.scheduleRemoveCodeCompletion(); |
244 | 261 |
245 // This is a hack to get Safari (iOS) to send mutation events on | 262 // This is a hack to get Safari (iOS) to send mutation events on |
246 // contenteditable. | 263 // contenteditable. |
247 // TODO(ahe): Move to onInput? | 264 // TODO(ahe): Move to onInput? |
248 var newDiv = new DivElement(); | 265 var newDiv = new DivElement(); |
249 hackDiv.replaceWith(newDiv); | 266 hackDiv.replaceWith(newDiv); |
250 hackDiv = newDiv; | 267 hackDiv = newDiv; |
251 } | 268 } |
252 | 269 |
253 void onMutation(List<MutationRecord> mutations, MutationObserver observer) { | 270 void onMutation(List<MutationRecord> mutations, MutationObserver observer) { |
254 print('onMutation'); | 271 print('onMutation'); |
255 | 272 |
256 List<Node> highlighting = mainEditorPane.querySelectorAll( | 273 List<Node> highlighting = mainEditorPane.querySelectorAll( |
257 'a.diagnostic>span, .dart-code-completion, .hazed-suggestion'); | 274 'a.diagnostic>span, .dart-code-completion, .hazed-suggestion'); |
258 for (Element element in highlighting) { | 275 for (Element element in highlighting) { |
259 element.remove(); | 276 element.remove(); |
260 } | 277 } |
261 | 278 |
262 Selection selection = window.getSelection(); | 279 Selection selection = window.getSelection(); |
263 TrySelection trySelection = new TrySelection(mainEditorPane, selection); | 280 TrySelection trySelection = new TrySelection(mainEditorPane, selection); |
264 | 281 |
| 282 Set<Node> normalizedNodes = new Set<Node>(); |
265 for (MutationRecord record in mutations) { | 283 for (MutationRecord record in mutations) { |
266 normalizeMutationRecord(record, trySelection); | 284 normalizeMutationRecord(record, trySelection, normalizedNodes); |
| 285 } |
| 286 |
| 287 if (normalizedNodes.length == 1) { |
| 288 Node node = normalizedNodes.single; |
| 289 if (node is Element && node.classes.contains('lineNumber')) { |
| 290 print('Single line change: ${node.outerHtml}'); |
| 291 |
| 292 String currentText = node.text; |
| 293 |
| 294 trySelection = new TrySelection(node, selection); |
| 295 trySelection.updateText(currentText); |
| 296 |
| 297 editor.isMalformedInput = false; |
| 298 int offset = 0; |
| 299 List<Node> nodes = <Node>[]; |
| 300 |
| 301 String state = ''; |
| 302 Element previousLine = node.previousElementSibling; |
| 303 if (previousLine != null) { |
| 304 state = previousLine.getAttribute('dart-state'); |
| 305 } |
| 306 for (String line in splitLines(currentText)) { |
| 307 List<Node> lineNodes = <Node>[]; |
| 308 state = tokenizeAndHighlight( |
| 309 line, state, offset, trySelection, lineNodes); |
| 310 offset += line.length; |
| 311 nodes.add(makeLine(lineNodes, state)); |
| 312 } |
| 313 |
| 314 node.parent.insertAllBefore(nodes, node); |
| 315 node.remove(); |
| 316 trySelection.adjust(selection); |
| 317 |
| 318 // Discard highlighting mutations. |
| 319 observer.takeRecords(); |
| 320 return; |
| 321 } |
267 } | 322 } |
268 | 323 |
269 String currentText = mainEditorPane.text; | 324 String currentText = mainEditorPane.text; |
270 trySelection.updateText(currentText); | 325 trySelection.updateText(currentText); |
271 | 326 |
272 context.currentCompilationUnit.content = currentText; | 327 context.currentCompilationUnit.content = currentText; |
273 | 328 |
274 editor.seenIdentifiers = new Set<String>.from(mock.identifiers); | 329 editor.seenIdentifiers = new Set<String>.from(mock.identifiers); |
275 | 330 |
276 editor.isMalformedInput = false; | 331 editor.isMalformedInput = false; |
277 int offset = 0; | 332 int offset = 0; |
278 List<Node> nodes = <Node>[]; | 333 List<Node> nodes = <Node>[]; |
279 | 334 |
280 String state = ''; | 335 String state = ''; |
281 for (String line in currentText.split(new RegExp('^', multiLine: true))) { | 336 for (String line in splitLines(currentText)) { |
282 List<Node> lineNodes = <Node>[]; | 337 List<Node> lineNodes = <Node>[]; |
283 state = | 338 state = |
284 tokenizeAndHighlight(line, state, offset, trySelection, lineNodes); | 339 tokenizeAndHighlight(line, state, offset, trySelection, lineNodes); |
285 offset += line.length; | 340 offset += line.length; |
286 nodes.add(new SpanElement() | 341 nodes.add(makeLine(lineNodes, state)); |
287 ..nodes.addAll(lineNodes) | |
288 ..classes.add('lineNumber')); | |
289 } | 342 } |
290 | 343 |
291 mainEditorPane | 344 mainEditorPane |
292 ..nodes.clear() | 345 ..nodes.clear() |
293 ..nodes.addAll(nodes); | 346 ..nodes.addAll(nodes); |
294 trySelection.adjust(selection); | 347 trySelection.adjust(selection); |
295 | 348 |
296 // Discard highlighting mutations. | 349 // Discard highlighting mutations. |
297 observer.takeRecords(); | 350 observer.takeRecords(); |
298 } | 351 } |
(...skipping 479 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
778 | 831 |
779 bool isUnterminatedMultiLineToken(UnterminatedToken token) { | 832 bool isUnterminatedMultiLineToken(UnterminatedToken token) { |
780 return | 833 return |
781 token.start == '/*' || | 834 token.start == '/*' || |
782 token.start == "'''" || | 835 token.start == "'''" || |
783 token.start == '"""' || | 836 token.start == '"""' || |
784 token.start == "r'''" || | 837 token.start == "r'''" || |
785 token.start == 'r"""'; | 838 token.start == 'r"""'; |
786 } | 839 } |
787 | 840 |
788 void normalizeMutationRecord(MutationRecord record, TrySelection selection) { | 841 void normalizeMutationRecord(MutationRecord record, |
789 if (record.addedNodes.isEmpty) return; | 842 TrySelection selection, |
| 843 Set<Node> normalizedNodes) { |
790 for (Node node in record.addedNodes) { | 844 for (Node node in record.addedNodes) { |
791 if (node.parent == null) continue; | 845 if (node.parent == null) continue; |
792 StringBuffer buffer = new StringBuffer(); | 846 StringBuffer buffer = new StringBuffer(); |
793 int selectionOffset = htmlToText(node, buffer, selection); | 847 int selectionOffset = htmlToText(node, buffer, selection); |
794 Text newNode = new Text('$buffer'); | 848 Text newNode = new Text('$buffer'); |
795 node.replaceWith(newNode); | 849 node.replaceWith(newNode); |
| 850 normalizedNodes.add(findLine(newNode)); |
796 if (selectionOffset != -1) { | 851 if (selectionOffset != -1) { |
797 selection.anchorNode = newNode; | 852 selection.anchorNode = newNode; |
798 selection.anchorOffset = selectionOffset; | 853 selection.anchorOffset = selectionOffset; |
799 } | 854 } |
800 } | 855 } |
| 856 if (!record.removedNodes.isEmpty) { |
| 857 normalizedNodes.add(findLine(record.target)); |
| 858 } |
| 859 if (record.type == "characterData") { |
| 860 normalizedNodes.add(findLine(record.target)); |
| 861 } |
801 } | 862 } |
| 863 |
| 864 // Finds the line of [node] (a parent node with CSS class 'lineNumber'). |
| 865 // If no such parent exists, return mainEditorPane if it is a parent. |
| 866 // Otherwise return [node]. |
| 867 Node findLine(Node node) { |
| 868 for (Node n = node; n != null; n = n.parent) { |
| 869 if (n is Element && n.classes.contains('lineNumber')) return n; |
| 870 if (n == mainEditorPane) return n; |
| 871 } |
| 872 return node; |
| 873 } |
| 874 |
| 875 Element makeLine(List<Node> lineNodes, String state) { |
| 876 // Using a div element here (anything with display=block) generally messes up |
| 877 // editing and navigation. We would like to use a block element here so |
| 878 // error messages show as expected. But no such luck. Fortunately, there |
| 879 // are strong indications that the current solution for displaying errors |
| 880 // isn't good enough anyways. |
| 881 return new SpanElement() |
| 882 ..setAttribute('dart-state', state) |
| 883 ..nodes.addAll(lineNodes) |
| 884 ..classes.add('lineNumber'); |
| 885 } |
| 886 |
| 887 bool isAtEndOfFile(Text text, int offset) { |
| 888 Node line = findLine(text); |
| 889 return |
| 890 line.nextNode == null && |
| 891 text.parent.nextNode == null && |
| 892 offset == text.length; |
| 893 } |
| 894 |
| 895 List<String> splitLines(String text) { |
| 896 return text.split(new RegExp('^', multiLine: true)); |
| 897 } |
OLD | NEW |