Added: trunk/LayoutTests/inspector/unit-tests/css-keyword-completions.html (0 => 279422)
--- trunk/LayoutTests/inspector/unit-tests/css-keyword-completions.html (rev 0)
+++ trunk/LayoutTests/inspector/unit-tests/css-keyword-completions.html 2021-06-30 18:14:17 UTC (rev 279422)
@@ -0,0 +1,186 @@
+<!doctype html>
+<html>
+<head>
+<script src=""
+<script>
+function test()
+{
+ let suite = InspectorTest.createSyncSuite("WI.CSSKeywordCompletions");
+
+ function addTestForPartialPropertyName({name, description, text, allowEmptyPrefix, expectedCompletions, expectedCompletionCount}) {
+ suite.addTestCase({
+ name,
+ description,
+ test() {
+ allowEmptyPrefix ??= false;
+ expectedCompletions ??= [];
+ expectedCompletionCount ??= -1;
+
+ // FIXME: <webkit.org/b/227157> Styles: Support completions mid-token.
+ let caretPosition = text.length;
+ let completionResults = WI.CSSKeywordCompletions.forPartialPropertyName(text, {caretPosition, allowEmptyPrefix});
+
+ if (expectedCompletionCount >= 0)
+ InspectorTest.expectEqual(completionResults.completions.length, expectedCompletionCount, `Expected exactly ${expectedCompletionCount} completion results.`);
+
+ // Because expected completions could be added at any time, just make sure the list contains our expected completions, instead of enforcing an exact match between expectations and reality.
+ let expectedCompletionsPresent = expectedCompletions.every((expectedCompletion) => {
+ if (!completionResults.completions.includes(expectedCompletion)) {
+ InspectorTest.fail(`Expected completion "${expectedCompletion}" in completions.`);
+ return false;
+ }
+ return true;
+ });
+ InspectorTest.expectThat(expectedCompletionsPresent, "All expected completions were present.");
+ }
+ });
+ }
+
+ addTestForPartialPropertyName({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyName.emptyTextDisallowsEmptyPrefix",
+ description: "Test that for empty text, there should be no completions when `allowEmptyPrefix` is `false`.",
+ text: "",
+ allowEmptyPrefix: false,
+ expectedCompletionCount: 0,
+ });
+
+ addTestForPartialPropertyName({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyName.emptyTextAllowsEmptyPrefix",
+ description: "Test that for empty text, there should be completions when `allowEmptyPrefix` is `true`.",
+ text: "",
+ allowEmptyPrefix: true,
+ expectedCompletions: ["border", "color", "margin", "padding"],
+ });
+
+ addTestForPartialPropertyName({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyName.singleCharacterMatches",
+ description: "Test that for a common single character, there will be multiple completions available.",
+ text: "r",
+ expectedCompletions: ["r", "range", "resize", "right", "rotate", "row-gap", "rx", "ry"],
+ });
+
+ addTestForPartialPropertyName({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyName.multipleCharacterMatches",
+ description: "Test that for a more specific set of characters, there will be some number of matches.",
+ text: "rang",
+ expectedCompletions: ["range"],
+ });
+
+ function addTestForPartialPropertyValue({name, description, propertyName, text, caretPosition, expectedPrefix, expectedCompletions}) {
+ suite.addTestCase({
+ name,
+ description,
+ test() {
+ caretPosition ??= text.length;
+ expectedPrefix ??= text;
+ expectedCompletions ??= [];
+
+ let completionResults = WI.CSSKeywordCompletions.forPartialPropertyValue(text, propertyName, {caretPosition});
+ InspectorTest.expectEqual(completionResults.prefix, expectedPrefix, `Expected result prefix to be "${expectedPrefix}"`);
+
+ // Because expected completions could be added at any time, just make sure the list contains our expected completions, instead of enforcing an exact match between expectations and reality.
+ let expectedCompletionsPresent = expectedCompletions.every((expectedCompletion) => {
+ if (!completionResults.completions.includes(expectedCompletion)) {
+ InspectorTest.fail(`Expected completion "${expectedCompletion}" in completions.`);
+ return false;
+ }
+ return true;
+ });
+ InspectorTest.expectThat(expectedCompletionsPresent, "All expected completions were present.");
+ }
+ });
+ }
+
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.EmptyTop",
+ description: "Test that an empty value provides the expected default completions.",
+ propertyName: "top",
+ text: "",
+ expectedCompletions: ["env()", "initial", "revert", "unset", "var()"],
+ });
+
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.EmptyColor",
+ description: "Test that color completions provide the color functions as completions.",
+ propertyName: "color",
+ text: "",
+ expectedCompletions: ["black", "blue", "green", "red", "white", "papayawhip", "color()", "color-contrast()", "color-mix()", "env()", "hsl()", "hsla()", "hwb()", "lab()", "lch()", "rgb()", "rgba()", "var()"],
+ });
+
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.PartialColor",
+ description: "Test that a partial color name that also matches a full color name still provides the other completion.",
+ propertyName: "color",
+ text: "papaya",
+ expectedCompletions: ["papayawhip"],
+ });
+
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.EmptyWordSpacing",
+ description: "Test that the `word-spacing` property provides its expected completions.",
+ propertyName: "word-spacing",
+ text: "",
+ expectedCompletions: ["normal", "calc()"],
+ });
+
+ // `border: 1px | red`
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.MidLineBorder",
+ description: "Test that a completion can be performed between two other values.",
+ propertyName: "border",
+ text: "1px red",
+ caretPosition: 4,
+ expectedPrefix: "",
+ expectedCompletions: ["solid"],
+ });
+
+ // `top: env(|`
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.EnvironmentFunctionCompletion",
+ description: "Test that a function completion can be performed inside a function without a closing parenthesis.",
+ propertyName: "top",
+ text: "env(",
+ expectedPrefix: "",
+ expectedCompletions: ["safe-area-inset-bottom", "safe-area-inset-left", "safe-area-inset-right", "safe-area-inset-top", "var()"],
+ });
+
+ // `top: env(|)`
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.MidLineEnvironmentFunction",
+ description: " Test that a function completion can be performed inside a function with a closing parenthesis.",
+ propertyName: "top",
+ text: "env()",
+ caretPosition: 4,
+ expectedPrefix: "",
+ expectedCompletions: ["safe-area-inset-bottom", "safe-area-inset-left", "safe-area-inset-right", "safe-area-inset-top", "var()"],
+ });
+
+ // `grid-template-columns: repeat(au|`
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.PartialFunctionValue",
+ description: "Test that completing a partial value inside a function produces expected completions.",
+ propertyName: "grid-template-columns",
+ text: "repeat(au",
+ expectedPrefix: "au",
+ expectedCompletions: ["auto-fill", "auto-fit"],
+ });
+
+ // `grid-template-columns: [linename1] 100px repeat(|, [linename2 linename3] 150px) [linename3] 100px [linename4]`
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.ComplexMultiValueFunction",
+ description: " Test that performing a completion mid-line inside a function right before a separator character, like a comma, produces expected completions.",
+ propertyName: "grid-template-columns",
+ text: "[linename1] 100px repeat(, [linename2 linename3] 150px) [linename3] 100px [linename4]",
+ caretPosition: 25,
+ expectedPrefix: "",
+ expectedCompletions: ["auto-fill", "auto-fit", "var()"],
+ });
+
+ suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body _onload_="runTest()">
+ <p>Testing WI.CSSKeywordCompletions.</p>
+</body>
+</html>
Modified: trunk/Source/WebInspectorUI/UserInterface/Models/CSSKeywordCompletions.js (279421 => 279422)
--- trunk/Source/WebInspectorUI/UserInterface/Models/CSSKeywordCompletions.js 2021-06-30 17:45:06 UTC (rev 279421)
+++ trunk/Source/WebInspectorUI/UserInterface/Models/CSSKeywordCompletions.js 2021-06-30 18:14:17 UTC (rev 279422)
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
- * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
@@ -31,6 +31,90 @@
WI.CSSKeywordCompletions = {};
+WI.CSSKeywordCompletions.forPartialPropertyName = function(text, {caretPosition, allowEmptyPrefix} = {})
+{
+ caretPosition ??= text.length;
+ allowEmptyPrefix ??= false;
+
+ // FIXME: <webkit.org/b/227157> Styles: Support completions mid-token.
+ if (caretPosition !== caretPosition)
+ return {prefix: text, completions: []};
+
+ if (!text.length && allowEmptyPrefix)
+ return {prefix: text, completions: WI.CSSCompletions.cssNameCompletions.values};
+ return {prefix: text, completions:WI.CSSCompletions.cssNameCompletions.startsWith(text)};
+};
+
+WI.CSSKeywordCompletions.forPartialPropertyValue = function(text, propertyName, {caretPosition} = {})
+{
+ caretPosition ??= text.length;
+
+ console.assert(caretPosition >= 0 && caretPosition <= text.length, text, caretPosition);
+ if (caretPosition < 0 || caretPosition > text.length)
+ return {prefix: "", completions: []};
+
+ if (!text.length)
+ return {prefix: "", completions: WI.CSSKeywordCompletions.forProperty(propertyName).values};
+
+ let tokens = WI.tokenizeCSSValue(text);
+
+ // Find the token that the cursor is either in or at the end of.
+ let indexOfTokenAtCaret = -1;
+ let passedCharacters = 0;
+ for (let i = 0; i < tokens.length; ++i) {
+ passedCharacters += tokens[i].value.length;
+ if (passedCharacters >= caretPosition) {
+ indexOfTokenAtCaret = i;
+ break;
+ }
+ }
+
+ let tokenAtCaret = tokens[indexOfTokenAtCaret];
+ console.assert(tokenAtCaret, text, caretPosition);
+ if (!tokenAtCaret)
+ return {prefix: "", completions: []};
+
+ if (tokenAtCaret.type && /\b(comment|string)\b/.test(tokenAtCaret.type))
+ return {prefix: "", completions: []};
+
+ let currentTokenValue = tokenAtCaret.value.trim();
+ let caretIsInMiddleOfToken = caretPosition !== passedCharacters;
+
+ // FIXME: <webkit.org/b/227157 Styles: Support completions mid-token.
+ // If the cursor was in middle of a token or the next token starts with a valid character for a value, we are effectively mid-token.
+ let tokenAfterCaret = tokens[indexOfTokenAtCaret + 1];
+ if ((caretIsInMiddleOfToken && currentTokenValue.length) || (!caretIsInMiddleOfToken && tokenAfterCaret && /[a-zA-Z0-9-]/.test(tokenAfterCaret.value[0])))
+ return {prefix: "", completions: []};
+
+ // If the current token value is a comma or opening parenthesis, treat it as if we are at the start of a new token.
+ if (currentTokenValue === "(" || currentTokenValue === ",")
+ currentTokenValue = "";
+
+ let functionName = null;
+ let preceedingFunctionDepth = 0;
+ for (let i = indexOfTokenAtCaret; i >= 0; --i) {
+ let value = tokens[i].value;
+
+ // There may be one or more complete functions between the cursor and the current scope's functions name.
+ if (value === ")")
+ ++preceedingFunctionDepth;
+ else if (value === "(") {
+ if (preceedingFunctionDepth)
+ --preceedingFunctionDepth;
+ else {
+ functionName = tokens[i - 1]?.value;
+ break;
+ }
+ }
+ }
+
+ // FIXME: <webkit.org/b/227098> Styles sidebar panel should autocomplete `var()` values.
+ if (functionName)
+ return {prefix: currentTokenValue, completions: WI.CSSKeywordCompletions.forFunction(functionName).startsWith(currentTokenValue)};
+
+ return {prefix: currentTokenValue, completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(currentTokenValue)};
+};
+
WI.CSSKeywordCompletions.forProperty = function(propertyName)
{
let acceptedKeywords = ["initial", "unset", "revert", "var()", "env()"];