瀏覽代碼

HUE-5526 [editor] Have single instance of the new autocompleter dropdown for performance

Johan Ahlen 9 年之前
父節點
當前提交
d435866

+ 0 - 12
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -3334,18 +3334,6 @@
         }
       };
 
-      huePubSub.subscribe('hue.ace.autocompleter.show', function (data) {
-        if ($('.hue-ace-autocompleter').length > 0) {
-          $('.hue-ace-autocompleter').remove();
-        }
-        var $hueAceAutocompleter = $('<div data-bind="component: { name: \'hueAceAutocompleter\', params: $data }">').addClass('hue-ace-autocompleter').css({ top: data.position.top + data.lineHeight + 3 + 'px', left: data.position.left  + 'px' });
-        $('body').append($hueAceAutocompleter);
-        ko.applyBindings({
-          snippet: snippet,
-          editor: editor
-        }, $hueAceAutocompleter[0]);
-      });
-
       var langTools = ace.require("ace/ext/language_tools");
       langTools.textCompleter.setSqlMode(snippet.isSqlDialect());
 

+ 140 - 107
desktop/core/src/desktop/static/desktop/js/sqlAutocompleter3.js

@@ -38,7 +38,6 @@ var SqlAutocompleter3 = (function () {
     JOIN: -1
   };
 
-
   var hiveReservedKeywords = {
     ALL: true, ALTER: true, AND: true, ARRAY: true, AS: true, AUTHORIZATION: true, BETWEEN: true, BIGINT: true, BINARY: true, BOOLEAN: true, BOTH: true, BY: true, CASE: true, CAST: true,
     CHAR: true, COLUMN: true, CONF: true, CREATE: true, CROSS: true, CUBE: true, CURRENT: true, CURRENT_DATE: true, CURRENT_TIMESTAMP: true, CURSOR: true,
@@ -100,113 +99,100 @@ var SqlAutocompleter3 = (function () {
   }
 
   /**
-   * Represents the keyword category of suggestions
    *
-   * @param parent
-   * @param colRefDeferral
+   * @param options
    * @constructor
    */
-  function KeywordsCategory (parent, colRefDeferral) {
+  function Suggestions (options) {
     var self = this;
-    self.label = AutocompleterGlobals.i18n.keywords;
-    self.suggestions = ko.observableArray([]);
-    self.loading = ko.observable(false);
+    self.apiHelper = ApiHelper.getInstance();
+    self.snippet = options.snippet;
 
-    if (parent.parseResult.suggestKeywords) {
-      var keywordSuggestions = $.map(parent.parseResult.suggestKeywords, function (keyword) {
-        return new Suggestion(parent.parseResult.lowerCase ? keyword.value.toLowerCase() : keyword.value, AutocompleterGlobals.i18n.meta.keyword, keyword.weight);
-      });
-      self.suggestions(keywordSuggestions);
-    }
+    self.keywords = ko.observableArray();
+    self.loadingKeywords = ko.observable(false);
 
-    if (parent.parseResult.suggestColRefKeywords) {
-      self.loading(true);
-      // We have to wait for the column reference type to be resolved
-      colRefDeferral.done(function (colRef) {
-        if (colRef !== null) {
-          var keywordSuggestions = self.suggestions();
-          Object.keys(parent.parseResult.suggestColRefKeywords).forEach(function (typeForKeywords) {
-            if (SqlFunctions.matchesType(sourceType, [typeForKeywords], [type.toUpperCase()])) {
-              parent.parseResult.suggestColRefKeywords[typeForKeywords].forEach(function (keyword) {
-                keywordSuggestions.push(new Suggestion(parent.parseResult.lowerCase ? keyword.toLowerCase() : keyword, AutocompleterGlobals.i18n.meta.keyword, DEFAULT_WEIGHTS.COLREF_KEYWORD));
-              })
-            }
-          });
-          self.suggestions(keywordSuggestions);
-        }
-        self.loading(false);
-      });
-    }
-  }
+    self.tables = ko.observableArray();
+    self.loadingTables = ko.observable(false);
 
-  /**
-   * @param parent
-   * @constructor
-   */
-  function TablesCategory (parent) {
-    var self = this;
-    self.label = AutocompleterGlobals.i18n.keywords;
-    self.suggestions = ko.observableArray([]);
-    self.loading = ko.observable(true);
+    self.columns = ko.observableArray();
+    self.loadingColumns = ko.observable(false);
 
-    var prefix = parent.parseResult.suggestTables.prependQuestionMark ? '? ' : '';
-    if (parent.parseResult.suggestTables.prependFrom) {
-      prefix += parent.parseResult.lowerCase ? 'from ' : 'FROM ';
-    }
+    self.filter = ko.observable();
 
-    parent.apiHelper.fetchTables({
-      sourceType: parent.sourceType,
-      databaseName: parent.parseResult.suggestTables.identifierChain && parent.parseResult.suggestTables.identifierChain.length === 1 ? parent.parseResult.suggestTables.identifierChain[0].name : parent.defaultDatabase,
-      successCallback: function (data) {
-        var tables = [];
-        data.tables_meta.forEach(function (tablesMeta) {
-          if (parent.parseResult.suggestTables.onlyTables && tablesMeta.type.toLowerCase() !== 'table' ||
-              parent.parseResult.suggestTables.onlyViews && tablesMeta.type.toLowerCase() !== 'view') {
-            return;
+    self.filtered = ko.pureComputed(function () {
+      var result = self.keywords().concat(self.tables(), self.columns());
+
+      if (self.filter()) {
+        var lowerCaseFilter = self.filter().toLowerCase();
+        result = result.filter(function (suggestion) {
+          // TODO: Extend with fuzzy matches
+          var foundIndex = suggestion.value.toLowerCase().indexOf(lowerCaseFilter);
+          if (foundIndex === -1) {
+            return false;
+          }
+          if (foundIndex === 0) {
+            suggestion.sortWeight = 2;
+          } else if (foundIndex > 0) {
+            suggestion.sortWeight = 1;
           }
-          tables.push(new Suggestion(prefix + backTickIfNeeded(parent.sourceType, tablesMeta.name), AutocompleterGlobals.i18n.meta[tablesMeta.type.toLowerCase()], DEFAULT_WEIGHTS.TABLE));
+          suggestion.matchIndex = foundIndex;
+          suggestion.matchLength = self.filter().length;
+          return true;
         });
-        self.loading(false);
-        self.suggestions(tables);
-      },
-      silenceErrors: true,
-      errorCallback: function () {
-        self.loading(false);
-      },
-      timeout: parent.timeout
+      }
+      result.sort(function (a, b) {
+        if (self.filter()) {
+          if (typeof a.sortWeight !== 'undefined' && typeof b.sortWeight !== 'undefined' && b.sortWeight !== a.sortWeight) {
+            return b.sortWeight - a.sortWeight;
+          }
+          if (typeof a.sortWeight !== 'undefined' && typeof b.sortWeight === 'undefined') {
+            return -1;
+          }
+          if (typeof a.sortWeight === 'undefined' && typeof b.sortWeight !== 'undefined') {
+            return 1;
+          }
+        }
+        if (typeof a.weight !== 'undefined' && typeof b.weight !== 'undefined' && b.weight !== a.weight) {
+          return b.weight - a.weight;
+        }
+        if (typeof a.weight !== 'undefined' && typeof b.weight === 'undefined') {
+          return -1;
+        }
+        if (typeof a.weight === 'undefined' && typeof b.weight !== 'undefined') {
+          return 1;
+        }
+        return a.value.localeCompare(b.value);
+      });
+      return result;
     });
   }
 
-  /**
-   *
-   * @param options
-   * @constructor
-   */
-  function Suggestions (options) {
+  Suggestions.prototype.update = function (parseResult) {
     var self = this;
-    self.apiHelper = ApiHelper.getInstance();
-    self.parseResult = options.parseResult;
-    self.sourceType = options.sourceType;
-    self.defaultDatabase = options.defaultDatabase;
-    self.timeout = options.timeout;
-    self.callback = options.callback;
+    self.defaultDatabase = parseResult.useDatabase || self.snippet.database();
+    self.parseResult = parseResult;
+    self.keywords([]);
+    self.tables([]);
+    self.columns([]);
+    self.loadingKeywords(false);
+    self.loadingTables(false);
+    self.loadingColumns(false);
+    self.filter('');
 
     var colRefDeferral = self.handleColumnReference();
-    var allDbsDeferral = self.loadAllDatabases();
+    var allDbsDeferral = self.loadDatabases();
 
     self.handleKeywords(colRefDeferral);
     self.handleTables(allDbsDeferral);
+  };
 
-    colRefDeferral.done(self.callback)
-  }
-
-  Suggestions.prototype.loadAllDatabases = function () {
+  Suggestions.prototype.loadDatabases = function () {
     var self = this;
     var dbsDeferral = $.Deferred();
     self.apiHelper.loadDatabases({
-      sourceType: self.sourceType,
+      sourceType: self.snippet.type(),
       successCallback: dbsDeferral.resolve,
-      timeout: self.timeout,
+      timeout: AUTOCOMPLETE_TIMEOUT,
       silenceErrors: true,
       errorCallback: function () {
         dbsDeferral.resolve([]);
@@ -217,31 +203,84 @@ var SqlAutocompleter3 = (function () {
 
   Suggestions.prototype.handleKeywords = function (colRefDeferral) {
     var self = this;
-    if (self.parseResult.suggestKeywords || self.parseResult.suggestColRefKeywords) {
-      self.keywords = new KeywordsCategory(self, colRefDeferral);
+    if (self.parseResult.suggestKeywords) {
+      var keywordSuggestions = $.map(self.parseResult.suggestKeywords, function (keyword) {
+        return new Suggestion(self.parseResult.lowerCase ? keyword.value.toLowerCase() : keyword.value, AutocompleterGlobals.i18n.meta.keyword, keyword.weight);
+      });
+      self.keywords(keywordSuggestions);
+    }
+
+    if (self.parseResult.suggestColRefKeywords) {
+      self.loadingKeywords(true);
+      // Wait for the column reference type to be resolved to pick the right keywords
+      colRefDeferral.done(function (colRef) {
+        if (colRef !== null) {
+          var keywordSuggestions = self.keywords();
+          Object.keys(self.parseResult.suggestColRefKeywords).forEach(function (typeForKeywords) {
+            if (SqlFunctions.matchesType(sourceType, [typeForKeywords], [type.toUpperCase()])) {
+              self.parseResult.suggestColRefKeywords[typeForKeywords].forEach(function (keyword) {
+                keywordSuggestions.push(new Suggestion(self.parseResult.lowerCase ? keyword.toLowerCase() : keyword, AutocompleterGlobals.i18n.meta.keyword, DEFAULT_WEIGHTS.COLREF_KEYWORD));
+              })
+            }
+          });
+          self.keywords(keywordSuggestions);
+        }
+        self.loadingKeywords(false);
+      });
     }
   };
 
   Suggestions.prototype.handleTables = function (allDbsDeferral, colRefDeferral) {
     var self = this;
     if (self.parseResult.suggestTables) {
-      if (self.sourceType == 'impala' && self.parseResult.suggestTables.identifierChain && self.parseResult.suggestTables.identifierChain.length === 1) {
+      var suggestTables = self.parseResult.suggestTables;
+      var fetchTables = function () {
+        self.loadingTables(true);
+        var prefix = suggestTables.prependQuestionMark ? '? ' : '';
+        if (suggestTables.prependFrom) {
+          prefix += self.parseResult.lowerCase ? 'from ' : 'FROM ';
+        }
+
+        self.apiHelper.fetchTables({
+          sourceType: self.snippet.type(),
+          databaseName: suggestTables.identifierChain && suggestTables.identifierChain.length === 1 ? suggestTables.identifierChain[0].name : self.defaultDatabase,
+          successCallback: function (data) {
+            var tableSuggestions = [];
+            data.tables_meta.forEach(function (tablesMeta) {
+              if (suggestTables.onlyTables && tablesMeta.type.toLowerCase() !== 'table' ||
+                  suggestTables.onlyViews && tablesMeta.type.toLowerCase() !== 'view') {
+                return;
+              }
+              tableSuggestions.push(new Suggestion(prefix + backTickIfNeeded(self.snippet.type(), tablesMeta.name), AutocompleterGlobals.i18n.meta[tablesMeta.type.toLowerCase()], DEFAULT_WEIGHTS.TABLE));
+            });
+            self.loadingTables(false);
+            self.tables(tableSuggestions);
+          },
+          silenceErrors: true,
+          errorCallback: function () {
+            self.loadingTables(false);
+          },
+          timeout: AUTOCOMPLETE_TIMEOUT
+        });
+      };
+
+      if (self.snippet.type() == 'impala' && self.parseResult.suggestTables.identifierChain && self.parseResult.suggestTables.identifierChain.length === 1) {
         allDbsDeferral.done(function (databases) {
           var foundDb = databases.filter(function (db) {
             return db.toLowerCase() === self.parseResult.suggestTables.identifierChain[0].name.toLowerCase();
           });
           if (foundDb.length > 0) {
-            self.tables = new TablesCategory(self);
+            fetchTables();
           } else {
             self.parseResult.suggestColumns = { tables: [{ identifierChain: self.parseResult.suggestTables.identifierChain }] };
             self.handleColumns(colRefDeferral);
           }
         });
-      } else if (self.sourceType == 'impala' && self.parseResult.suggestTables.identifierChain && self.parseResult.suggestTables.identifierChain.length > 1) {
+      } else if (self.snippet.type() == 'impala' && self.parseResult.suggestTables.identifierChain && self.parseResult.suggestTables.identifierChain.length > 1) {
         self.parseResult.suggestColumns = { tables: [{ identifierChain: self.parseResult.suggestTables.identifierChain }] };
         self.handleColumns(colRefDeferral);
       } else {
-        self.tables = new TablesCategory(self);
+        fetchTables();
       }
     }
   };
@@ -282,7 +321,7 @@ var SqlAutocompleter3 = (function () {
         });
       }
     } else {
-      colRefDeferral.resolve();
+      colRefDeferral.resolve({ type: 'T' });
     }
     return colRefDeferral;
   };
@@ -316,13 +355,13 @@ var SqlAutocompleter3 = (function () {
       }
 
       self.apiHelper.fetchFields({
-        sourceType: self.sourceType,
+        sourceType: self.snippet.type(),
         databaseName: database,
         tableName: table,
         fields: fetchedFields,
-        timeout: self.timeout,
+        timeout: AUTOCOMPLETE_TIMEOUT,
         successCallback: function (data) {
-          if (self.sourceType === 'hive'
+          if (self.snippet.type() === 'hive'
               && typeof data.extended_columns !== 'undefined'
               && data.extended_columns.length === 1
               && data.extended_columns.length
@@ -356,11 +395,11 @@ var SqlAutocompleter3 = (function () {
 
     // For Hive it could be either:
     // SELECT col.struct FROM db.tbl -or- SELECT col.struct FROM tbl
-    if (self.sourceType === 'impala' || self.sourceType === 'hive') {
+    if (self.snippet.type() === 'impala' || self.snippet.type() === 'hive') {
       if (identifierChain.length > 1) {
         self.apiHelper.loadDatabases({
-          sourceType: self.sourceType,
-          timeout: self.timeout,
+          sourceType: self.snippet.type(),
+          timeout: AUTOCOMPLETE_TIMEOUT,
           successCallback: function (data) {
             var foundDb = data.filter(function (db) {
               return db.toLowerCase() === identifierChain[0].name.toLowerCase();
@@ -387,30 +426,24 @@ var SqlAutocompleter3 = (function () {
   /**
    * @param {Object} options
    * @param {Snippet} options.snippet
-   * @param {Number} options.timeout
    * @constructor
    */
   function SqlAutocompleter3(options) {
     var self = this;
     self.snippet = options.snippet;
-    self.timeout = options.timeout;
+    self.editor = options.editor;
+    self.suggestions = new Suggestions(options);
   }
 
-  SqlAutocompleter3.prototype.autocomplete = function (beforeCursor, afterCursor) {
+  SqlAutocompleter3.prototype.autocomplete = function () {
     var self = this;
-    var sourceType = self.snippet.type();
-    var parseResult = sql.parseSql(beforeCursor, afterCursor, sourceType, false);
+    var parseResult = sql.parseSql(self.editor().getTextBeforeCursor(), self.editor().getTextAfterCursor(), self.snippet.type(), false);
 
     if (typeof hueDebug !== 'undefined' && hueDebug.showParseResult) {
       console.log(parseResult);
     }
 
-    return new Suggestions({
-      parseResult: parseResult,
-      sourceType: sourceType,
-      defaultDatabase: parseResult.useDatabase || self.snippet.database(),
-      timeout: self.timeout
-    });
+    self.suggestions.update(parseResult);
   };
 
   return SqlAutocompleter3;

+ 2 - 0
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -1656,6 +1656,8 @@ from notebook.conf import ENABLE_QUERY_BUILDER, ENABLE_QUERY_SCHEDULING, ENABLE_
           minLines: $root.editorMode() ? null : 3
         }
       }, style: {opacity: statementType() !== 'text' ? '0.75' : '1', 'top': $root.editorMode() && statementType() !== 'text' ? '60px' : '0'}"></div>
+      <!-- ko component: { name: 'hueAceAutocompleter', params: { editor: ace, snippet: $data } } --><!-- /ko -->
+
       <ul class="table-drop-menu hue-context-menu">
         <li class="editor-drop-value"><a href="javascript:void(0);">"<span class="editor-drop-identifier"></span>"</a></li>
         <li class="divider"></li>

+ 82 - 108
desktop/libs/notebook/src/notebook/templates/hue_ace_autocompleter.mako

@@ -22,7 +22,7 @@ from desktop.views import _ko
 <%def name="hueAceAutocompleter()">
   <style>
     .hue-ace-autocompleter {
-      position: absolute;
+      position: fixed;
       z-index: 100000;
       width: 500px;
       height: 250px;
@@ -65,15 +65,19 @@ from desktop.views import _ko
 
   </style>
   <script type="text/html" id="hue-ace-autocompleter">
-    <div class="autocompleter-header"><div class="pull-right">header</div></div>
-    <div class="autocompleter-list" data-bind="foreach: filteredSuggestions">
-      <div data-bind="click: function () { $parent.selectedIndex($index()); $parent.insertSuggestion(); $parent.editor.focus(); }, css: { 'selected': $index() === $parent.selectedIndex() }"><div class="pull-left" data-bind="matchedText: { suggestion: $data, filter: $parent.prefixFilter }"></div><div class="pull-right" data-bind="text: meta"></div></div>
+    <!-- ko if: active -->
+    <div class="hue-ace-autocompleter" data-bind="style: { top: top() + 'px', left: left() + 'px' }">
+      <div class="autocompleter-header"><div class="pull-right">header</div></div>
+      <div class="autocompleter-list" data-bind="foreach: suggestions.filtered">
+        <div data-bind="click: function () { $parent.selectedIndex($index()); $parent.insertSuggestion(); $parent.editor().focus(); }, css: { 'selected': $index() === $parent.selectedIndex() }"><div class="pull-left" data-bind="matchedText: { suggestion: $data, filter: $parent.suggestions.filter }"></div><div class="pull-right" data-bind="text: meta"></div></div>
+      </div>
     </div>
     <!-- /ko -->
   </script>
 
   <script type="text/javascript" charset="utf-8">
     (function () {
+
       var aceUtil = ace.require('ace/autocomplete/util');
       var HashHandler = ace.require('ace/keyboard/hash_handler').HashHandler;
 
@@ -98,84 +102,27 @@ from desktop.views import _ko
 
       function HueAceAutocompleter (params, element) {
         var self = this;
-        var snippet = params.snippet;
         self.editor = params.editor;
+        self.snippet = params.snippet;
 
-        var autocompleter = new SqlAutocompleter3({ snippet: snippet, timeout: AUTOCOMPLETE_TIMEOUT });
-        self.suggestions = autocompleter.autocomplete(self.editor.getTextBeforeCursor(), self.editor.getTextAfterCursor());
-
-        var session = self.editor.getSession();
-        var pos = self.editor.getCursorPosition();
+        self.autocompleter = new SqlAutocompleter3(params);
+        self.suggestions = self.autocompleter.suggestions;
 
-        var line = session.getLine(pos.row);
-        var prefix = aceUtil.retrievePrecedingIdentifier(line, pos.column);
-        self.base = session.doc.createAnchor(pos.row, pos.column - prefix.length);
-        self.base.$insertRight = true;
+        self.active = ko.observable(false);
+        self.top = ko.observable(1);
+        self.left = ko.observable(1);
 
-        self.prefixFilter = ko.observable(prefix.toLowerCase());
+        self.selectedIndex = ko.observable(0);
 
-        self.activeSuggestions = ko.pureComputed(function () {
-          var result = [];
-          if (self.suggestions.keywords) {
-            result = result.concat(self.suggestions.keywords.suggestions());
+        self.suggestions.filtered.subscribe(function (newValue) {
+          if (self.selectedIndex() > newValue.length - 1) {
+            self.selectedIndex(Math.max(0, newValue.length -1));
           }
-          if (self.suggestions.tables) {
-            result = result.concat(self.suggestions.tables.suggestions());
+          if (newValue.length === 0) {
+            self.detach();
           }
-          return result;
-        });
-
-        self.filteredSuggestions = ko.pureComputed(function () {
-          var result = [];
-          if (self.prefixFilter()) {
-            result = self.activeSuggestions().filter(function (suggestion) {
-              // TODO: Extend with fuzzy matches
-              var foundIndex = suggestion.value.toLowerCase().indexOf(self.prefixFilter());
-              if (foundIndex === 0) {
-                suggestion.sortWeight = 2;
-              } else if (foundIndex > 0) {
-                suggestion.sortWeight = 1;
-              }
-              suggestion.matchIndex = foundIndex;
-              suggestion.matchLength = self.prefixFilter().length;
-              return suggestion.value.toLowerCase().indexOf(self.prefixFilter()) !== -1;
-            });
-
-            if (self.selectedIndex() > result.length - 1) {
-              self.selectedIndex(result.length - 1);
-            }
-          } else {
-            result = self.activeSuggestions();
-          }
-
-          result.sort(function (a, b) {
-            if (self.prefixFilter()) {
-              if (typeof a.sortWeight !== 'undefined' && typeof b.sortWeight !== 'undefined' && b.sortWeight !== a.sortWeight) {
-                return b.sortWeight - a.sortWeight;
-              }
-              if (typeof a.sortWeight !== 'undefined' && typeof b.sortWeight === 'undefined') {
-                return -1;
-              }
-              if (typeof b.sortWeight !== 'undefined' && typeof a.sortWeight === 'undefined') {
-                return 1;
-              }
-            }
-            if (typeof a.weight !== 'undefined' && typeof b.weight !== 'undefined' && b.weight !== a.weight) {
-              return b.weight - a.weight;
-            }
-            if (typeof a.weight !== 'undefined' && typeof b.weight === 'undefined') {
-              return -1;
-            }
-            if (typeof b.weight !== 'undefined' && typeof a.weight === 'undefined') {
-              return 1;
-            }
-            return a.value.localeCompare(b.value);
-          });
-          return result;
         });
 
-        self.selectedIndex = ko.observable(0);
-
         self.keyboardHandler = new HashHandler();
         self.keyboardHandler.bindKeys({
           'Up': function() {
@@ -185,7 +132,7 @@ from desktop.views import _ko
             }
           },
           'Down': function() {
-            if (self.selectedIndex() < self.filteredSuggestions().length - 1) {
+            if (self.selectedIndex() < self.suggestions.filtered().length - 1) {
               self.selectedIndex(self.selectedIndex() + 1);
               self.scrollSelectionIntoView();
             }
@@ -195,13 +142,13 @@ from desktop.views import _ko
             self.scrollSelectionIntoView();
           },
           'Ctrl-Down|Ctrl-End': function() {
-            if (self.filteredSuggestions().length > 0 ) {
-              self.selectedIndex(self.filteredSuggestions().length - 1);
+            if (self.suggestions.filtered().length > 0 ) {
+              self.selectedIndex(self.suggestions.filtered().length - 1);
               self.scrollSelectionIntoView();
             }
           },
           'Esc': function() {
-            self.destroy();
+            self.detach();
           },
           'Return': function() {
             self.insertSuggestion();
@@ -219,69 +166,96 @@ from desktop.views import _ko
 
         self.changeListener = function () {
           window.clearTimeout(changeTimeout);
-          var cursor = self.editor.selection.lead;
+          var cursor = self.editor().selection.lead;
           if (cursor.row != self.base.row || cursor.column < self.base.column) {
-            self.destroy();
+            self.detach();
           } else {
             changeTimeout = window.setTimeout(function () {
-              self.prefixFilter(self.editor.session.getTextRange({ start: self.base, end: self.editor.getCursorPosition() }).toLowerCase())
+              self.suggestions.filter(self.editor().session.getTextRange({ start: self.base, end: self.editor().getCursorPosition() }));
             }, 200);
           }
         };
 
         self.mousedownListener = function () {
-          self.destroy();
+          self.detach();
         };
 
         self.mousewheelListener = function () {
-          self.destroy();
+          self.detach();
         };
 
         var closeOnClickOutside = function (event) {
           if ($.contains(document, event.target) && !$.contains(element, event.target)) {
-            self.destroy();
+            self.detach();
           }
         };
 
-        var pubSubDestroy = huePubSub.subscribe('hue.ace.autocompleter.hide', function () {
-          self.destroy()
-        });
-
-        self.destroy = function () {
+        self.detach = function () {
+          self.active(false);
           self.base.detach();
+          self.base = null;
           window.clearTimeout(changeTimeout);
           $(document).off('click', closeOnClickOutside);
-          self.editor.keyBinding.removeKeyboardHandler(self.keyboardHandler);
-          self.editor.off('changeSelection', self.changeListener);
-          self.editor.off('mousedown', self.mousedownListener);
-          self.editor.off('mousewheel', self.mousewheelListener);
-          $(element).remove();
-          pubSubDestroy.remove();
+          self.editor().keyBinding.removeKeyboardHandler(self.keyboardHandler);
+          self.editor().off('changeSelection', self.changeListener);
+          self.editor().off('mousedown', self.mousedownListener);
+          self.editor().off('mousewheel', self.mousewheelListener);
         };
 
-        $(document).on('click', closeOnClickOutside);
-        self.editor.keyBinding.addKeyboardHandler(self.keyboardHandler);
-        self.editor.on('changeSelection', self.changeListener);
-        self.editor.on('mousedown', self.mousedownListener);
-        self.editor.on('mousewheel', self.mousewheelListener);
+        self.attach = function () {
+          $(document).on('click', closeOnClickOutside);
+          self.editor().keyBinding.addKeyboardHandler(self.keyboardHandler);
+          self.editor().on('changeSelection', self.changeListener);
+          self.editor().on('mousedown', self.mousedownListener);
+          self.editor().on('mousewheel', self.mousewheelListener);
+        };
+
+        huePubSub.subscribe('hue.ace.autocompleter.show', function (data) {
+          var session = self.editor().getSession();
+          var pos = self.editor().getCursorPosition();
+          var line = session.getLine(pos.row);
+          var prefix = aceUtil.retrievePrecedingIdentifier(line, pos.column);
+          var newBase = session.doc.createAnchor(pos.row, pos.column - prefix.length);
+          self.top(data.position.top + data.lineHeight + 3);
+          self.left(data.position.left);
+          if (self.active()) {
+            if (!self.base || newBase.column !== self.base.column || newBase.row !== self.base.row) {
+              self.autocompleter.autocomplete();
+            }
+            self.detach();
+          } else {
+            self.autocompleter.autocomplete();
+          }
+          newBase.$insertRight = true;
+          self.base = newBase;
+          self.suggestions.filter(prefix);
+          self.active(true);
+          self.attach();
+          self.selectedIndex(0);
+        });
+
+        huePubSub.subscribe('hue.ace.autocompleter.hide', function () {
+          self.detach();
+        });
+
       }
 
       HueAceAutocompleter.prototype.insertSuggestion = function () {
         var self = this;
-        if (self.filteredSuggestions().length === 0) {
-          self.destroy();
+        if (self.suggestions.filtered().length === 0) {
+          self.detach();
           return;
         }
-        if (self.prefixFilter()) {
-          var ranges = self.editor.selection.getAllRanges();
+        if (self.suggestions.filter()) {
+          var ranges = self.editor().selection.getAllRanges();
           for (var i = 0, range; range = ranges[i]; i++) {
-            range.start.column -= self.prefixFilter().length;
-            self.editor.session.remove(range);
+            range.start.column -= self.suggestions.filter().length;
+            self.editor().session.remove(range);
           }
         }
-        self.editor.execCommand('insertstring', self.filteredSuggestions()[self.selectedIndex()].value);
-        self.editor.renderer.scrollCursorIntoView();
-        self.destroy();
+        self.editor().execCommand('insertstring', self.suggestions.filtered()[self.selectedIndex()].value);
+        self.editor().renderer.scrollCursorIntoView();
+        self.detach();
       };
 
       HueAceAutocompleter.prototype.scrollSelectionIntoView = function () {