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

Unified 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 side-by-side diff with in-line comments
Download patch
« 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 »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: dart/site/try/src/interaction_manager.dart
diff --git a/dart/site/try/src/interaction_manager.dart b/dart/site/try/src/interaction_manager.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b00d121dba328ddef448558315e220bcaacb14ef
--- /dev/null
+++ b/dart/site/try/src/interaction_manager.dart
@@ -0,0 +1,644 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library trydart.interaction_manager;
+
+import 'dart:html';
+
+import 'dart:convert' show
+ JSON;
+
+import 'dart:math' show
+ max,
+ min;
+
+import 'package:compiler/implementation/scanner/scannerlib.dart'
+ show
+ EOF_TOKEN,
+ StringScanner,
+ Token;
+
+import 'package:compiler/implementation/source_file.dart' show
+ StringSourceFile;
+
+import 'compilation.dart' show
+ scheduleCompilation;
+
+import 'ui.dart' show
+ currentTheme,
+ hackDiv,
+ inputPre,
+ observer,
+ outputDiv;
+
+import 'decoration.dart' show
+ CodeCompletionDecoration,
+ Decoration,
+ DiagnosticDecoration,
+ error,
+ info,
+ warning;
+
+import 'editor.dart' as editor;
+
+import 'mock.dart' as mock;
+
+import 'settings.dart' as settings;
+
+/**
+ * UI interaction manager for the entire application.
+ */
+abstract class InteractionManager {
+ // Design note: All UI interactions go through one instance of this
+ // class. This is by design.
+ //
+ // Simplicity in UI is in the eye of the beholder, not the implementor. Great
+ // 'natural UI' is usually achieved with substantial implementation
+ // complexity that doesn't modularise well and has nasty complicated state
+ // dependencies.
+ //
+ // In rare cases, some UI components can be independent of this state
+ // machine. For example, animation and auto-save loops.
+
+ // Implementation note: The state machine is actually implemented by
+ // [InteractionContext], this class represents public event handlers.
+
+ factory InteractionManager() => new InteractionContext();
+
+ InteractionManager.internal();
+
+ void onInput(Event event);
+
+ void onKeyUp(KeyboardEvent event);
+
+ void onMutation(List<MutationRecord> mutations, MutationObserver observer);
+
+ void onSelectionChange(Event event);
+}
+
+/**
+ * State machine for UI interactions.
+ */
+class InteractionContext extends InteractionManager {
+ InteractionState state;
+
+ InteractionContext()
+ : super.internal() {
+ state = new InitialState(this);
+ }
+
+ void onInput(Event event) => state.onInput(event);
+
+ void onKeyUp(KeyboardEvent event) => state.onKeyUp(event);
+
+ void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
+ return state.onMutation(mutations, observer);
+ }
+
+ void onSelectionChange(Event event) => state.onSelectionChange(event);
+}
+
+abstract class InteractionState implements InteractionManager {
+ void onStateChanged(InteractionState previous) {
+ print('State change ${previous.runtimeType} -> ${runtimeType}.');
+ }
+}
+
+class InitialState extends InteractionState {
+ final InteractionContext context;
+ bool requestCodeCompletion = false;
+
+ InitialState(this.context);
+
+ void set state(InteractionState state) {
+ InteractionState previous = context.state;
+ if (previous != state) {
+ context.state = state;
+ state.onStateChanged(previous);
+ }
+ }
+
+ void onInput(Event event) {
+ state = new PendingInputState(context);
+ }
+
+ void onKeyUp(KeyboardEvent event) {
+ if (computeHasModifier(event)) {
+ print('onKeyUp (modified)');
+ onModifiedKeyUp(event);
+ } else {
+ print('onKeyUp (unmodified)');
+ onUnmodifiedKeyUp(event);
+ }
+ }
+
+ void onModifiedKeyUp(KeyboardEvent event) {
+ }
+
+ void onUnmodifiedKeyUp(KeyboardEvent event) {
+ switch (event.keyCode) {
+ case KeyCode.ENTER: {
+ event.preventDefault();
+ Selection selection = window.getSelection();
+ if (selection.isCollapsed && selection.anchorNode is Text) {
+ Text text = selection.anchorNode;
+ int offset = selection.anchorOffset;
+ text.insertData(offset, '\n');
+ selection.collapse(text, offset + 1);
+ }
+ break;
+ }
+ }
+
+ // editor.scheduleRemoveCodeCompletion();
+
+ // This is a hack to get Safari (iOS) to send mutation events on
+ // contenteditable.
+ // TODO(ahe): Move to onInput?
+ var newDiv = new DivElement();
+ hackDiv.replaceWith(newDiv);
+ hackDiv = newDiv;
+ }
+
+ // TODO(ahe): This method should be cleaned up. It is too large.
+ void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
+ print('onMutation');
+
+ for (String query in const ['a.diagnostic>span',
+ '.dart-code-completion',
+ '.hazed-suggestion']) {
+ for (Element element in inputPre.querySelectorAll(query)) {
+ element.remove();
+ }
+ }
+
+ // Discard clean-up mutations.
+ observer.takeRecords();
+
+ Selection selection = window.getSelection();
+
+ while (!mutations.isEmpty) {
+ for (MutationRecord record in mutations) {
+ String type = record.type;
+ switch (type) {
+
+ case 'characterData':
+ bool hasSelection = false;
+ int offset = selection.anchorOffset;
+ if (selection.isCollapsed &&
+ selection.anchorNode == record.target) {
+ hasSelection = true;
+ }
+ var parent = record.target.parentNode;
+ if (parent != inputPre) {
+ editor.inlineChildren(parent);
+ }
+ if (hasSelection) {
+ selection.collapse(record.target, offset);
+ }
+ break;
+
+ default:
+ if (!record.addedNodes.isEmpty) {
+ for (var node in record.addedNodes) {
+
+ if (node.nodeType != Node.ELEMENT_NODE) continue;
+
+ if (node is BRElement) {
+ if (selection.anchorNode != node) {
+ node.replaceWith(new Text('\n'));
+ }
+ } else {
+ var parent = node.parentNode;
+ if (parent == null) continue;
+ var nodes = new List.from(node.nodes);
+ var style = node.getComputedStyle();
+ if (style.display != 'inline') {
+ var previous = node.previousNode;
+ if (previous is Text) {
+ previous.appendData('\n');
+ } else {
+ parent.insertBefore(new Text('\n'), node);
+ }
+ }
+ for (Node child in nodes) {
+ child.remove();
+ parent.insertBefore(child, node);
+ }
+ node.remove();
+ }
+ }
+ }
+ }
+ }
+ mutations = observer.takeRecords();
+ }
+
+ if (!inputPre.nodes.isEmpty && inputPre.nodes.last is Text) {
+ Text text = inputPre.nodes.last;
+ if (!text.text.endsWith('\n')) {
+ text.appendData('\n');
+ }
+ }
+
+ int offset = 0;
+ int anchorOffset = 0;
+ bool hasSelection = false;
+ Node anchorNode = selection.anchorNode;
+ // TODO(ahe): Try to share walk4 methods.
+ void walk4(Node node) {
+ // TODO(ahe): Use TreeWalker when that is exposed.
+ // function textNodesUnder(root){
+ // var n, a=[], walk=document.createTreeWalker(
+ // root,NodeFilter.SHOW_TEXT,null,false);
+ // while(n=walk.nextNode()) a.push(n);
+ // return a;
+ // }
+ int type = node.nodeType;
+ if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) {
+ CharacterData text = node;
+ if (anchorNode == node) {
+ hasSelection = true;
+ anchorOffset = selection.anchorOffset + offset;
+ return;
+ }
+ offset += text.length;
+ }
+
+ var child = node.firstChild;
+ while (child != null) {
+ walk4(child);
+ if (hasSelection) return;
+ child = child.nextNode;
+ }
+ }
+ if (selection.isCollapsed) {
+ walk4(inputPre);
+ }
+
+ editor.currentSource = inputPre.text;
+ inputPre.nodes.clear();
+ inputPre.appendText(editor.currentSource);
+ if (hasSelection) {
+ selection.collapse(inputPre.firstChild, anchorOffset);
+ }
+
+ editor.isMalformedInput = false;
+ for (var n in new List.from(inputPre.nodes)) {
+ if (n is! Text) continue;
+ Text node = n;
+ String text = node.text;
+
+ Token token = tokenize(text);
+ int offset = 0;
+ editor.seenIdentifiers = new Set<String>.from(mock.identifiers);
+ for (; token.kind != EOF_TOKEN; token = token.next) {
+ Decoration decoration = editor.getDecoration(token);
+ if (decoration == null) continue;
+ bool hasSelection = false;
+ int selectionOffset = selection.anchorOffset;
+
+ if (selection.isCollapsed && selection.anchorNode == node) {
+ hasSelection = true;
+ selectionOffset = selection.anchorOffset;
+ }
+ int splitPoint = token.charOffset - offset;
+ Text str = node.splitText(splitPoint);
+ Text after = str.splitText(token.charCount);
+ offset += splitPoint + token.charCount;
+ inputPre.insertBefore(after, node.nextNode);
+ inputPre.insertBefore(decoration.applyTo(str), after);
+
+ if (hasSelection && selectionOffset > node.length) {
+ selectionOffset -= node.length;
+ if (selectionOffset > str.length) {
+ selectionOffset -= str.length;
+ selection.collapse(after, selectionOffset);
+ } else {
+ selection.collapse(str, selectionOffset);
+ }
+ }
+ node = after;
+ }
+ }
+
+ window.localStorage['currentSource'] = editor.currentSource;
+ print('Saved source');
+
+ // Discard highlighting mutations.
+ observer.takeRecords();
+ }
+
+ void onSelectionChange(Event event) {
+ }
+
+ void onStateChanged(InteractionState previous) {
+ super.onStateChanged(previous);
+ scheduleCompilation();
+ }
+}
+
+class PendingInputState extends InitialState {
+ PendingInputState(InteractionContext context)
+ : super(context);
+
+ void onInput(Event event) {
+ // Do nothing.
+ }
+
+ void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
+ super.onMutation(mutations, observer);
+
+ InteractionState nextState = new InitialState(context);
+ if (settings.enableCodeCompletion.value) {
+ Element parent = editor.getElementAtSelection();
+ Element ui;
+ if (parent != null) {
+ ui = parent.querySelector('.dart-code-completion');
+ if (ui != null) {
+ nextState = new CodeCompletionState(context, parent, ui);
+ }
+ }
+ }
+ state = nextState;
+ }
+}
+
+class CodeCompletionState extends InitialState {
+ final Element activeCompletion;
+ final Element ui;
+ int minWidth = 0;
+ DivElement staticResults;
+ SpanElement inline;
+ DivElement serverResults;
+ String inlineSuggestion;
+
+ CodeCompletionState(InteractionContext context,
+ this.activeCompletion,
+ this.ui)
+ : super(context);
+
+ void onInput(Event event) {
+ // Do nothing.
+ }
+
+ void onModifiedKeyUp(KeyboardEvent event) {
+ // TODO(ahe): Handle DOWN (jump to server results).
+ }
+
+ void onUnmodifiedKeyUp(KeyboardEvent event) {
+ switch (event.keyCode) {
+ case KeyCode.DOWN:
+ return moveDown(event);
+
+ case KeyCode.UP:
+ return moveUp(event);
+
+ case KeyCode.ESC:
+ event.preventDefault();
+ return endCompletion();
+
+ case KeyCode.TAB:
+ case KeyCode.RIGHT:
+ case KeyCode.ENTER:
+ event.preventDefault();
+ return endCompletion(acceptSuggestion: true);
+ }
+ }
+
+ void moveDown(Event event) {
+ event.preventDefault();
+ move(1);
+ }
+
+ void moveUp(Event event) {
+ event.preventDefault();
+ move(-1);
+ }
+
+ void move(int direction) {
+ Element element = editor.moveActive(direction);
+ if (element == null) return;
+ var text = activeCompletion.firstChild;
+ String prefix = "";
+ if (text is Text) prefix = text.data.trim();
+ updateInlineSuggestion(prefix, element.text);
+ }
+
+ void endCompletion({bool acceptSuggestion: false}) {
+ if (acceptSuggestion) {
+ suggestionAccepted();
+ }
+ activeCompletion.classes.remove('active');
+ inputPre.querySelectorAll('.hazed-suggestion').forEach((e) => e.remove());
+ // The above changes create mutation records. This implicitly fire mutation
+ // events that result in saving the source code in local storage.
+ // TODO(ahe): Consider making this more explicit.
+ state = new InitialState(context);
+ }
+
+ void suggestionAccepted() {
+ if (inlineSuggestion != null) {
+ Text text = new Text(inlineSuggestion);
+ activeCompletion.replaceWith(text);
+ window.getSelection().collapse(text, inlineSuggestion.length);
+ }
+ }
+
+ void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
+ for (MutationRecord record in mutations) {
+ if (!activeCompletion.contains(record.target)) {
+ endCompletion();
+ return super.onMutation(mutations, observer);
+ }
+ }
+
+ var text = activeCompletion.firstChild;
+ if (text is! Text) return endCompletion();
+ updateSuggestions(text.data.trim());
+ }
+
+ void onStateChanged(InteractionState previous) {
+ super.onStateChanged(previous);
+ displayCodeCompletion();
+ }
+
+ void displayCodeCompletion() {
+ Selection selection = window.getSelection();
+ if (selection.anchorNode is! Text) {
+ return endCompletion();
+ }
+ Text text = selection.anchorNode;
+ if (!activeCompletion.contains(text)) {
+ return endCompletion();
+ }
+
+ int anchorOffset = selection.anchorOffset;
+
+ String prefix = text.data.substring(0, anchorOffset).trim();
+ if (prefix.isEmpty) {
+ return endCompletion();
+ }
+
+ num height = activeCompletion.getBoundingClientRect().height;
+ activeCompletion.classes.add('active');
+ ui.nodes.clear();
+
+ inline = new SpanElement()
+ ..classes.add('hazed-suggestion');
+ Text rest = text.splitText(anchorOffset);
+ text.parentNode.insertBefore(inline, text.nextNode);
+ activeCompletion.parentNode.insertBefore(
+ rest, activeCompletion.nextNode);
+
+ staticResults = new DivElement()
+ ..classes.addAll(['dart-static', 'dart-limited-height']);
+ serverResults = new DivElement()
+ ..style.display = 'none'
+ ..classes.add('dart-server');
+ ui.nodes.addAll([staticResults, serverResults]);
+ ui.style.top = '${height}px';
+
+ staticResults.nodes.add(buildCompletionEntry(prefix));
+
+ updateSuggestions(prefix);
+ }
+
+ void updateInlineSuggestion(String prefix, String suggestion) {
+ inlineSuggestion = suggestion;
+
+ minWidth = max(minWidth, activeCompletion.getBoundingClientRect().width);
+
+ activeCompletion.style
+ ..display = 'inline-block'
+ ..minWidth = '${minWidth}px';
+
+ inline
+ ..nodes.clear()
+ ..appendText(suggestion.substring(prefix.length))
+ ..style.display = '';
+
+ observer.takeRecords(); // Discard mutations.
+ }
+
+ void updateSuggestions(String prefix) {
+ if (prefix.isEmpty) {
+ return endCompletion();
+ }
+
+ Token first = tokenize(prefix);
+ for (Token token = first; token.kind != EOF_TOKEN; token = token.next) {
+ String tokenInfo = token.info.value;
+ if (token != first ||
+ tokenInfo != 'identifier' &&
+ tokenInfo != 'keyword') {
+ return endCompletion();
+ }
+ }
+
+ var borderHeight = 2; // 1 pixel border top & bottom.
+ num height = ui.getBoundingClientRect().height - borderHeight;
+ ui.style.minHeight = '${height}px';
+
+ minWidth =
+ max(minWidth, activeCompletion.getBoundingClientRect().width);
+
+ staticResults.nodes.clear();
+ serverResults.nodes.clear();
+
+ if (inlineSuggestion != null && inlineSuggestion.startsWith(prefix)) {
+ inline
+ ..nodes.clear()
+ ..appendText(inlineSuggestion.substring(prefix.length));
+ }
+
+ List<String> results = editor.seenIdentifiers.where(
+ (String identifier) {
+ return identifier != prefix && identifier.startsWith(prefix);
+ }).toList(growable: false);
+ results.sort();
+ if (results.isEmpty) results = <String>[prefix];
+
+ results.forEach((String completion) {
+ staticResults.nodes.add(buildCompletionEntry(completion));
+ });
+
+ if (settings.enableDartMind) {
+ // TODO(ahe): Move this code to its own function or class.
+ String encodedArg0 = Uri.encodeComponent('"$prefix"');
+ String mindQuery =
+ 'http://dart-mind.appspot.com/rpc'
+ '?action=GetExportingPubCompletions'
+ '&arg0=$encodedArg0';
+ try {
+ var serverWatch = new Stopwatch()..start();
+ HttpRequest.getString(mindQuery).then((String responseText) {
+ serverWatch.stop();
+ List<String> serverSuggestions = JSON.decode(responseText);
+ if (!serverSuggestions.isEmpty) {
+ updateInlineSuggestion(prefix, serverSuggestions.first);
+ }
+ for (int i = 1; i < serverSuggestions.length; i++) {
+ String completion = serverSuggestions[i];
+ DivElement where = staticResults;
+ int index = results.indexOf(completion);
+ if (index != -1) {
+ List<Element> entries =
+ document.querySelectorAll('.dart-static>.dart-entry');
+ entries[index].classes.add('doubleplusgood');
+ } else {
+ if (results.length > 3) {
+ serverResults.style.display = 'block';
+ where = serverResults;
+ }
+ Element entry = buildCompletionEntry(completion);
+ entry.classes.add('doubleplusgood');
+ where.nodes.add(entry);
+ }
+ }
+ serverResults.appendHtml(
+ '<div>${serverWatch.elapsedMilliseconds}ms</div>');
+ // Discard mutations.
+ observer.takeRecords();
+ }).catchError((error, stack) {
+ window.console.dir(error);
+ window.console.error('$stack');
+ });
+ } catch (error, stack) {
+ window.console.dir(error);
+ window.console.error('$stack');
+ }
+ }
+ // Discard mutations.
+ observer.takeRecords();
+ }
+
+ Element buildCompletionEntry(String completion) {
+ return new DivElement()
+ ..classes.add('dart-entry')
+ ..appendText(completion);
+ }
+}
+
+Token tokenize(String text) {
+ var file = new StringSourceFile('', text);
+ return new StringScanner(file, includeComments: true).tokenize();
+}
+
+bool computeHasModifier(KeyboardEvent event) {
+ return
+ event.getModifierState("Alt") ||
+ event.getModifierState("AltGraph") ||
+ event.getModifierState("CapsLock") ||
+ event.getModifierState("Control") ||
+ event.getModifierState("Fn") ||
+ event.getModifierState("Meta") ||
+ event.getModifierState("NumLock") ||
+ event.getModifierState("ScrollLock") ||
+ event.getModifierState("Scroll") ||
+ event.getModifierState("Win") ||
+ event.getModifierState("Shift") ||
+ event.getModifierState("SymbolLock") ||
+ event.getModifierState("OS");
+}
« 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