forked from mirrors/gecko-dev
		
	Depends on D67603 This patch is an automated string replace of shared/fronts/ to client/fronts/ in devtools. Differential Revision: https://phabricator.services.mozilla.com/D67604 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			1252 lines
		
	
	
	
		
			42 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1252 lines
		
	
	
	
		
			42 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
const {
 | 
						|
  cssTokenizer,
 | 
						|
  cssTokenizerWithLineColumn,
 | 
						|
} = require("devtools/shared/css/parsing-utils");
 | 
						|
const {
 | 
						|
  getClientCssProperties,
 | 
						|
} = require("devtools/client/fronts/css-properties");
 | 
						|
 | 
						|
/**
 | 
						|
 * Here is what this file (+ css-parsing-utils.js) do.
 | 
						|
 *
 | 
						|
 * The main objective here is to provide as much suggestions to the user editing
 | 
						|
 * a stylesheet in Style Editor. The possible things that can be suggested are:
 | 
						|
 *  - CSS property names
 | 
						|
 *  - CSS property values
 | 
						|
 *  - CSS Selectors
 | 
						|
 *  - Some other known CSS keywords
 | 
						|
 *
 | 
						|
 * Gecko provides a list of both property names and their corresponding values.
 | 
						|
 * We take out a list of matching selectors using the Inspector actor's
 | 
						|
 * `getSuggestionsForQuery` method. Now the only thing is to parse the CSS being
 | 
						|
 * edited by the user, figure out what token or word is being written and last
 | 
						|
 * but the most difficult, what is being edited.
 | 
						|
 *
 | 
						|
 * The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens,
 | 
						|
 * each having a certain type associated with it. These tokens help us to figure
 | 
						|
 * out the currently edited word and to write a CSS state machine to figure out
 | 
						|
 * what the user is currently editing. By that, I mean, whether he is editing a
 | 
						|
 * selector or a property or a value, or even fine grained information like an
 | 
						|
 * id in the selector.
 | 
						|
 *
 | 
						|
 * The `resolveState` method iterated over the tokens spitted out by the
 | 
						|
 * tokenizer, using switch cases, follows a state machine logic and finally
 | 
						|
 * figures out these informations:
 | 
						|
 *  - The state of the CSS at the cursor (one out of CSS_STATES)
 | 
						|
 *  - The current token that is being edited `cmpleting`
 | 
						|
 *  - If the state is "selector", the selector state (one of SELECTOR_STATES)
 | 
						|
 *  - If the state is "selector", the current selector till the cursor
 | 
						|
 *  - If the state is "value", the corresponding property name
 | 
						|
 *
 | 
						|
 * In case of "value" and "property" states, we simply use the information
 | 
						|
 * provided by Gecko to filter out the possible suggestions.
 | 
						|
 * For "selector" state, we request the Inspector actor to query the page DOM
 | 
						|
 * and filter out the possible suggestions.
 | 
						|
 * For "media" and "keyframes" state, the only possible suggestions for now are
 | 
						|
 * "media" and "keyframes" respectively, although "media" can have suggestions
 | 
						|
 * like "max-width", "orientation" etc. Similarly "value" state can also have
 | 
						|
 * much better logical suggestions if we fine grain identify a sub state just
 | 
						|
 * like we do for the "selector" state.
 | 
						|
 */
 | 
						|
 | 
						|
// Autocompletion types.
 | 
						|
 | 
						|
const CSS_STATES = {
 | 
						|
  null: "null",
 | 
						|
  property: "property", // foo { bar|: … }
 | 
						|
  value: "value", // foo {bar: baz|}
 | 
						|
  selector: "selector", // f| {bar: baz}
 | 
						|
  media: "media", // @med| , or , @media scr| { }
 | 
						|
  keyframes: "keyframes", // @keyf|
 | 
						|
  frame: "frame", // @keyframs foobar { t|
 | 
						|
};
 | 
						|
 | 
						|
