diff options
Diffstat (limited to 'devtools/client/shared/sourceeditor/css-autocompleter.js')
-rw-r--r-- | devtools/client/shared/sourceeditor/css-autocompleter.js | 578 |
1 files changed, 316 insertions, 262 deletions
diff --git a/devtools/client/shared/sourceeditor/css-autocompleter.js b/devtools/client/shared/sourceeditor/css-autocompleter.js index 7db3cbbc0f..4266bd02b8 100644 --- a/devtools/client/shared/sourceeditor/css-autocompleter.js +++ b/devtools/client/shared/sourceeditor/css-autocompleter.js @@ -28,15 +28,14 @@ const { * 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. + * what the user is currently editing (e.g. 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` + * - The current token that is being edited `completing` * - 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 @@ -214,30 +213,26 @@ CSSCompleter.prototype = { // 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; + if (token.tokenType === "Colon") { + scopeStack.push(":"); + if (tokens[cursor - 2].tokenType != "WhiteSpace") { + propertyName = tokens[cursor - 2].text; + } else { + propertyName = tokens[cursor - 3].text; + } + _state = CSS_STATES.value; + } - 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; + if (token.tokenType === "CloseCurlyBracket") { + if (/[{f]/.test(peek(scopeStack))) { + const popped = scopeStack.pop(); + if (popped == "f") { + _state = CSS_STATES.frame; + } else { + selector = ""; + selectors = []; + _state = CSS_STATES.null; + } } } break; @@ -245,31 +240,27 @@ CSSCompleter.prototype = { 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; + if (token.tokenType === "Semicolon") { + if (/[:]/.test(peek(scopeStack))) { + scopeStack.pop(); + _state = CSS_STATES.property; + } + } - case "}": - if (peek(scopeStack) == ":") { - scopeStack.pop(); - } + if (token.tokenType === "CloseCurlyBracket") { + 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; + if (/[{f]/.test(peek(scopeStack))) { + const popped = scopeStack.pop(); + if (popped == "f") { + _state = CSS_STATES.frame; + } else { + selector = ""; + selectors = []; + _state = CSS_STATES.null; + } } } break; @@ -277,7 +268,7 @@ CSSCompleter.prototype = { 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 == "{") { + if (token.tokenType === "CurlyBracketBlock") { scopeStack.push("{"); _state = CSS_STATES.property; selectors.push(selector); @@ -290,74 +281,87 @@ CSSCompleter.prototype = { case SELECTOR_STATES.class: case SELECTOR_STATES.tag: switch (token.tokenType) { - case "hash": - case "id": + case "Hash": + case "IDHash": selectorState = SELECTOR_STATES.id; - selector += "#" + token.text; + selector += token.text; break; - case "symbol": + case "Delim": if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector += "."; if ( cursor <= tokIndex && - tokens[cursor].tokenType == "ident" + tokens[cursor].tokenType == "Ident" ) { token = tokens[cursor++]; selector += token.text; } } else if (token.text == "#") { + // Lonely # char, that doesn't produce a Hash nor IDHash selectorState = SELECTOR_STATES.id; selector += "#"; - } else if (/[>~+]/.test(token.text)) { + } else if ( + token.text == "+" || + token.text == "~" || + 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; - } + } + 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": + case "Comma": + selectorState = SELECTOR_STATES.null; + selectors.push(selector); + selector = ""; + break; + + case "Colon": + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) { + break; + } + + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { 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; + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; + } + break; + + case "SquareBracketBlock": + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; + + case "CloseParenthesis": + if (peek(scopeStack) == "(") { + scopeStack.pop(); + selector = selectorBeforeNot + "not(" + selector + ")"; + selectorBeforeNot = null; + } else { + selector += ")"; } + selectorState = SELECTOR_STATES.null; break; - case "whitespace": + case "WhiteSpace": selectorState = SELECTOR_STATES.null; selector && (selector += " "); break; @@ -369,81 +373,94 @@ CSSCompleter.prototype = { // SELECTOR_STATES.id, SELECTOR_STATES.class or // SELECTOR_STATES.tag switch (token.tokenType) { - case "hash": - case "id": + case "Hash": + case "IDHash": selectorState = SELECTOR_STATES.id; - selector += "#" + token.text; + selector += token.text; break; - case "ident": + case "Ident": selectorState = SELECTOR_STATES.tag; selector += token.text; break; - case "symbol": + case "Delim": if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector += "."; if ( cursor <= tokIndex && - tokens[cursor].tokenType == "ident" + tokens[cursor].tokenType == "Ident" ) { token = tokens[cursor++]; selector += token.text; } } else if (token.text == "#") { + // Lonely # char, that doesn't produce a Hash nor IDHash selectorState = SELECTOR_STATES.id; selector += "#"; } else if (token.text == "*") { selectorState = SELECTOR_STATES.tag; selector += "*"; - } else if (/[>~+]/.test(token.text)) { + } else if ( + token.text == "+" || + token.text == "~" || + 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; - } + } + 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": + case "Comma": + selectorState = SELECTOR_STATES.null; + selectors.push(selector); + selector = ""; + break; + + case "Colon": + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) { + break; + } + + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { 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; + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; } break; - case "whitespace": + case "SquareBracketBlock": + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; + + case "CloseParenthesis": + if (peek(scopeStack) == "(") { + scopeStack.pop(); + selector = selectorBeforeNot + "not(" + selector + ")"; + selectorBeforeNot = null; + } else { + selector += ")"; + } + selectorState = SELECTOR_STATES.null; + break; + + case "WhiteSpace": selector && (selector += " "); break; } @@ -451,46 +468,55 @@ CSSCompleter.prototype = { case SELECTOR_STATES.pseudo: switch (token.tokenType) { - case "symbol": - if (/[>~+]/.test(token.text)) { + case "Delim": + if ( + token.text == "+" || + token.text == "~" || + 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; - } + } + break; + + case "Comma": + selectorState = SELECTOR_STATES.null; + selectors.push(selector); + selector = ""; + break; + + case "Colon": + 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": + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { selector += token.text; - break; - } - } else if (token.text == "[") { - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; } break; + case "SquareBracketBlock": + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; - case "whitespace": + case "WhiteSpace": selectorState = SELECTOR_STATES.null; selector && (selector += " "); break; @@ -499,29 +525,40 @@ CSSCompleter.prototype = { case SELECTOR_STATES.attribute: switch (token.tokenType) { - case "symbol": - if (/[~|^$*]/.test(token.text)) { - selector += token.text; - token = tokens[cursor++]; - } else if (token.text == "=") { + case "IncludeMatch": + case "DashMatch": + case "PrefixMatch": + case "IncludeSuffixMatchMatch": + case "SubstringMatch": + selector += token.text; + token = tokens[cursor++]; + break; + + case "Delim": + if (token.text == "=") { selectorState = SELECTOR_STATES.value; selector += token.text; - } else if (token.text == "]") { - if (peek(scopeStack) == "[") { - scopeStack.pop(); - } + } + break; - selectorState = SELECTOR_STATES.null; - selector += "]"; + case "CloseSquareBracket": + if (peek(scopeStack) == "[") { + scopeStack.pop(); } + + selectorState = SELECTOR_STATES.null; + selector += "]"; break; - case "ident": - case "string": + case "Ident": selector += token.text; break; - case "whitespace": + case "QuotedString": + selector += token.value; + break; + + case "WhiteSpace": selector && (selector += " "); break; } @@ -529,23 +566,24 @@ CSSCompleter.prototype = { case SELECTOR_STATES.value: switch (token.tokenType) { - case "string": - case "ident": + case "Ident": selector += token.text; break; - case "symbol": - if (token.text == "]") { - if (peek(scopeStack) == "[") { - scopeStack.pop(); - } + case "QuotedString": + selector += token.value; + break; - selectorState = SELECTOR_STATES.null; - selector += "]"; + case "CloseSquareBracket": + if (peek(scopeStack) == "[") { + scopeStack.pop(); } + + selectorState = SELECTOR_STATES.null; + selector += "]"; break; - case "whitespace": + case "WhiteSpace": selector && (selector += " "); break; } @@ -557,29 +595,30 @@ CSSCompleter.prototype = { // From CSS_STATES.null state, we can go to either CSS_STATES.media or // CSS_STATES.selector. switch (token.tokenType) { - case "hash": - case "id": + case "Hash": + case "IDHash": selectorState = SELECTOR_STATES.id; - selector = "#" + token.text; + selector = token.text; _state = CSS_STATES.selector; break; - case "ident": + case "Ident": selectorState = SELECTOR_STATES.tag; selector = token.text; _state = CSS_STATES.selector; break; - case "symbol": + case "Delim": if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector = "."; _state = CSS_STATES.selector; - if (cursor <= tokIndex && tokens[cursor].tokenType == "ident") { + if (cursor <= tokIndex && tokens[cursor].tokenType == "Ident") { token = tokens[cursor++]; selector += token.text; } } else if (token.text == "#") { + // Lonely # char, that doesn't produce a Hash nor IDHash selectorState = SELECTOR_STATES.id; selector = "#"; _state = CSS_STATES.selector; @@ -587,45 +626,52 @@ CSSCompleter.prototype = { 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; - } + } + 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 "Colon": + _state = CSS_STATES.selector; + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) { + break; + } - case "ident": + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { 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(); - } + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; + } + break; + + case "CloseSquareBracket": + _state = CSS_STATES.selector; + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; + + case "CurlyBracketBlock": + if (peek(scopeStack) == "@m") { + scopeStack.pop(); } break; - case "at": - _state = token.text.startsWith("m") + case "AtKeyword": + // XXX: We should probably handle other at-rules (@container, @property, …) + _state = token.value.startsWith("m") ? CSS_STATES.media : CSS_STATES.keyframes; break; @@ -635,7 +681,7 @@ CSSCompleter.prototype = { 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 == "{") { + if (token.tokenType == "CurlyBracketBlock") { scopeStack.push("@m"); _state = CSS_STATES.null; } @@ -644,7 +690,7 @@ CSSCompleter.prototype = { 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 == "{") { + if (token.tokenType == "CurlyBracketBlock") { scopeStack.push("@k"); _state = CSS_STATES.frame; } @@ -654,17 +700,15 @@ CSSCompleter.prototype = { // 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; + if (token.tokenType == "CurlyBracketBlock") { + scopeStack.push("f"); + _state = CSS_STATES.property; + } else if (token.tokenType == "CloseCurlyBracket") { + if (peek(scopeStack) == "@k") { + scopeStack.pop(); } + + _state = CSS_STATES.null; } break; } @@ -688,6 +732,8 @@ CSSCompleter.prototype = { this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]); } } + // ^ while loop end + this.state = _state; this.propertyName = _state == CSS_STATES.value ? propertyName : null; this.selectorState = _state == CSS_STATES.selector ? selectorState : null; @@ -701,10 +747,18 @@ CSSCompleter.prototype = { } this.selectors = selectors; - if (token && token.tokenType != "whitespace") { + if (token && token.tokenType != "WhiteSpace") { let text; - if (token.tokenType == "dimension" || !token.text) { + if (token.tokenType == "Dimension" || !token.text) { text = source.substring(token.startOffset, token.endOffset); + } else if ( + token.tokenType === "IDHash" || + token.tokenType === "Hash" || + token.tokenType === "AtKeyword" || + token.tokenType === "Function" || + token.tokenType === "QuotedString" + ) { + text = token.value; } else { text = token.text; } @@ -1047,10 +1101,10 @@ CSSCompleter.prototype = { } let prevToken = undefined; - const tokens = cssTokenizer(lineText); + const tokensIterator = cssTokenizer(lineText); let found = false; const ech = line == caret.line ? caret.ch : 0; - for (let token of tokens) { + for (let token of tokensIterator) { // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource += lineText; @@ -1061,8 +1115,8 @@ CSSCompleter.prototype = { ); } - // Whitespace cannot change state. - if (token.tokenType == "whitespace") { + // WhiteSpace cannot change state. + if (token.tokenType == "WhiteSpace") { prevToken = token; continue; } @@ -1072,7 +1126,7 @@ CSSCompleter.prototype = { ch: token.endOffset + ech, }); if (check(forwState)) { - if (prevToken && prevToken.tokenType == "whitespace") { + if (prevToken && prevToken.tokenType == "WhiteSpace") { token = prevToken; } location = { @@ -1123,8 +1177,8 @@ CSSCompleter.prototype = { limitedSource = limitedSource.slice(0, -1 * length); } - // Whitespace cannot change state. - if (token.tokenType == "whitespace") { + // WhiteSpace cannot change state. + if (token.tokenType == "WhiteSpace") { continue; } @@ -1133,7 +1187,7 @@ CSSCompleter.prototype = { ch: token.startOffset, }); if (check(backState)) { - if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") { + if (tokens[i + 1] && tokens[i + 1].tokenType == "WhiteSpace") { token = tokens[i + 1]; } location = { |