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

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: Too many changes to summarize in one line. 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
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}.');
kasperl 2014/03/24 10:32:51 Debug printing (repeated)? Maybe introduce some ki
ahe 2014/03/24 15:20:23 There isn't much output from these print statement
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 ['a.diagnostic>span',
kasperl 2014/03/24 10:32:51 const [...]
ahe 2014/03/24 15:20:23 Done.
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
kasperl 2014/03/24 10:32:51 Remove newline.
ahe 2014/03/24 15:20:23 I've added it to aid navigation in Emacs.
186 case 'characterData':
187
kasperl 2014/03/24 10:32:51 Removew newline.
ahe 2014/03/24 15:20:23 Done.
188 bool hasSelection = false;
189 int offset = selection.anchorOffset;
190 if (selection.isCollapsed &&
191 selection.anchorNode == record.target) {
192 hasSelection = true;
193 }
194 var parent = record.target.parentNode;
195 if (parent != inputPre) {
196 editor.inlineChildren(parent);
197 }
198 if (hasSelection) {
199 selection.collapse(record.target, offset);
200 }
201 break;
202
203 default:
204 if (!record.addedNodes.isEmpty) {
205 for (var node in record.addedNodes) {
206
207 if (node.nodeType != Node.ELEMENT_NODE) continue;
208
209 if (node is BRElement) {
210 if (selection.anchorNode != node) {
211 node.replaceWith(new Text('\n'));
212 }
213 } else {
214 var parent = node.parentNode;
215 if (parent == null) continue;
216 var nodes = new List.from(node.nodes);
217 var style = node.getComputedStyle();
218 if (style.display != 'inline') {
219 var previous = node.previousNode;
220 if (previous is Text) {
221 previous.appendData('\n');
222 } else {
223 parent.insertBefore(new Text('\n'), node);
224 }
225 }
226 for (Node child in nodes) {
227 child.remove();
228 parent.insertBefore(child, node);
229 }
230 node.remove();
231 }
232 }
233 }
234 }
235 }
236 mutations = observer.takeRecords();
237 }
238
239 if (!inputPre.nodes.isEmpty && inputPre.nodes.last is Text) {
240 Text text = inputPre.nodes.last;
241 if (!text.text.endsWith('\n')) {
242 text.appendData('\n');
243 }
244 }
245
246 int offset = 0;
247 int anchorOffset = 0;
248 bool hasSelection = false;
249 Node anchorNode = selection.anchorNode;
250 // TODO(ahe): Try to share walk4 methods.
251 void walk4(Node node) {
252 // TODO(ahe): Use TreeWalker when that is exposed.
253 // function textNodesUnder(root){
254 // var n, a=[], walk=document.createTreeWalker(
255 // root,NodeFilter.SHOW_TEXT,null,false);
256 // while(n=walk.nextNode()) a.push(n);
257 // return a;
258 // }
259 int type = node.nodeType;
260 if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) {
261 CharacterData text = node;
262 if (anchorNode == node) {
263 hasSelection = true;
264 anchorOffset = selection.anchorOffset + offset;
265 return;
266 }
267 offset += text.length;
268 }
269
270 var child = node.firstChild;
271 while (child != null) {
272 walk4(child);
273 if (hasSelection) return;
274 child = child.nextNode;
275 }
276 }
277 if (selection.isCollapsed) {
278 walk4(inputPre);
279 }
280
281 editor.currentSource = inputPre.text;
282 inputPre.nodes.clear();
283 inputPre.appendText(editor.currentSource);
284 if (hasSelection) {
285 selection.collapse(inputPre.firstChild, anchorOffset);
286 }
287
288 editor.isMalformedInput = false;
289 for (var n in new List.from(inputPre.nodes)) {
290 if (n is! Text) continue;
291 Text node = n;
292 String text = node.text;
293
294 Token token = tokenize(text);
295 int offset = 0;
296 editor.seenIdentifiers = new Set<String>.from(mock.identifiers);
297 for (;token.kind != EOF_TOKEN; token = token.next) {
kasperl 2014/03/24 10:32:51 I'd put a space after ;.
ahe 2014/03/24 15:20:23 Done.
298 Decoration decoration = editor.getDecoration(token);
299 if (decoration == null) continue;
300 bool hasSelection = false;
301 int selectionOffset = selection.anchorOffset;
302
303 if (selection.isCollapsed && selection.anchorNode == node) {
304 hasSelection = true;
305 selectionOffset = selection.anchorOffset;
306 }
307 int splitPoint = token.charOffset - offset;
308 Text str = node.splitText(splitPoint);
309 Text after = str.splitText(token.charCount);
310 offset += splitPoint + token.charCount;
311 inputPre.insertBefore(after, node.nextNode);
312 inputPre.insertBefore(decoration.applyTo(str), after);
313
314 if (hasSelection && selectionOffset > node.length) {
315 selectionOffset -= node.length;
316 if (selectionOffset > str.length) {
317 selectionOffset -= str.length;
318 selection.collapse(after, selectionOffset);
319 } else {
320 selection.collapse(str, selectionOffset);
321 }
322 }
323 node = after;
324 }
325 }
326
327 window.localStorage['currentSource'] = editor.currentSource;
328 print('Saved source');
329
330 // Discard highlighting mutations.
331 observer.takeRecords();
332 }
333
334 void onSelectionChange(Event event) {
335 }
336
337 void onStateChanged(InteractionState previous) {
338 super.onStateChanged(previous);
339 scheduleCompilation();
340 }
341 }
342
343 class PendingInputState extends InitialState {
344 PendingInputState(InteractionContext context)
345 : super(context);
346
347 void onInput(Event event) {
348 // Do nothing.
349 }
350
351 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
352 super.onMutation(mutations, observer);
353
354 InteractionState nextState = new InitialState(context);
355 if (settings.enableCodeCompletion.value) {
356 Element parent = editor.getElementAtSelection();
357 Element ui;
358 if (parent != null) {
359 ui = parent.querySelector('.dart-code-completion');
360 if (ui != null) {
361 nextState = new CodeCompletionState(context, parent, ui);
362 }
363 }
364 }
365 state = nextState;
366 }
367 }
368
369 class CodeCompletionState extends InitialState {
370 final Element activeCompletion;
371 final Element ui;
372 int minWidth = 0;
373 DivElement staticResults;
374 SpanElement inline;
375 DivElement serverResults;
376 String inlineSuggestion;
377
378 CodeCompletionState(InteractionContext context,
379 this.activeCompletion,
380 this.ui)
381 : super(context);
382
383 void onInput(Event event) {
384 // Do nothing.
385 }
386
387 void onModifiedKeyUp(KeyboardEvent event) {
388 // TODO(ahe): Handle DOWN (jump to server results).
389 }
390
391 void onUnmodifiedKeyUp(KeyboardEvent event) {
392 switch (event.keyCode) {
393 case KeyCode.DOWN:
394 return moveDown(event);
395
396 case KeyCode.UP:
397 return moveUp(event);
398
399 case KeyCode.ESC:
400 event.preventDefault();
401 return endCompletion();
402
403 case KeyCode.TAB:
404 case KeyCode.RIGHT:
405 case KeyCode.ENTER:
406 event.preventDefault();
407 return endCompletion(acceptSuggestion: true);
408 }
409 }
410
411 void moveDown(Event event) {
412 event.preventDefault();
413 move(1);
414 }
415
416 void moveUp(Event event) {
417 event.preventDefault();
418 move(-1);
419 }
420
421 void move(int direction) {
422 Element element = editor.moveActive(direction);
423 if (element == null) return;
424 var text = activeCompletion.firstChild;
425 String prefix = "";
426 if (text is Text) prefix = text.data.trim();
427 updateInlineSuggestion(prefix, element.text);
428 }
429
430 void endCompletion({bool acceptSuggestion: false}) {
431 if (acceptSuggestion) {
432 suggestionAccepted();
433 }
434 activeCompletion.classes.remove('active');
435 inputPre.querySelectorAll('.hazed-suggestion').forEach((e) => e.remove());
436 // The above changes create mutation records. This implicitly fire mutation
437 // events that result in saving the source code in local storage.
438 // TODO(ahe): Consider making this more explicit.
439 state = new InitialState(context);
440 }
441
442 void suggestionAccepted() {
443 if (inlineSuggestion != null) {
444 Text text = new Text(inlineSuggestion);
445 activeCompletion.replaceWith(text);
446 window.getSelection().collapse(text, inlineSuggestion.length);
447 }
448 }
449
450 void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
451 for (MutationRecord record in mutations) {
452 if (!activeCompletion.contains(record.target)) {
453 endCompletion();
454 return super.onMutation(mutations, observer);
455 }
456 }
457
458 var text = activeCompletion.firstChild;
459 if (text is! Text) return endCompletion();
460 updateSuggestions(text.data.trim());
461 }
462
463 void onStateChanged(InteractionState previous) {
464 super.onStateChanged(previous);
465 displayCodeCompletion();
466 }
467
468 void displayCodeCompletion() {
469 Selection selection = window.getSelection();
470 if (selection.anchorNode is! Text) {
471 return endCompletion();
472 }
473 Text text = selection.anchorNode;
474 if (!activeCompletion.contains(text)) {
475 return endCompletion();
476 }
477
478 int anchorOffset = selection.anchorOffset;
479
480 String prefix = text.data.substring(0, anchorOffset).trim();
481 if (prefix.isEmpty) {
482 return endCompletion();
483 }
484
485 num height = activeCompletion.getBoundingClientRect().height;
486 activeCompletion.classes.add('active');
487 ui.nodes.clear();
488
489 inline = new SpanElement()
490 ..classes.add('hazed-suggestion');
491 Text rest = text.splitText(anchorOffset);
492 text.parentNode.insertBefore(inline, text.nextNode);
493 activeCompletion.parentNode.insertBefore(
494 rest, activeCompletion.nextNode);
495
496 staticResults = new DivElement()
497 ..classes.addAll(['dart-static', 'dart-limited-height']);
498 serverResults = new DivElement()
499 ..style.display = 'none'
500 ..classes.add('dart-server');
501 ui.nodes.addAll([staticResults, serverResults]);
502 ui.style.top = '${height}px';
503
504 staticResults.nodes.add(buildCompletionEntry(prefix));
505
506 updateSuggestions(prefix);
507 }
508
509 void updateInlineSuggestion(String prefix, String suggestion) {
510 inlineSuggestion = suggestion;
511
512 minWidth = max(minWidth, activeCompletion.getBoundingClientRect().width);
513
514 activeCompletion.style
515 ..display = 'inline-block'
516 ..minWidth = '${minWidth}px';
517
518 inline
519 ..nodes.clear()
520 ..appendText(suggestion.substring(prefix.length))
521 ..style.display = '';
522
523 observer.takeRecords(); // Discard mutations.
524 }
525
526 void updateSuggestions(String prefix) {
527 if (prefix.isEmpty) {
528 return endCompletion();
529 }
530
531 Token first = tokenize(prefix);
532 for (Token token = first; token.kind != EOF_TOKEN; token = token.next) {
533 String tokenInfo = token.info.value;
534 if (token != first ||
535 tokenInfo != 'identifier' &&
536 tokenInfo != 'keyword') {
537 return endCompletion();
538 }
539 }
540
541 var borderHeight = 2; // 1 pixel border top & bottom.
542 num height = ui.getBoundingClientRect().height - borderHeight;
543 ui.style.minHeight = '${height}px';
544
545 minWidth =
546 max(minWidth, activeCompletion.getBoundingClientRect().width);
547
548 staticResults.nodes.clear();
549 serverResults.nodes.clear();
550
551 if (inlineSuggestion != null && inlineSuggestion.startsWith(prefix)) {
552 inline
553 ..nodes.clear()
554 ..appendText(inlineSuggestion.substring(prefix.length));
555 }
556
557 List<String> results = editor.seenIdentifiers.where(
558 (String identifier) {
559 return identifier != prefix && identifier.startsWith(prefix);
560 }).toList(growable: false);
561 results.sort();
562 if (results.isEmpty) results = <String>[prefix];
563
564 results.forEach((String completion) {
565 staticResults.nodes.add(buildCompletionEntry(completion));
566 });
567
568 if (settings.enableDartMind) {
kasperl 2014/03/24 10:32:51 Maybe factor the DartMind support out in its own c
ahe 2014/03/24 15:20:23 Added TODO.
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

Powered by Google App Engine
This is Rietveld 408576698