Bug 944499 - Make the css autocompletion faster, r=harth
authorGirish Sharma <scrapmachines@gmail.com>
Thu, 13 Mar 2014 22:49:54 +0530
changeset 173484 00a016605d9e874d716f526f341582f5aab66806
parent 173483 75f3bd5737a6704c456535f206de6c09adbd9381
child 173485 1d0e1126f1da716df608871bcf88fd5397486e92
push id26406
push userkwierso@gmail.com
push dateFri, 14 Mar 2014 02:09:34 +0000
treeherdermozilla-central@f073b3d6db1f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs944499
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 944499 - Make the css autocompletion faster, r=harth
browser/devtools/sourceeditor/autocomplete.js
browser/devtools/sourceeditor/css-autocompleter.js
--- a/browser/devtools/sourceeditor/autocomplete.js
+++ b/browser/devtools/sourceeditor/autocomplete.js
@@ -55,20 +55,20 @@ function setupAutoCompletion(ctx, walker
       }
 
       return win.CodeMirror.Pass;
     }
   };
   keyMap[Editor.accel("Space")] = cm => autoComplete(ctx);
   cm.addKeyMap(keyMap);
 
-  cm.on("keydown", (cm, e) => onEditorKeypress(ed, e));
+  cm.on("keydown", (cm, e) => onEditorKeypress(ctx, e));
   ed.on("change", () => autoComplete(ctx));
   ed.on("destroy", () => {
-    cm.off("keydown", (cm, e) => onEditorKeypress(ed, e));
+    cm.off("keydown", (cm, e) => onEditorKeypress(ctx, e));
     ed.off("change", () => autoComplete(ctx));
     popup.destroy();
     popup = null;
     completer = null;
   });
 
   privates.set(ed, {
     popup: popup,
@@ -150,28 +150,34 @@ function cycleSuggestions(ed, reverse) {
   // This event is used in tests.
   ed.emit("suggestion-entered");
 }
 
 /**
  * onkeydown handler for the editor instance to prevent autocompleting on some
  * keypresses.
  */
-function onEditorKeypress(ed, event) {
+function onEditorKeypress({ ed, Editor }, event) {
   let private = privates.get(ed);
   switch (event.keyCode) {
     case event.DOM_VK_ESCAPE:
       if (private.popup.isOpen)
         event.preventDefault();
     case event.DOM_VK_LEFT:
     case event.DOM_VK_RIGHT:
     case event.DOM_VK_HOME:
     case event.DOM_VK_END:
+      private.doNotAutocomplete = true;
+      private.popup.hidePopup();
+      break;
+
     case event.DOM_VK_BACK_SPACE:
     case event.DOM_VK_DELETE:
+      if (ed.config.mode == Editor.modes.css)
+        private.completer.invalidateCache(ed.getCursor().line)
       private.doNotAutocomplete = true;
       private.popup.hidePopup();
       break;
 
     default:
       private.doNotAutocomplete = false;
   }
 }
--- a/browser/devtools/sourceeditor/css-autocompleter.js
+++ b/browser/devtools/sourceeditor/css-autocompleter.js
@@ -79,16 +79,20 @@ const { properties, propertyNames } = ge
  * @param options {Object} An options object containing the following options:
  *        - walker {Object} The object used for query selecting from the current
  *                 target's DOM.
  *        - maxEntries {Number} Maximum selectors suggestions to display.
  */
 function CSSCompleter(options = {}) {
   this.walker = options.walker;
   this.maxEntries = options.maxEntries || 15;
+
+  // Array containing the [line, ch, scopeStack] for the locations where the
+  // CSS state is "null"
+  this.nullStates = [];
 }
 
 CSSCompleter.prototype = {
 
   /**
    * Returns a list of suggestions based on the caret position.
    *
    * @param source {String} String of the source code.
@@ -145,39 +149,56 @@ CSSCompleter.prototype = {
    * @param caret {Object} Cursor location with line and ch properties.
    *
    * @returns CSS_STATE
    *          One of CSS_STATE enum or null if the state cannot be resolved.
    */
   resolveState: function(source, {line, ch}) {
     // Function to return the last element of an array
     let peek = arr => arr[arr.length - 1];
+    // _state can be one of CSS_STATES;
+    let _state = CSS_STATES.null;
+    let selector = "";
+    let selectorState = SELECTOR_STATES.null;
+    let propertyName = null;
+    let scopeStack = [];
+
+    // Fetch the closest null state line, ch from cached null state locations
+    let matchedStateIndex = this.findNearestNullState(line);
+    if (matchedStateIndex > -1) {
+      let state = this.nullStates[matchedStateIndex];
+      line -= state[0];
+      if (line == 0)
+        ch -= state[1];
+      source = source.split("\n").slice(state[0]);
+      source[0] = source[0].slice(state[1]);
+      source = source.join("\n");
+      scopeStack = [...state[2]];
+      this.nullStates.length = matchedStateIndex + 1;
+    }
+    else {
+      this.nullStates = [];
+    }
     let tokens = cssTokenizer(source, {loc:true});
     let tokIndex = tokens.length - 1;
     if (tokens[tokIndex].loc.end.line < line ||
        (tokens[tokIndex].loc.end.line === line &&
         tokens[tokIndex].loc.end.column < ch)) {
       // If the last token is not an EOF, we didn't tokenize it correctly.
       // This special case is handled in case we couldn't tokenize, but the last
       // token that *could be tokenized* was an identifier.
       return null;
     }
     // Since last token is EOF, the cursor token is last - 1
     tokIndex--;
 
-    // _state can be one of CSS_STATES;
-    let _state = CSS_STATES.null;
     let cursor = 0;
     // This will maintain a stack of paired elements like { & }, @m & }, : & ; etc
-    let scopeStack = [];
     let token = null;
-    let propertyName = null;
-    let selector = "";
     let selectorBeforeNot = "";
-    let selectorState = SELECTOR_STATES.null;
     while (cursor <= tokIndex && (token = tokens[cursor++])) {
       switch (_state) {
         case CSS_STATES.property:
           // From CSS_STATES.property, we can either go to CSS_STATES.value state
           // when we hit the first ':' or CSS_STATES.selector if "}" is reached.
           switch(token.tokenType) {
             case ":":
               scopeStack.push(":");
@@ -596,16 +617,30 @@ CSSCompleter.prototype = {
           } else if (token.tokenType == "}") {
             if (peek(scopeStack) == "@k")
               scopeStack.pop();
 
             _state = CSS_STATES.null;
           }
           break;
       }
+      if (_state == CSS_STATES.null) {
+        if (this.nullStates.length == 0) {
+          this.nullStates.push([token.loc.end.line, token.loc.end.column,
+                                [...scopeStack]]);
+          continue;
+        }
+        let tokenLine = token.loc.end.line;
+        let tokenCh = token.loc.end.column;
+        if (tokenLine == 0)
+          continue;
+        if (matchedStateIndex > -1)
+          tokenLine += this.nullStates[matchedStateIndex][0];
+        this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
+      }
     }
     this.state = _state;
     if (!token)
       return _state;
 
     if (token && token.tokenType != "WHITESPACE") {
       this.completing = ((token.value || token.repr || token.tokenType) + "")
                           .slice(0, ch - token.loc.start.column)
@@ -763,16 +798,70 @@ CSSCompleter.prototype = {
         });
       } else if (list[i] > startValue) {
         // We have crossed all possible matches alphabetically.
         break;
       }
     }
     return Promise.resolve(finalList);
   },
+
+  /**
+   * A biased binary search in a sorted array where the middle element is
+   * calculated based on the values at the lower and the upper index in each
+   * iteration.
+   *
+   * This method returns the index of the closest null state from the passed
+   * `line` argument. Once we have the closest null state, we can start applying
+   * the state machine logic from that location instead of the absolute starting
+   * of the CSS source. This speeds up the tokenizing and the state machine a
+   * lot while using autocompletion at high line numbers in a CSS source.
+   */
+  findNearestNullState: function(line) {
+    let arr = this.nullStates;
+    let high = arr.length - 1;
+    let low = 0;
+    let target = 0;
+
+    if (high < 0)
+      return -1;
+    if (arr[high][0] <= line)
+      return high;
+    if (arr[low][0] > line)
+      return -1;
+
+    while (high > low) {
+      if (arr[low][0] <= line && arr[low [0]+ 1] > line)
+        return low;
+      if (arr[high][0] > line && arr[high - 1][0] <= line)
+        return high - 1;
+
+      target = (((line - arr[low][0]) / (arr[high][0] - arr[low][0])) *
+                (high - low)) | 0;
+
+      if (arr[target][0] <= line && arr[target + 1][0] > line) {
+        return target;
+      } else if (line > arr[target][0]) {
+        low = target + 1;
+        high--;
+      } else {
+        high = target - 1;
+        low++;
+      }
+    }
+
+    return -1;
+  },
+
+  /**
+   * Invalidates the state cache for and above the line.
+   */
+  invalidateCache: function(line) {
+    this.nullStates.length = this.findNearestNullState(line) + 1;
+  }
 }
 
 /**
  * Returns a list of all property names and a map of property name vs possible
  * CSS values provided by the Gecko engine.
  *
  * @return {Object} An object with following properties:
  *         - propertyNames {Array} Array of string containing all the possible