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

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

Issue 197923002: Mock up code completion. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge
Patch Set: Merged with r34309. Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « dart/site/try/src/editor.dart ('k') | dart/site/try/src/isolate_legacy.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library trydart.interaction_manager;
6
7 import 'dart:html';
8
9 import 'dart:convert' show
10 JSON;
11
12 import 'dart:math' show
13 max,
14 min;
15
16 import 'package:compiler/implementation/scanner/scannerlib.dart'
17 show
18 EOF_TOKEN,
19 StringScanner,
20 Token;
21
22 import 'package:compiler/implementation/source_file.dart' show
23 StringSourceFile;
24
25 import 'compilation.dart' show
26 scheduleCompilation;
27
28 import 'ui.dart' show
29 currentTheme,
30 hackDiv,
31 inputPre,
32 observer,
33 outputDiv;
34
35 import 'decoration.dart' show
36 CodeCompletionDecoration,
37 Decoration,
38 DiagnosticDecoration,
39 error,
40 info,
41 warning;
42
43 import 'editor.dart' as editor;
44
45 import 'mock.dart' as mock;
46
47 import 'settings.dart' as settings;
48
49 /**
50 * UI interaction manager for the entire application.
51 */
52 abstract class InteractionManager {
53 // Design note: All UI interactions go through one instance of this
54 // class. This is by design.
55 //
56 // Simplicity in UI is in the eye of the beholder, not the implementor. Great
57 // 'natural UI' is usually achieved with substantial implementation
58 // complexity that doesn't modularise well and has nasty complicated state
59 // dependencies.
60 //
61 // In rare cases, some UI components can be independent of this state
62 // machine. For example, animation and auto-save loops.
63
64 // Implementation note: The state machine is actually implemented by
65 // [InteractionContext], this class represents public event handlers.
66
67 factory InteractionManager() => new InteractionContext();
68
69 InteractionManager.internal();
70
71 void onInput(Event event);
72
73 void onKeyUp(KeyboardEvent event);
74
75 void onMutation(List<MutationRecord> mutations, MutationObserver observer);
76
77 void onSelectionChange(Event event);
78 }
79
80 /**
81 * State machine for UI interactions.
82 */
83 class InteractionContext extends InteractionManager {
84 InteractionState state;
85
86 InteractionContext()
87 : super.internal() {
88 state = new InitialState(this);
89 }
90
91 void onInput(Event event) => state.onInput(event);
92
93 void onKeyUp(KeyboardEvent event) => state.onKeyUp(event);
94
95 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
96 return state.onMutation(mutations, observer);
97 }
98
99 void onSelectionChange(Event event) => state.onSelectionChange(event);
100 }
101
102 abstract class InteractionState implements InteractionManager {
103 void onStateChanged(InteractionState previous) {
104 print('State change ${previous.runtimeType} -> ${runtimeType}.');
105 }
106 }
107
108 class InitialState extends InteractionState {
109 final InteractionContext context;
110 bool requestCodeCompletion = false;
111
112 InitialState(this.context);
113
114 void set state(InteractionState state) {
115 InteractionState previous = context.state;
116 if (previous != state) {
117 context.state = state;
118 state.onStateChanged(previous);
119 }
120 }
121
122 void onInput(Event event) {
123 state = new PendingInputState(context);
124 }
125
126 void onKeyUp(KeyboardEvent event) {
127 if (computeHasModifier(event)) {
128 print('onKeyUp (modified)');
129 onModifiedKeyUp(event);
130 } else {
131 print('onKeyUp (unmodified)');
132 onUnmodifiedKeyUp(event);
133 }
134 }
135
136 void onModifiedKeyUp(KeyboardEvent event) {
137 }
138
139 void onUnmodifiedKeyUp(KeyboardEvent event) {
140 switch (event.keyCode) {
141 case KeyCode.ENTER: {
142 event.preventDefault();
143 Selection selection = window.getSelection();
144 if (selection.isCollapsed && selection.anchorNode is Text) {
145 Text text = selection.anchorNode;
146 int offset = selection.anchorOffset;
147 text.insertData(offset, '\n');
148 selection.collapse(text, offset + 1);
149 }
150 break;
151 }
152 }
153
154 // editor.scheduleRemoveCodeCompletion();
155
156 // This is a hack to get Safari (iOS) to send mutation events on
157 // contenteditable.
158 // TODO(ahe): Move to onInput?
159 var newDiv = new DivElement();
160 hackDiv.replaceWith(newDiv);
161 hackDiv = newDiv;
162 }
163
164 // TODO(ahe): This method should be cleaned up. It is too large.
165 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
166 print('onMutation');
167
168 for (String query in const ['a.diagnostic>span',
169 '.dart-code-completion',
170 '.hazed-suggestion']) {
171 for (Element element in inputPre.querySelectorAll(query)) {
172 element.remove();
173 }
174 }
175
176 // Discard clean-up mutations.
177 observer.takeRecords();
178
179 Selection selection = window.getSelection();
180
181 while (!mutations.isEmpty) {
182 for (MutationRecord record in mutations) {
183 String type = record.type;
184 switch (type) {
185
186 case 'characterData':
187 bool hasSelection = false;
188 int offset = selection.anchorOffset;
189 if (selection.isCollapsed &&
190 selection.anchorNode == record.target) {
191 hasSelection = true;
192 }
193 var parent = record.target.parentNode;
194 if (parent != inputPre) {
195 editor.inlineChildren(parent);
196 }
197 if (hasSelection) {
198 selection.collapse(record.target, offset);
199 }
200 break;
201
202 default:
203 if (!record.addedNodes.isEmpty) {
204 for (var node in record.addedNodes) {
205
206 if (node.nodeType != Node.ELEMENT_NODE) continue;
207
208 if (node is BRElement) {
209 if (selection.anchorNode != node) {
210 node.replaceWith(new Text('\n'));
211 }
212 } else {
213 var parent = node.parentNode;
214 if (parent == null) continue;
215 var nodes = new List.from(node.nodes);
216 var style = node.getComputedStyle();
217 if (style.display != 'inline') {
218 var previous = node.previousNode;
219 if (previous is Text) {
220 previous.appendData('\n');
221 } else {
222 parent.insertBefore(new Text('\n'), node);
223 }
224 }
225 for (Node child in nodes) {
226 child.remove();
227 parent.insertBefore(child, node);
228 }
229 node.remove();
230 }
231 }
232 }
233 }
234 }
235 mutations = observer.takeRecords();
236 }
237
238 if (!inputPre.nodes.isEmpty && inputPre.nodes.last is Text) {
239 Text text = inputPre.nodes.last;
240 if (!text.text.endsWith('\n')) {
241 text.appendData('\n');
242 }
243 }
244
245 int offset = 0;
246 int anchorOffset = 0;
247 bool hasSelection = false;
248 Node anchorNode = selection.anchorNode;
249 // TODO(ahe): Try to share walk4 methods.
250 void walk4(Node node) {
251 // TODO(ahe): Use TreeWalker when that is exposed.
252 // function textNodesUnder(root){
253 // var n, a=[], walk=document.createTreeWalker(
254 // root,NodeFilter.SHOW_TEXT,null,false);
255 // while(n=walk.nextNode()) a.push(n);
256 // return a;
257 // }
258 int type = node.nodeType;
259 if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) {
260 CharacterData text = node;
261 if (anchorNode == node) {
262 hasSelection = true;
263 anchorOffset = selection.anchorOffset + offset;
264 return;
265 }
266 offset += text.length;
267 }
268
269 var child = node.firstChild;
270 while (child != null) {
271 walk4(child);
272 if (hasSelection) return;
273 child = child.nextNode;
274 }
275 }
276 if (selection.isCollapsed) {
277 walk4(inputPre);
278 }
279
280 editor.currentSource = inputPre.text;
281 inputPre.nodes.clear();
282 inputPre.appendText(editor.currentSource);
283 if (hasSelection) {
284 selection.collapse(inputPre.firstChild, anchorOffset);
285 }
286
287 editor.isMalformedInput = false;
288 for (var n in new List.from(inputPre.nodes)) {
289 if (n is! Text) continue;
290 Text node = n;
291 String text = node.text;
292
293 Token token = tokenize(text);
294 int offset = 0;
295 editor.seenIdentifiers = new Set<String>.from(mock.identifiers);
296 for (; token.kind != EOF_TOKEN; token = token.next) {
297 Decoration decoration = editor.getDecoration(token);
298 if (decoration == null) continue;
299 bool hasSelection = false;
300 int selectionOffset = selection.anchorOffset;
301
302 if (selection.isCollapsed && selection.anchorNode == node) {
303 hasSelection = true;
304 selectionOffset = selection.anchorOffset;
305 }
306 int splitPoint = token.charOffset - offset;
307 Text str = node.splitText(splitPoint);
308 Text after = str.splitText(token.charCount);
309 offset += splitPoint + token.charCount;
310 inputPre.insertBefore(after, node.nextNode);
311 inputPre.insertBefore(decoration.applyTo(str), after);
312
313 if (hasSelection && selectionOffset > node.length) {
314 selectionOffset -= node.length;
315 if (selectionOffset > str.length) {
316 selectionOffset -= str.length;
317 selection.collapse(after, selectionOffset);
318 } else {
319 selection.collapse(str, selectionOffset);
320 }
321 }
322 node = after;
323 }
324 }
325
326 window.localStorage['currentSource'] = editor.currentSource;
327 print('Saved source');
328
329 // Discard highlighting mutations.
330 observer.takeRecords();
331 }
332
333 void onSelectionChange(Event event) {
334 }
335
336 void onStateChanged(InteractionState previous) {
337 super.onStateChanged(previous);
338 scheduleCompilation();
339 }
340 }
341
342 class PendingInputState extends InitialState {
343 PendingInputState(InteractionContext context)
344 : super(context);
345
346 void onInput(Event event) {
347 // Do nothing.
348 }
349
350 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
351 super.onMutation(mutations, observer);
352
353 InteractionState nextState = new InitialState(context);
354 if (settings.enableCodeCompletion.value) {
355 Element parent = editor.getElementAtSelection();
356 Element ui;
357 if (parent != null) {
358 ui = parent.querySelector('.dart-code-completion');
359 if (ui != null) {
360 nextState = new CodeCompletionState(context, parent, ui);
361 }
362 }
363 }
364 state = nextState;
365 }
366 }
367
368 class CodeCompletionState extends InitialState {
369 final Element activeCompletion;
370 final Element ui;
371 int minWidth = 0;
372 DivElement staticResults;
373 SpanElement inline;
374 DivElement serverResults;
375 String inlineSuggestion;
376
377 CodeCompletionState(InteractionContext context,
378 this.activeCompletion,
379 this.ui)
380 : super(context);
381
382 void onInput(Event event) {
383 // Do nothing.
384 }
385
386 void onModifiedKeyUp(KeyboardEvent event) {
387 // TODO(ahe): Handle DOWN (jump to server results).
388 }
389
390 void onUnmodifiedKeyUp(KeyboardEvent event) {
391 switch (event.keyCode) {
392 case KeyCode.DOWN:
393 return moveDown(event);
394
395 case KeyCode.UP:
396 return moveUp(event);
397
398 case KeyCode.ESC:
399 event.preventDefault();
400 return endCompletion();
401
402 case KeyCode.TAB:
403 case KeyCode.RIGHT:
404 case KeyCode.ENTER:
405 event.preventDefault();
406 return endCompletion(acceptSuggestion: true);
407 }
408 }
409
410 void moveDown(Event event) {
411 event.preventDefault();
412 move(1);
413 }
414
415 void moveUp(Event event) {
416 event.preventDefault();
417 move(-1);
418 }
419
420 void move(int direction) {
421 Element element = editor.moveActive(direction);
422 if (element == null) return;
423 var text = activeCompletion.firstChild;
424 String prefix = "";
425 if (text is Text) prefix = text.data.trim();
426 updateInlineSuggestion(prefix, element.text);
427 }
428
429 void endCompletion({bool acceptSuggestion: false}) {
430 if (acceptSuggestion) {
431 suggestionAccepted();
432 }
433 activeCompletion.classes.remove('active');
434 inputPre.querySelectorAll('.hazed-suggestion').forEach((e) => e.remove());
435 // The above changes create mutation records. This implicitly fire mutation
436 // events that result in saving the source code in local storage.
437 // TODO(ahe): Consider making this more explicit.
438 state = new InitialState(context);
439 }
440
441 void suggestionAccepted() {
442 if (inlineSuggestion != null) {
443 Text text = new Text(inlineSuggestion);
444 activeCompletion.replaceWith(text);
445 window.getSelection().collapse(text, inlineSuggestion.length);
446 }
447 }
448
449 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
450 for (MutationRecord record in mutations) {
451 if (!activeCompletion.contains(record.target)) {
452 endCompletion();
453 return super.onMutation(mutations, observer);
454 }
455 }
456
457 var text = activeCompletion.firstChild;
458 if (text is! Text) return endCompletion();
459 updateSuggestions(text.data.trim());
460 }
461
462 void onStateChanged(InteractionState previous) {
463 super.onStateChanged(previous);
464 displayCodeCompletion();
465 }
466
467 void displayCodeCompletion() {
468 Selection selection = window.getSelection();
469 if (selection.anchorNode is! Text) {
470 return endCompletion();
471 }
472 Text text = selection.anchorNode;
473 if (!activeCompletion.contains(text)) {
474 return endCompletion();
475 }
476
477 int anchorOffset = selection.anchorOffset;
478
479 String prefix = text.data.substring(0, anchorOffset).trim();
480 if (prefix.isEmpty) {
481 return endCompletion();
482 }
483
484 num height = activeCompletion.getBoundingClientRect().height;
485 activeCompletion.classes.add('active');
486 ui.nodes.clear();
487
488 inline = new SpanElement()
489 ..classes.add('hazed-suggestion');
490 Text rest = text.splitText(anchorOffset);
491 text.parentNode.insertBefore(inline, text.nextNode);
492 activeCompletion.parentNode.insertBefore(
493 rest, activeCompletion.nextNode);
494
495 staticResults = new DivElement()
496 ..classes.addAll(['dart-static', 'dart-limited-height']);
497 serverResults = new DivElement()
498 ..style.display = 'none'
499 ..classes.add('dart-server');
500 ui.nodes.addAll([staticResults, serverResults]);
501 ui.style.top = '${height}px';
502
503 staticResults.nodes.add(buildCompletionEntry(prefix));
504
505 updateSuggestions(prefix);
506 }
507
508 void updateInlineSuggestion(String prefix, String suggestion) {
509 inlineSuggestion = suggestion;
510
511 minWidth = max(minWidth, activeCompletion.getBoundingClientRect().width);
512
513 activeCompletion.style
514 ..display = 'inline-block'
515 ..minWidth = '${minWidth}px';
516
517 inline
518 ..nodes.clear()
519 ..appendText(suggestion.substring(prefix.length))
520 ..style.display = '';
521
522 observer.takeRecords(); // Discard mutations.
523 }
524
525 void updateSuggestions(String prefix) {
526 if (prefix.isEmpty) {
527 return endCompletion();
528 }
529
530 Token first = tokenize(prefix);
531 for (Token token = first; token.kind != EOF_TOKEN; token = token.next) {
532 String tokenInfo = token.info.value;
533 if (token != first ||
534 tokenInfo != 'identifier' &&
535 tokenInfo != 'keyword') {
536 return endCompletion();
537 }
538 }
539
540 var borderHeight = 2; // 1 pixel border top & bottom.
541 num height = ui.getBoundingClientRect().height - borderHeight;
542 ui.style.minHeight = '${height}px';
543
544 minWidth =
545 max(minWidth, activeCompletion.getBoundingClientRect().width);
546
547 staticResults.nodes.clear();
548 serverResults.nodes.clear();
549
550 if (inlineSuggestion != null && inlineSuggestion.startsWith(prefix)) {
551 inline
552 ..nodes.clear()
553 ..appendText(inlineSuggestion.substring(prefix.length));
554 }
555
556 List<String> results = editor.seenIdentifiers.where(
557 (String identifier) {
558 return identifier != prefix && identifier.startsWith(prefix);
559 }).toList(growable: false);
560 results.sort();
561 if (results.isEmpty) results = <String>[prefix];
562
563 results.forEach((String completion) {
564 staticResults.nodes.add(buildCompletionEntry(completion));
565 });
566
567 if (settings.enableDartMind) {
568 // TODO(ahe): Move this code to its own function or class.
569 String encodedArg0 = Uri.encodeComponent('"$prefix"');
570 String mindQuery =
571 'http://dart-mind.appspot.com/rpc'
572 '?action=GetExportingPubCompletions'
573 '&arg0=$encodedArg0';
574 try {
575 var serverWatch = new Stopwatch()..start();
576 HttpRequest.getString(mindQuery).then((String responseText) {
577 serverWatch.stop();
578 List<String> serverSuggestions = JSON.decode(responseText);
579 if (!serverSuggestions.isEmpty) {
580 updateInlineSuggestion(prefix, serverSuggestions.first);
581 }
582 for (int i = 1; i < serverSuggestions.length; i++) {
583 String completion = serverSuggestions[i];
584 DivElement where = staticResults;
585 int index = results.indexOf(completion);
586 if (index != -1) {
587 List<Element> entries =
588 document.querySelectorAll('.dart-static>.dart-entry');
589 entries[index].classes.add('doubleplusgood');
590 } else {
591 if (results.length > 3) {
592 serverResults.style.display = 'block';
593 where = serverResults;
594 }
595 Element entry = buildCompletionEntry(completion);
596 entry.classes.add('doubleplusgood');
597 where.nodes.add(entry);
598 }
599 }
600 serverResults.appendHtml(
601 '<div>${serverWatch.elapsedMilliseconds}ms</div>');
602 // Discard mutations.
603 observer.takeRecords();
604 }).catchError((error, stack) {
605 window.console.dir(error);
606 window.console.error('$stack');
607 });
608 } catch (error, stack) {
609 window.console.dir(error);
610 window.console.error('$stack');
611 }
612 }
613 // Discard mutations.
614 observer.takeRecords();
615 }
616
617 Element buildCompletionEntry(String completion) {
618 return new DivElement()
619 ..classes.add('dart-entry')
620 ..appendText(completion);
621 }
622 }
623
624 Token tokenize(String text) {
625 var file = new StringSourceFile('', text);
626 return new StringScanner(file, includeComments: true).tokenize();
627 }
628
629 bool computeHasModifier(KeyboardEvent event) {
630 return
631 event.getModifierState("Alt") ||
632 event.getModifierState("AltGraph") ||
633 event.getModifierState("CapsLock") ||
634 event.getModifierState("Control") ||
635 event.getModifierState("Fn") ||
636 event.getModifierState("Meta") ||
637 event.getModifierState("NumLock") ||
638 event.getModifierState("ScrollLock") ||
639 event.getModifierState("Scroll") ||
640 event.getModifierState("Win") ||
641 event.getModifierState("Shift") ||
642 event.getModifierState("SymbolLock") ||
643 event.getModifierState("OS");
644 }
OLDNEW
« no previous file with comments | « dart/site/try/src/editor.dart ('k') | dart/site/try/src/isolate_legacy.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698