const SELECTOR_STATES = {
 | 
						|
  null: "null",
 | 
						|
  id: "id", // #f|
 | 
						|
  class: "class", // #foo.b|
 | 
						|
  tag: "tag", // fo|
 | 
						|
  pseudo: "pseudo", // foo:|
 | 
						|
  attribute: "attribute", // foo[b|
 | 
						|
  value: "value", // foo[bar=b|
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Constructor for the autocompletion object.
 | 
						|
 *
 | 
						|
 * @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.
 | 
						|
 *        - cssProperties {Object} The database of CSS properties.
 | 
						|
 */
 | 
						|
function CSSCompleter(options = {}) {
 | 
						|
  this.walker = options.walker;
 | 
						|
  this.maxEntries = options.maxEntries || 15;
 | 
						|
  // If no css properties database is passed in, default to the client list.
 | 
						|
  this.cssProperties = options.cssProperties || getClientCssProperties();
 | 
						|
 | 
						|
  this.propertyNames = this.cssProperties.getNames().sort();
 | 
						|
 | 
						|
  // 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.
 | 
						|
   * @param caret {Object} Cursor location with line and ch properties.
 | 
						|
   *
 | 
						|
   * @returns [{object}] A sorted list of objects containing the following
 | 
						|
   *          peroperties:
 | 
						|
   *          - label {String} Full keyword for the suggestion
 | 
						|
   *          - preLabel {String} Already entered part of the label
 | 
						|
   */
 | 
						|
  complete: function(source, caret) {
 | 
						|
    // Getting the context from the caret position.
 | 
						|
    if (!this.resolveState(source, caret)) {
 | 
						|
      // We couldn't resolve the context, we won't be able to complete.
 | 
						|
      return Promise.resolve([]);
 | 
						|
    }
 | 
						|
 | 
						|
    // Properly suggest based on the state.
 | 
						|
    switch (this.state) {
 | 
						|
      case CSS_STATES.property:
 | 
						|
        return this.completeProperties(this.completing);
 | 
						|
 | 
						|
      case CSS_STATES.value:
 | 
						|
        return this.completeValues(this.propertyName, this.completing);
 | 
						|
 | 
						|
      case CSS_STATES.selector:
 | 
						|
        return this.suggestSelectors();
 | 
						|
 | 
						|
      case CSS_STATES.media:
 | 
						|
      case CSS_STATES.keyframes:
 | 
						|
        if ("media".startsWith(this.completing)) {
 | 
						|
          return Promise.resolve([
 | 
						|
            {
 | 
						|
              label: "media",
 | 
						|
              preLabel: this.completing,
 | 
						|
              text: "media",
 | 
						|
            },
 | 
						|
          ]);
 | 
						|
        } else if ("keyframes".startsWith(this.completing)) {
 | 
						|
          return Promise.resolve([
 | 
						|
            {
 | 
						|
              label: "keyframes",
 | 
						|
              preLabel: this.completing,
 | 
						|
              text: "keyframes",
 | 
						|
            },
 | 
						|
          ]);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return Promise.resolve([]);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Resolves the state of CSS at the cursor location. This method implements a
 | 
						|
   * custom written CSS state machine. The various switch statements provide the
 | 
						|
   * transition rules for the state. It also finds out various informatino about
 | 
						|
   * the nearby CSS like the property name being completed, the complete
 | 
						|
   * selector, etc.
 | 
						|
   *
 | 
						|
   * @param source {String} String of the source code.
 | 
						|
   * @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.
 | 
						|
   */
 | 
						|
  // eslint-disable-next-line complexity
 | 
						|
  resolveState: function(source, { line, ch }) {
 | 
						|
    // Function to return the last element of an array
 | 
						|
    const 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 = [];
 | 
						|
    let selectors = [];
 | 
						|
 | 
						|
    // Fetch the closest null state line, ch from cached null state locations
 | 
						|
    const matchedStateIndex = this.findNearestNullState(line);
 | 
						|
    if (matchedStateIndex > -1) {
 | 
						|
      const 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 = [];
 | 
						|
    }
 | 
						|
    const tokens = cssTokenizerWithLineColumn(source);
 | 
						|
    const tokIndex = tokens.length - 1;
 | 
						|
    if (
 | 
						|
      tokIndex >= 0 &&
 | 
						|
      (tokens[tokIndex].loc.end.line < line ||
 | 
						|
        (tokens[tokIndex].loc.end.line === line &&
 | 
						|
          tokens[tokIndex].loc.end.column < ch))
 | 
						|
    ) {
 | 
						|
      // If the last token ends before the cursor location, we didn't
 | 
						|
      // tokenize it correctly.  This special case can happen if the
 | 
						|
      // final token is a comment.
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    let cursor = 0;
 | 
						|
    // This will maintain a stack of paired elements like { & }, @m & }, : & ;
 | 
						|
    // etc
 | 
						|
    let token = null;
 | 
						|
    let selectorBeforeNot = 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.
 | 
						|
          if (token.tokenType === "symbol") {
 | 
						|
            switch (token.text) {
 | 
						|
              case ":":
 | 
						|
                scopeStack.push(":");
 | 
						|
                if (tokens[cursor - 2].tokenType != "whitespace") {
 | 
						|
                  propertyName = tokens[cursor - 2].text;
 | 
						|
                } else {
 | 
						|
                  propertyName = tokens[cursor - 3].text;
 | 
						|
                }
 | 
						|
                _state = CSS_STATES.value;
 | 
						|
                break;
 | 
						|
 | 
						|
              case "}":
 | 
						|
                if (/[{f]/.test(peek(scopeStack))) {
 | 
						|
                  const popped = scopeStack.pop();
 | 
						|
                  if (popped == "f") {
 | 
						|
                    _state = CSS_STATES.frame;
 | 
						|
                  } else {
 | 
						|
                    selector = "";
 | 
						|
                    selectors = [];
 | 
						|
                    _state = CSS_STATES.null;
 | 
						|
                  }
 | 
						|
                }
 | 
						|
                break;
 | 
						|
            }
 | 
						|
          }
 | 
						|
          break;
 | 
						|
 | 
						|
        case CSS_STATES.value:
 | 
						|
          // From CSS_STATES.value, we can go to one of CSS_STATES.property,
 | 
						|
          // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
 | 
						|
          if (token.tokenType === "symbol") {
 | 
						|
            switch (token.text) {
 | 
						|
              case ";":
 | 
						|
                if (/[:]/.test(peek(scopeStack))) {
 | 
						|
                  scopeStack.pop();
 | 
						|
                  _state = CSS_STATES.property;
 | 
						|
                }
 | 
						|
                break;
 | 
						|
 | 
						|
              case "}":
 | 
						|
                if (peek(scopeStack) == ":") {
 | 
						|
                  scopeStack.pop();
 | 
						|
                }
 | 
						|
 | 
						|
                if (/[{f]/.test(peek(scopeStack))) {
 | 
						|
                  const popped = scopeStack.pop();
 | 
						|
                  if (popped == "f") {
 | 
						|
                    _state = CSS_STATES.frame;
 | 
						|
                  } else {
 | 
						|
                    selector = "";
 | 
						|
                    selectors = [];
 | 
						|
                    _state = CSS_STATES.null;
 | 
						|
                  }
 | 
						|
                }
 | 
						|
                break;
 | 
						|
            }
 | 
						|
          }
 | 
						|
          break;
 | 
						|
 | 
						|
        case CSS_STATES.selector:
 | 
						|
          // From CSS_STATES.selector, we can only go to CSS_STATES.property
 | 
						|
          // when we hit "{"
 | 
						|
          if (token.tokenType === "symbol" && token.text == "{") {
 | 
						|
            scopeStack.push("{");
 | 
						|
            _state = CSS_STATES.property;
 | 
						|
            selectors.push(selector);
 | 
						|
            selector = "";
 | 
						|
            break;
 | 
						|
          }
 | 
						|
 | 
						|
          switch (selectorState) {
 | 
						|
            case SELECTOR_STATES.id:
 | 
						|
            case SELECTOR_STATES.class:
 | 
						|
            case SELECTOR_STATES.tag:
 | 
						|
              switch (token.tokenType) {
 | 
						|
                case "hash":
 | 
						|
                case "id":
 | 
						|
                  selectorState = SELECTOR_STATES.id;
 | 
						|
                  selector += "#" + token.text;
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "symbol":
 | 
						|
                  if (token.text == ".") {
 | 
						|
                    selectorState = SELECTOR_STATES.class;
 | 
						|
                    selector += ".";
 | 
						|
                    if (
 | 
						|
                      cursor <= tokIndex &&
 | 
						|
                      tokens[cursor].tokenType == "ident"
 | 
						|
                    ) {
 | 
						|
                      token = tokens[cursor++];
 | 
						|
                      selector += token.text;
 | 
						|
                    }
 | 
						|
                  } else if (token.text == "#") {
 | 
						|
                    selectorState = SELECTOR_STATES.id;
 | 
						|
                    selector += "#";
 | 
						|
                  } else if (/[>~+]/.test(token.text)) {
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    selector += token.text;
 | 
						|
                  } else if (token.text == ",") {
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    selectors.push(selector);
 | 
						|
                    selector = "";
 | 
						|
                  } else if (token.text == ":") {
 | 
						|
                    selectorState = SELECTOR_STATES.pseudo;
 | 
						|
                    selector += ":";
 | 
						|
                    if (cursor > tokIndex) {
 | 
						|
                      break;
 | 
						|
                    }
 | 
						|
 | 
						|
                    token = tokens[cursor++];
 | 
						|
                    switch (token.tokenType) {
 | 
						|
                      case "function":
 | 
						|
                        if (token.text == "not") {
 | 
						|
                          selectorBeforeNot = selector;
 | 
						|
                          selector = "";
 | 
						|
                          scopeStack.push("(");
 | 
						|
                        } else {
 | 
						|
                          selector += token.text + "(";
 | 
						|
                        }
 | 
						|
                        selectorState = SELECTOR_STATES.null;
 | 
						|
                        break;
 | 
						|
 | 
						|
                      case "ident":
 | 
						|
                        selector += token.text;
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                  } else if (token.text == "[") {
 | 
						|
                    selectorState = SELECTOR_STATES.attribute;
 | 
						|
                    scopeStack.push("[");
 | 
						|
                    selector += "[";
 | 
						|
                  } else if (token.text == ")") {
 | 
						|
                    if (peek(scopeStack) == "(") {
 | 
						|
                      scopeStack.pop();
 | 
						|
                      selector = selectorBeforeNot + "not(" + selector + ")";
 | 
						|
                      selectorBeforeNot = null;
 | 
						|
                    } else {
 | 
						|
                      selector += ")";
 | 
						|
                    }
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                  }
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "whitespace":
 | 
						|
                  selectorState = SELECTOR_STATES.null;
 | 
						|
                  selector && (selector += " ");
 | 
						|
                  break;
 | 
						|
              }
 | 
						|
              break;
 | 
						|
 | 
						|
            case SELECTOR_STATES.null:
 | 
						|
              // From SELECTOR_STATES.null state, we can go to one of
 | 
						|
              // SELECTOR_STATES.id, SELECTOR_STATES.class or
 | 
						|
              // SELECTOR_STATES.tag
 | 
						|
              switch (token.tokenType) {
 | 
						|
                case "hash":
 | 
						|
                case "id":
 | 
						|
                  selectorState = SELECTOR_STATES.id;
 | 
						|
                  selector += "#" + token.text;
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "ident":
 | 
						|
                  selectorState = SELECTOR_STATES.tag;
 | 
						|
                  selector += token.text;
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "symbol":
 | 
						|
                  if (token.text == ".") {
 | 
						|
                    selectorState = SELECTOR_STATES.class;
 | 
						|
                    selector += ".";
 | 
						|
                    if (
 | 
						|
                      cursor <= tokIndex &&
 | 
						|
                      tokens[cursor].tokenType == "ident"
 | 
						|
                    ) {
 | 
						|
                      token = tokens[cursor++];
 | 
						|
                      selector += token.text;
 | 
						|
                    }
 | 
						|
                  } else if (token.text == "#") {
 | 
						|
                    selectorState = SELECTOR_STATES.id;
 | 
						|
                    selector += "#";
 | 
						|
                  } else if (token.text == "*") {
 | 
						|
                    selectorState = SELECTOR_STATES.tag;
 | 
						|
                    selector += "*";
 | 
						|
                  } else if (/[>~+]/.test(token.text)) {
 | 
						|
                    selector += token.text;
 | 
						|
                  } else if (token.text == ",") {
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    selectors.push(selector);
 | 
						|
                    selector = "";
 | 
						|
                  } else if (token.text == ":") {
 | 
						|
                    selectorState = SELECTOR_STATES.pseudo;
 | 
						|
                    selector += ":";
 | 
						|
                    if (cursor > tokIndex) {
 | 
						|
                      break;
 | 
						|
                    }
 | 
						|
 | 
						|
                    token = tokens[cursor++];
 | 
						|
                    switch (token.tokenType) {
 | 
						|
                      case "function":
 | 
						|
                        if (token.text == "not") {
 | 
						|
                          selectorBeforeNot = selector;
 | 
						|
                          selector = "";
 | 
						|
                          scopeStack.push("(");
 | 
						|
                        } else {
 | 
						|
                          selector += token.text + "(";
 | 
						|
                        }
 | 
						|
                        selectorState = SELECTOR_STATES.null;
 | 
						|
                        break;
 | 
						|
 | 
						|
                      case "ident":
 | 
						|
                        selector += token.text;
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                  } else if (token.text == "[") {
 | 
						|
                    selectorState = SELECTOR_STATES.attribute;
 | 
						|
                    scopeStack.push("[");
 | 
						|
                    selector += "[";
 | 
						|
                  } else if (token.text == ")") {
 | 
						|
                    if (peek(scopeStack) == "(") {
 | 
						|
                      scopeStack.pop();
 | 
						|
                      selector = selectorBeforeNot + "not(" + selector + ")";
 | 
						|
                      selectorBeforeNot = null;
 | 
						|
                    } else {
 | 
						|
                      selector += ")";
 | 
						|
                    }
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                  }
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "whitespace":
 | 
						|
                  selector && (selector += " ");
 | 
						|
                  break;
 | 
						|
              }
 | 
						|
              break;
 | 
						|
 | 
						|
            case SELECTOR_STATES.pseudo:
 | 
						|
              switch (token.tokenType) {
 | 
						|
                case "symbol":
 | 
						|
                  if (/[>~+]/.test(token.text)) {
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    selector += token.text;
 | 
						|
                  } else if (token.text == ",") {
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    selectors.push(selector);
 | 
						|
                    selector = "";
 | 
						|
                  } else if (token.text == ":") {
 | 
						|
                    selectorState = SELECTOR_STATES.pseudo;
 | 
						|
                    selector += ":";
 | 
						|
                    if (cursor > tokIndex) {
 | 
						|
                      break;
 | 
						|
                    }
 | 
						|
 | 
						|
                    token = tokens[cursor++];
 | 
						|
                    switch (token.tokenType) {
 | 
						|
                      case "function":
 | 
						|
                        if (token.text == "not") {
 | 
						|
                          selectorBeforeNot = selector;
 | 
						|
                          selector = "";
 | 
						|
                          scopeStack.push("(");
 | 
						|
                        } else {
 | 
						|
                          selector += token.text + "(";
 | 
						|
                        }
 | 
						|
                        selectorState = SELECTOR_STATES.null;
 | 
						|
                        break;
 | 
						|
 | 
						|
                      case "ident":
 | 
						|
                        selector += token.text;
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                  } else if (token.text == "[") {
 | 
						|
                    selectorState = SELECTOR_STATES.attribute;
 | 
						|
                    scopeStack.push("[");
 | 
						|
                    selector += "[";
 | 
						|
                  }
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "whitespace":
 | 
						|
                  selectorState = SELECTOR_STATES.null;
 | 
						|
                  selector && (selector += " ");
 | 
						|
                  break;
 | 
						|
              }
 | 
						|
              break;
 | 
						|
 | 
						|
            case SELECTOR_STATES.attribute:
 | 
						|
              switch (token.tokenType) {
 | 
						|
                case "symbol":
 | 
						|
                  if (/[~|^$*]/.test(token.text)) {
 | 
						|
                    selector += token.text;
 | 
						|
                    token = tokens[cursor++];
 | 
						|
                  } else if (token.text == "=") {
 | 
						|
                    selectorState = SELECTOR_STATES.value;
 | 
						|
                    selector += token.text;
 | 
						|
                  } else if (token.text == "]") {
 | 
						|
                    if (peek(scopeStack) == "[") {
 | 
						|
                      scopeStack.pop();
 | 
						|
                    }
 | 
						|
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    selector += "]";
 | 
						|
                  }
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "ident":
 | 
						|
                case "string":
 | 
						|
                  selector += token.text;
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "whitespace":
 | 
						|
                  selector && (selector += " ");
 | 
						|
                  break;
 | 
						|
              }
 | 
						|
              break;
 | 
						|
 | 
						|
            case SELECTOR_STATES.value:
 | 
						|
              switch (token.tokenType) {
 | 
						|
                case "string":
 | 
						|
                case "ident":
 | 
						|
                  selector += token.text;
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "symbol":
 | 
						|
                  if (token.text == "]") {
 | 
						|
                    if (peek(scopeStack) == "[") {
 | 
						|
                      scopeStack.pop();
 | 
						|
                    }
 | 
						|
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    selector += "]";
 | 
						|
                  }
 | 
						|
                  break;
 | 
						|
 | 
						|
                case "whitespace":
 | 
						|
                  selector && (selector += " ");
 | 
						|
                  break;
 | 
						|
              }
 | 
						|
              break;
 | 
						|
          }
 | 
						|
          break;
 | 
						|
 | 
						|
        case CSS_STATES.null:
 | 
						|
          // From CSS_STATES.null state, we can go to either CSS_STATES.media or
 | 
						|
          // CSS_STATES.selector.
 | 
						|
          switch (token.tokenType) {
 | 
						|
            case "hash":
 | 
						|
            case "id":
 | 
						|
              selectorState = SELECTOR_STATES.id;
 | 
						|
              selector = "#" + token.text;
 | 
						|
              _state = CSS_STATES.selector;
 | 
						|
              break;
 | 
						|
 | 
						|
            case "ident":
 | 
						|
              selectorState = SELECTOR_STATES.tag;
 | 
						|
              selector = token.text;
 | 
						|
              _state = CSS_STATES.selector;
 | 
						|
              break;
 | 
						|
 | 
						|
            case "symbol":
 | 
						|
              if (token.text == ".") {
 | 
						|
                selectorState = SELECTOR_STATES.class;
 | 
						|
                selector = ".";
 | 
						|
                _state = CSS_STATES.selector;
 | 
						|
                if (cursor <= tokIndex && tokens[cursor].tokenType == "ident") {
 | 
						|
                  token = tokens[cursor++];
 | 
						|
                  selector += token.text;
 | 
						|
                }
 | 
						|
              } else if (token.text == "#") {
 | 
						|
                selectorState = SELECTOR_STATES.id;
 | 
						|
                selector = "#";
 | 
						|
                _state = CSS_STATES.selector;
 | 
						|
              } else if (token.text == "*") {
 | 
						|
                selectorState = SELECTOR_STATES.tag;
 | 
						|
                selector = "*";
 | 
						|
                _state = CSS_STATES.selector;
 | 
						|
              } else if (token.text == ":") {
 | 
						|
                _state = CSS_STATES.selector;
 | 
						|
                selectorState = SELECTOR_STATES.pseudo;
 | 
						|
                selector += ":";
 | 
						|
                if (cursor > tokIndex) {
 | 
						|
                  break;
 | 
						|
                }
 | 
						|
 | 
						|
                token = tokens[cursor++];
 | 
						|
                switch (token.tokenType) {
 | 
						|
                  case "function":
 | 
						|
                    if (token.text == "not") {
 | 
						|
                      selectorBeforeNot = selector;
 | 
						|
                      selector = "";
 | 
						|
                      scopeStack.push("(");
 | 
						|
                    } else {
 | 
						|
                      selector += token.text + "(";
 | 
						|
                    }
 | 
						|
                    selectorState = SELECTOR_STATES.null;
 | 
						|
                    break;
 | 
						|
 | 
						|
                  case "ident":
 | 
						|
                    selector += token.text;
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
              } else if (token.text == "[") {
 | 
						|
                _state = CSS_STATES.selector;
 | 
						|
                selectorState = SELECTOR_STATES.attribute;
 | 
						|
                scopeStack.push("[");
 | 
						|
                selector += "[";
 | 
						|
              } else if (token.text == "}") {
 | 
						|
                if (peek(scopeStack) == "@m") {
 | 
						|
                  scopeStack.pop();
 | 
						|
                }
 | 
						|
              }
 | 
						|
              break;
 | 
						|
 | 
						|
            case "at":
 | 
						|
              _state = token.text.startsWith("m")
 | 
						|
                ? CSS_STATES.media
 | 
						|
                : CSS_STATES.keyframes;
 | 
						|
              break;
 | 
						|
          }
 | 
						|
          break;
 | 
						|
 | 
						|
        case CSS_STATES.media:
 | 
						|
          // From CSS_STATES.media, we can only go to CSS_STATES.null state when
 | 
						|
          // we hit the first '{'
 | 
						|
          if (token.tokenType == "symbol" && token.text == "{") {
 | 
						|
            scopeStack.push("@m");
 | 
						|
            _state = CSS_STATES.null;
 | 
						|
          }
 | 
						|
          break;
 | 
						|
 | 
						|
        case CSS_STATES.keyframes:
 | 
						|
          // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
 | 
						|
          // when we hit the first '{'
 | 
						|
          if (token.tokenType == "symbol" && token.text == "{") {
 | 
						|
            scopeStack.push("@k");
 | 
						|
            _state = CSS_STATES.frame;
 | 
						|
          }
 | 
						|
          break;
 | 
						|
 | 
						|
        case CSS_STATES.frame:
 | 
						|
          // From CSS_STATES.frame, we can either go to CSS_STATES.property
 | 
						|
          // state when we hit the first '{' or to CSS_STATES.selector when we
 | 
						|
          // hit '}'
 | 
						|
          if (token.tokenType == "symbol") {
 | 
						|
            if (token.text == "{") {
 | 
						|
              scopeStack.push("f");
 | 
						|
              _state = CSS_STATES.property;
 | 
						|
            } else if (token.text == "}") {
 | 
						|
              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;
 | 
						|
        const 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;
 | 
						|
    this.propertyName = _state == CSS_STATES.value ? propertyName : null;
 | 
						|
    this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
 | 
						|
    this.selectorBeforeNot =
 | 
						|
      selectorBeforeNot == null ? null : selectorBeforeNot;
 | 
						|
    if (token) {
 | 
						|
      selector = selector.slice(0, selector.length + token.loc.end.column - ch);
 | 
						|
      this.selector = selector;
 | 
						|
    } else {
 | 
						|
      this.selector = "";
 | 
						|
    }
 | 
						|
    this.selectors = selectors;
 | 
						|
 | 
						|
    if (token && token.tokenType != "whitespace") {
 | 
						|
      let text;
 | 
						|
      if (token.tokenType == "dimension" || !token.text) {
 | 
						|
        text = source.substring(token.startOffset, token.endOffset);
 | 
						|
      } else {
 | 
						|
        text = token.text;
 | 
						|
      }
 | 
						|
      this.completing = text
 | 
						|
        .slice(0, ch - token.loc.start.column)
 | 
						|
        .replace(/^[.#]$/, "");
 | 
						|
    } else {
 | 
						|
      this.completing = "";
 | 
						|
    }
 | 
						|
    // Special case the situation when the user just entered ":" after typing a
 | 
						|
    // property name.
 | 
						|
    if (this.completing == ":" && _state == CSS_STATES.value) {
 | 
						|
      this.completing = "";
 | 
						|
    }
 | 
						|
 | 
						|
    // Special check for !important; case.
 | 
						|
    if (
 | 
						|
      token &&
 | 
						|
      tokens[cursor - 2] &&
 | 
						|
      tokens[cursor - 2].text == "!" &&
 | 
						|
      this.completing == "important".slice(0, this.completing.length)
 | 
						|
    ) {
 | 
						|
      this.completing = "!" + this.completing;
 | 
						|
    }
 | 
						|
    return _state;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Queries the DOM Walker actor for suggestions regarding the selector being
 | 
						|
   * completed
 | 
						|
   */
 | 
						|
  suggestSelectors: function() {
 | 
						|
    const walker = this.walker;
 | 
						|
    if (!walker) {
 | 
						|
      return Promise.resolve([]);
 | 
						|
    }
 | 
						|
 | 
						|
    let query = this.selector;
 | 
						|
    // Even though the selector matched atleast one node, there is still
 | 
						|
    // possibility of suggestions.
 | 
						|
    switch (this.selectorState) {
 | 
						|
      case SELECTOR_STATES.null:
 | 
						|
        if (this.completing === ",") {
 | 
						|
          return Promise.resolve([]);
 | 
						|
        }
 | 
						|
 | 
						|
        query += "*";
 | 
						|
        break;
 | 
						|
 | 
						|
      case SELECTOR_STATES.tag:
 | 
						|
        query = query.slice(0, query.length - this.completing.length);
 | 
						|
        break;
 | 
						|
 | 
						|
      case SELECTOR_STATES.id:
 | 
						|
      case SELECTOR_STATES.class:
 | 
						|
      case SELECTOR_STATES.pseudo:
 | 
						|
        if (/^[.:#]$/.test(this.completing)) {
 | 
						|
          query = query.slice(0, query.length - this.completing.length);
 | 
						|
          this.completing = "";
 | 
						|
        } else {
 | 
						|
          query = query.slice(0, query.length - this.completing.length - 1);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      /[\s+>~]$/.test(query) &&
 | 
						|
      this.selectorState != SELECTOR_STATES.attribute &&
 | 
						|
      this.selectorState != SELECTOR_STATES.value
 | 
						|
    ) {
 | 
						|
      query += "*";
 | 
						|
    }
 | 
						|
 | 
						|
    // Set the values that this request was supposed to suggest to.
 | 
						|
    this._currentQuery = query;
 | 
						|
    return walker
 | 
						|
      .getSuggestionsForQuery(query, this.completing, this.selectorState)
 | 
						|
      .then(result => this.prepareSelectorResults(result));
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Prepares the selector suggestions returned by the walker actor.
 | 
						|
   */
 | 
						|
  prepareSelectorResults: function(result) {
 | 
						|
    if (this._currentQuery != result.query) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
 | 
						|
    result = result.suggestions;
 | 
						|
    const query = this.selector;
 | 
						|
    const completion = [];
 | 
						|
    for (let [value, count, state] of result) {
 | 
						|
      switch (this.selectorState) {
 | 
						|
        case SELECTOR_STATES.id:
 | 
						|
        case SELECTOR_STATES.class:
 | 
						|
        case SELECTOR_STATES.pseudo:
 | 
						|
          if (/^[.:#]$/.test(this.completing)) {
 | 
						|
            value =
 | 
						|
              query.slice(0, query.length - this.completing.length) + value;
 | 
						|
          } else {
 | 
						|
            value =
 | 
						|
              query.slice(0, query.length - this.completing.length - 1) + value;
 | 
						|
          }
 | 
						|
          break;
 | 
						|
 | 
						|
        case SELECTOR_STATES.tag:
 | 
						|
          value = query.slice(0, query.length - this.completing.length) + value;
 | 
						|
          break;
 | 
						|
 | 
						|
        case SELECTOR_STATES.null:
 | 
						|
          value = query + value;
 | 
						|
          break;
 | 
						|
 | 
						|
        default:
 | 
						|
          value = query.slice(0, query.length - this.completing.length) + value;
 | 
						|
      }
 | 
						|
 | 
						|
      const item = {
 | 
						|
        label: value,
 | 
						|
        preLabel: query,
 | 
						|
        text: value,
 | 
						|
        score: count,
 | 
						|
      };
 | 
						|
 | 
						|
      // In case the query's state is tag and the item's state is id or class
 | 
						|
      // adjust the preLabel
 | 
						|
      if (
 | 
						|
        this.selectorState === SELECTOR_STATES.tag &&
 | 
						|
        state === SELECTOR_STATES.class
 | 
						|
      ) {
 | 
						|
        item.preLabel = "." + item.preLabel;
 | 
						|
      }
 | 
						|
      if (
 | 
						|
        this.selectorState === SELECTOR_STATES.tag &&
 | 
						|
        state === SELECTOR_STATES.id
 | 
						|
      ) {
 | 
						|
        item.preLabel = "#" + item.preLabel;
 | 
						|
      }
 | 
						|
 | 
						|
      completion.push(item);
 | 
						|
 | 
						|
      if (completion.length > this.maxEntries - 1) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return completion;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns CSS property name suggestions based on the input.
 | 
						|
   *
 | 
						|
   * @param startProp {String} Initial part of the property being completed.
 | 
						|
   */
 | 
						|
  completeProperties: function(startProp) {
 | 
						|
    const finalList = [];
 | 
						|
    if (!startProp) {
 | 
						|
      return Promise.resolve(finalList);
 | 
						|
    }
 | 
						|
 | 
						|
    const length = this.propertyNames.length;
 | 
						|
    let i = 0,
 | 
						|
      count = 0;
 | 
						|
    for (; i < length && count < this.maxEntries; i++) {
 | 
						|
      if (this.propertyNames[i].startsWith(startProp)) {
 | 
						|
        count++;
 | 
						|
        const propName = this.propertyNames[i];
 | 
						|
        finalList.push({
 | 
						|
          preLabel: startProp,
 | 
						|
          label: propName,
 | 
						|
          text: propName + ": ",
 | 
						|
        });
 | 
						|
      } else if (this.propertyNames[i] > startProp) {
 | 
						|
        // We have crossed all possible matches alphabetically.
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return Promise.resolve(finalList);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns CSS value suggestions based on the corresponding property.
 | 
						|
   *
 | 
						|
   * @param propName {String} The property to which the value being completed
 | 
						|
   *        belongs.
 | 
						|
   * @param startValue {String} Initial part of the value being completed.
 | 
						|
   */
 | 
						|
  completeValues: function(propName, startValue) {
 | 
						|
    const finalList = [];
 | 
						|
    const list = ["!important;", ...this.cssProperties.getValues(propName)];
 | 
						|
    // If there is no character being completed, we are showing an initial list
 | 
						|
    // of possible values. Skipping '!important' in this case.
 | 
						|
    if (!startValue) {
 | 
						|
      list.splice(0, 1);
 | 
						|
    }
 | 
						|
 | 
						|
    const length = list.length;
 | 
						|
    let i = 0,
 | 
						|
      count = 0;
 | 
						|
    for (; i < length && count < this.maxEntries; i++) {
 | 
						|
      if (list[i].startsWith(startValue)) {
 | 
						|
        count++;
 | 
						|
        const value = list[i];
 | 
						|
        finalList.push({
 | 
						|
          preLabel: startValue,
 | 
						|
          label: value,
 | 
						|
          text: value,
 | 
						|
        });
 | 
						|
      } 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) {
 | 
						|
    const 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;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the state information about a token surrounding the {line, ch} position
 | 
						|
   *
 | 
						|
   * @param {string} source
 | 
						|
   *        The complete source of the CSS file. Unlike resolve state method,
 | 
						|
   *        this method requires the full source.
 | 
						|
   * @param {object} caret
 | 
						|
   *        The line, ch position of the caret.
 | 
						|
   *
 | 
						|
   * @returns {object}
 | 
						|
   *          An object containing the state of token covered by the caret.
 | 
						|
   *          The object has following properties when the the state is
 | 
						|
   *          "selector", "value" or "property", null otherwise:
 | 
						|
   *           - state {string} one of CSS_STATES - "selector", "value" etc.
 | 
						|
   *           - selector {string} The selector at the caret when `state` is
 | 
						|
   *                      selector. OR
 | 
						|
   *           - selectors {[string]} Array of selector strings in case when
 | 
						|
   *                       `state` is "value" or "property"
 | 
						|
   *           - propertyName {string} The property name at the current caret or
 | 
						|
   *                          the property name corresponding to the value at
 | 
						|
   *                          the caret.
 | 
						|
   *           - value {string} The css value at the current caret.
 | 
						|
   *           - loc {object} An object containing the starting and the ending
 | 
						|
   *                 caret position of the whole selector, value or property.
 | 
						|
   *                  - { start: {line, ch}, end: {line, ch}}
 | 
						|
   */
 | 
						|
  getInfoAt: function(source, caret) {
 | 
						|
    // Limits the input source till the {line, ch} caret position
 | 
						|
    function limit(sourceArg, { line, ch }) {
 | 
						|
      line++;
 | 
						|
      const list = sourceArg.split("\n");
 | 
						|
      if (list.length < line) {
 | 
						|
        return sourceArg;
 | 
						|
      }
 | 
						|
      if (line == 1) {
 | 
						|
        return list[0].slice(0, ch);
 | 
						|
      }
 | 
						|
      return [...list.slice(0, line - 1), list[line - 1].slice(0, ch)].join(
 | 
						|
        "\n"
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    // Get the state at the given line, ch
 | 
						|
    const state = this.resolveState(limit(source, caret), caret);
 | 
						|
    const propertyName = this.propertyName;
 | 
						|
    let { line, ch } = caret;
 | 
						|
    const sourceArray = source.split("\n");
 | 
						|
    let limitedSource = limit(source, caret);
 | 
						|
 | 
						|
    /**
 | 
						|
     * Method to traverse forwards from the caret location to figure out the
 | 
						|
     * ending point of a selector or css value.
 | 
						|
     *
 | 
						|
     * @param {function} check
 | 
						|
     *        A method which takes the current state as an input and determines
 | 
						|
     *        whether the state changed or not.
 | 
						|
     */
 | 
						|
    const traverseForward = check => {
 | 
						|
      let location;
 | 
						|
      // Backward loop to determine the beginning location of the selector.
 | 
						|
      do {
 | 
						|
        let lineText = sourceArray[line];
 | 
						|
        if (line == caret.line) {
 | 
						|
          lineText = lineText.substring(caret.ch);
 | 
						|
        }
 | 
						|
 | 
						|
        let prevToken = undefined;
 | 
						|
        const tokens = cssTokenizer(lineText);
 | 
						|
        let found = false;
 | 
						|
        const ech = line == caret.line ? caret.ch : 0;
 | 
						|
        for (let token of tokens) {
 | 
						|
          // If the line is completely spaces, handle it differently
 | 
						|
          if (lineText.trim() == "") {
 | 
						|
            limitedSource += lineText;
 | 
						|
          } else {
 | 
						|
            limitedSource += sourceArray[line].substring(
 | 
						|
              ech + token.startOffset,
 | 
						|
              ech + token.endOffset
 | 
						|
            );
 | 
						|
          }
 | 
						|
 | 
						|
          // Whitespace cannot change state.
 | 
						|
          if (token.tokenType == "whitespace") {
 | 
						|
            prevToken = token;
 | 
						|
            continue;
 | 
						|
          }
 | 
						|
 | 
						|
          const forwState = this.resolveState(limitedSource, {
 | 
						|
            line: line,
 | 
						|
            ch: token.endOffset + ech,
 | 
						|
          });
 | 
						|
          if (check(forwState)) {
 | 
						|
            if (prevToken && prevToken.tokenType == "whitespace") {
 | 
						|
              token = prevToken;
 | 
						|
            }
 | 
						|
            location = {
 | 
						|
              line: line,
 | 
						|
              ch: token.startOffset + ech,
 | 
						|
            };
 | 
						|
            found = true;
 | 
						|
            break;
 | 
						|
          }
 | 
						|
          prevToken = token;
 | 
						|
        }
 | 
						|
        limitedSource += "\n";
 | 
						|
        if (found) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      } while (line++ < sourceArray.length);
 | 
						|
      return location;
 | 
						|
    };
 | 
						|
 | 
						|
    /**
 | 
						|
     * Method to traverse backwards from the caret location to figure out the
 | 
						|
     * starting point of a selector or css value.
 | 
						|
     *
 | 
						|
     * @param {function} check
 | 
						|
     *        A method which takes the current state as an input and determines
 | 
						|
     *        whether the state changed or not.
 | 
						|
     * @param {boolean} isValue
 | 
						|
     *        true if the traversal is being done for a css value state.
 | 
						|
     */
 | 
						|
    const traverseBackwards = (check, isValue) => {
 | 
						|
      let location;
 | 
						|
      // Backward loop to determine the beginning location of the selector.
 | 
						|
      do {
 | 
						|
        let lineText = sourceArray[line];
 | 
						|
        if (line == caret.line) {
 | 
						|
          lineText = lineText.substring(0, caret.ch);
 | 
						|
        }
 | 
						|
 | 
						|
        const tokens = Array.from(cssTokenizer(lineText));
 | 
						|
        let found = false;
 | 
						|
        for (let i = tokens.length - 1; i >= 0; i--) {
 | 
						|
          let token = tokens[i];
 | 
						|
          // If the line is completely spaces, handle it differently
 | 
						|
          if (lineText.trim() == "") {
 | 
						|
            limitedSource = limitedSource.slice(0, -1 * lineText.length);
 | 
						|
          } else {
 | 
						|
            const length = token.endOffset - token.startOffset;
 | 
						|
            limitedSource = limitedSource.slice(0, -1 * length);
 | 
						|
          }
 | 
						|
 | 
						|
          // Whitespace cannot change state.
 | 
						|
          if (token.tokenType == "whitespace") {
 | 
						|
            continue;
 | 
						|
          }
 | 
						|
 | 
						|
          const backState = this.resolveState(limitedSource, {
 | 
						|
            line: line,
 | 
						|
            ch: token.startOffset,
 | 
						|
          });
 | 
						|
          if (check(backState)) {
 | 
						|
            if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") {
 | 
						|
              token = tokens[i + 1];
 | 
						|
            }
 | 
						|
            location = {
 | 
						|
              line: line,
 | 
						|
              ch: isValue ? token.endOffset : token.startOffset,
 | 
						|
            };
 | 
						|
            found = true;
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        limitedSource = limitedSource.slice(0, -1);
 | 
						|
        if (found) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      } while (line-- >= 0);
 | 
						|
      return location;
 | 
						|
    };
 | 
						|
 | 
						|
    if (state == CSS_STATES.selector) {
 | 
						|
      // For selector state, the ending and starting point of the selector is
 | 
						|
      // either when the state changes or the selector becomes empty and a
 | 
						|
      // single selector can span multiple lines.
 | 
						|
      // Backward loop to determine the beginning location of the selector.
 | 
						|
      const start = traverseBackwards(backState => {
 | 
						|
        return (
 | 
						|
          backState != CSS_STATES.selector ||
 | 
						|
          (this.selector == "" && this.selectorBeforeNot == null)
 | 
						|
        );
 | 
						|
      });
 | 
						|
 | 
						|
      line = caret.line;
 | 
						|
      limitedSource = limit(source, caret);
 | 
						|
      // Forward loop to determine the ending location of the selector.
 | 
						|
      const end = traverseForward(forwState => {
 | 
						|
        return (
 | 
						|
          forwState != CSS_STATES.selector ||
 | 
						|
          (this.selector == "" && this.selectorBeforeNot == null)
 | 
						|
        );
 | 
						|
      });
 | 
						|
 | 
						|
      // Since we have start and end positions, figure out the whole selector.
 | 
						|
      let selector = source.split("\n").slice(start.line, end.line + 1);
 | 
						|
      selector[selector.length - 1] = selector[selector.length - 1].substring(
 | 
						|
        0,
 | 
						|
        end.ch
 | 
						|
      );
 | 
						|
      selector[0] = selector[0].substring(start.ch);
 | 
						|
      selector = selector.join("\n");
 | 
						|
      return {
 | 
						|
        state: state,
 | 
						|
        selector: selector,
 | 
						|
        loc: {
 | 
						|
          start: start,
 | 
						|
          end: end,
 | 
						|
        },
 | 
						|
      };
 | 
						|
    } else if (state == CSS_STATES.property) {
 | 
						|
      // A property can only be a single word and thus very easy to calculate.
 | 
						|
      const tokens = cssTokenizer(sourceArray[line]);
 | 
						|
      for (const token of tokens) {
 | 
						|
        // Note that, because we're tokenizing a single line, the
 | 
						|
        // token's offset is also the column number.
 | 
						|
        if (token.startOffset <= ch && token.endOffset >= ch) {
 | 
						|
          return {
 | 
						|
            state: state,
 | 
						|
            propertyName: token.text,
 | 
						|
            selectors: this.selectors,
 | 
						|
            loc: {
 | 
						|
              start: {
 | 
						|
                line: line,
 | 
						|
                ch: token.startOffset,
 | 
						|
              },
 | 
						|
              end: {
 | 
						|
                line: line,
 | 
						|
                ch: token.endOffset,
 | 
						|
              },
 | 
						|
            },
 | 
						|
          };
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else if (state == CSS_STATES.value) {
 | 
						|
      // CSS value can be multiline too, so we go forward and backwards to
 | 
						|
      // determine the bounds of the value at caret
 | 
						|
      const start = traverseBackwards(
 | 
						|
        backState => backState != CSS_STATES.value,
 | 
						|
        true
 | 
						|
      );
 | 
						|
 | 
						|
      line = caret.line;
 | 
						|
      limitedSource = limit(source, caret);
 | 
						|
      const end = traverseForward(forwState => forwState != CSS_STATES.value);
 | 
						|
 | 
						|
      let value = source.split("\n").slice(start.line, end.line + 1);
 | 
						|
      value[value.length - 1] = value[value.length - 1].substring(0, end.ch);
 | 
						|
      value[0] = value[0].substring(start.ch);
 | 
						|
      value = value.join("\n");
 | 
						|
      return {
 | 
						|
        state: state,
 | 
						|
        propertyName: propertyName,
 | 
						|
        selectors: this.selectors,
 | 
						|
        value: value,
 | 
						|
        loc: {
 | 
						|
          start: start,
 | 
						|
          end: end,
 | 
						|
        },
 | 
						|
      };
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
module.exports = CSSCompleter;
 |