Diff
Modified: trunk/LayoutTests/ChangeLog (286610 => 286611)
--- trunk/LayoutTests/ChangeLog 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/LayoutTests/ChangeLog 2021-12-07 20:51:52 UTC (rev 286611)
@@ -1,3 +1,18 @@
+2021-12-07 Razvan Caliman <[email protected]>
+
+ Web Inspector: Support fuzzy matching in CSS completions
+ https://bugs.webkit.org/show_bug.cgi?id=230351
+ <rdar://82976292>
+
+ Reviewed by Devin Rousso.
+
+ Add test for `WI.CSSQueryController` to check fuzzy matching logic for CSS completions.
+ Follows prior example from `LayoutTests/inspector/unit-tests/resource-query-controller.html`
+ since `WI.ResouceQueryController` served as the model class that was adapted for CSS.
+
+ * inspector/unit-tests/css-query-controller-expected.txt: Added.
+ * inspector/unit-tests/css-query-controller.html: Added.
+
2021-12-07 Chris Dumez <[email protected]>
ASSERTION FAILED: m_messagesBeingDispatched.isEmpty() on http/tests/resourceLoadStatistics/website-data-removal-for-site-with-user-interaction.html
Modified: trunk/LayoutTests/inspector/unit-tests/css-keyword-completions-expected.txt (286610 => 286611)
--- trunk/LayoutTests/inspector/unit-tests/css-keyword-completions-expected.txt 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/LayoutTests/inspector/unit-tests/css-keyword-completions-expected.txt 2021-12-07 20:51:52 UTC (rev 286611)
@@ -71,3 +71,12 @@
PASS: Expected result prefix to be ""
PASS: All expected completions were present.
+-- Running test case: WI.CSSKeywordCompletions.forPartialPropertyValue.NoWhitespaceAfterFunction
+PASS: Expected result prefix to be ""
+PASS: Expected exactly 0 completion results.
+PASS: All expected completions were present.
+
+-- Running test case: WI.CSSKeywordCompletions.forPartialPropertyValue.WhitespaceAfterFunction
+PASS: Expected result prefix to be "a"
+PASS: All expected completions were present.
+
Modified: trunk/LayoutTests/inspector/unit-tests/css-keyword-completions.html (286610 => 286611)
--- trunk/LayoutTests/inspector/unit-tests/css-keyword-completions.html 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/LayoutTests/inspector/unit-tests/css-keyword-completions.html 2021-12-07 20:51:52 UTC (rev 286611)
@@ -66,7 +66,7 @@
expectedCompletions: ["range"],
});
- function addTestForPartialPropertyValue({name, description, propertyName, text, caretPosition, expectedPrefix, expectedCompletions, additionalFunctionValueCompletionsProvider}) {
+ function addTestForPartialPropertyValue({name, description, propertyName, text, caretPosition, expectedPrefix, expectedCompletions, expectedCompletionCount, additionalFunctionValueCompletionsProvider}) {
suite.addTestCase({
name,
description,
@@ -74,11 +74,15 @@
caretPosition ??= text.length;
expectedPrefix ??= text;
expectedCompletions ??= [];
+ expectedCompletionCount ??= -1;
additionalFunctionValueCompletionsProvider ??= () => {};
let completionResults = WI.CSSKeywordCompletions.forPartialPropertyValue(text, propertyName, {caretPosition, additionalFunctionValueCompletionsProvider});
InspectorTest.expectEqual(completionResults.prefix, expectedPrefix, `Expected result prefix to be "${expectedPrefix}"`);
+ 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)) {
@@ -229,6 +233,28 @@
additionalFunctionValueCompletionsProvider: () => ["--one", "--two"],
});
+ // `hsl()a|`
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.NoWhitespaceAfterFunction",
+ description: "Test that color completions are not offered immediately after a closing parenthesis",
+ propertyName: "background",
+ text: "hsl()a",
+ caretPosition: 6,
+ expectedPrefix: "",
+ expectedCompletionCount: 0
+ });
+
+ // `hsl() a|`
+ addTestForPartialPropertyValue({
+ name: "WI.CSSKeywordCompletions.forPartialPropertyValue.WhitespaceAfterFunction",
+ description: "Test that color completions are offered when a closing parenthesis is followed by whitespace",
+ propertyName: "background",
+ text: "hsl() a",
+ caretPosition: 7,
+ expectedPrefix: "a",
+ expectedCompletions: ["aliceblue", "antiquewhite"],
+ });
+
suite.runTestCasesAndFinish();
}
</script>
Added: trunk/LayoutTests/inspector/unit-tests/css-query-controller-expected.txt (0 => 286611)
--- trunk/LayoutTests/inspector/unit-tests/css-query-controller-expected.txt (rev 0)
+++ trunk/LayoutTests/inspector/unit-tests/css-query-controller-expected.txt 2021-12-07 20:51:52 UTC (rev 286611)
@@ -0,0 +1,54 @@
+Testing WI.CSSQueryController
+
+
+== Running test suite: CSSQueryController
+-- Running test case: FindSpecialCharacterIndices
+PASS: Result for margin should match expected special indices.
+PASS: Result for MARGIN should match expected special indices.
+PASS: Result for margin-right should match expected special indices.
+PASS: Result for -webkit-mask should match expected special indices.
+PASS: Result for --var-kebab-case should match expected special indices.
+PASS: Result for --varCamelCase should match expected special indices.
+PASS: Result for --varlowercase should match expected special indices.
+PASS: Result for --VARUPPERCASE should match expected special indices.
+PASS: Result for --var_snake_case should match expected special indices.
+PASS: Result for var(--name) should match expected special indices.
+PASS: Result for rgb(0, 0, 0) should match expected special indices.
+PASS: Result for rgb(0 0 0 / 100%) should match expected special indices.
+PASS: Result for rgb(0 0 0 / 0.1) should match expected special indices.
+
+-- Running test case: ExecuteQueryAgainstNoValues
+PASS: Should return no results.
+
+-- Running test case: ExecuteWhitespaceQueryOrEmptyQuery
+PASS: Whitespace query should return no results.
+PASS: Whitespace query should return no results.
+PASS: Whitespace query should return no results.
+PASS: Whitespace query should return no results.
+PASS: Empty query should return no results.
+
+-- Running test case: ExecuteQueryMatchNone
+PASS: Query "abcde" shouldn't match "abcd".
+PASS: Query "abcd-" shouldn't match "abcde".
+PASS: Query "abcde" shouldn't match "abced".
+
+-- Running test case: ExecuteQueryMatchesExpectedCharacters
+PASS: Query "abcd" should match "abcd" in "abcde".
+PASS: Query "a-bcde" should match "a - bcde" in "abcde-abcde".
+PASS: Query "abcde" should match "A B C D E" in "AaBbCcDdEe".
+PASS: Query "abcde" should match "A B C De" in "AbcdBcdCdDe".
+PASS: Query "abcdex" should match "A B C d ex" in "AxBxCxdxexDxyxEF".
+PASS: Query "bc" should match " bC" in "abCd".
+PASS: Query "bb" should match " bB" in "abBc".
+
+-- Running test case: ExecuteQueryGeneralRankings
+PASS: Results should be ranked by descending relevancy.
+
+-- Running test case: ExecuteQueryPositionRankings
+PASS: Results should be ranked by descending relevancy.
+
+-- Running test case: GetMatchingTextRanges
+PASS: Result TextRanges should match the expected ranges.
+PASS: Result TextRanges should match the expected ranges.
+PASS: Result TextRanges should match the expected ranges.
+
Added: trunk/LayoutTests/inspector/unit-tests/css-query-controller.html (0 => 286611)
--- trunk/LayoutTests/inspector/unit-tests/css-query-controller.html (rev 0)
+++ trunk/LayoutTests/inspector/unit-tests/css-query-controller.html 2021-12-07 20:51:52 UTC (rev 286611)
@@ -0,0 +1,263 @@
+<!doctype html>
+<html>
+<head>
+<script src=""
+<script>
+function test()
+{
+ let suite = InspectorTest.createSyncSuite("CSSQueryController");
+
+ suite.addTestCase({
+ name: "FindSpecialCharacterIndices",
+ description: "Should correctly find special characters.",
+ test() {
+ let matcher = new WI.CSSQueryController;
+ let tests = [
+ {
+ padvalue: "margin",
+ expected: "^",
+ },
+ {
+ padvalue: "MARGIN",
+ expected: "^",
+ },
+ {
+ padvalue: "margin-right",
+ expected: "^ ^^",
+ },
+ {
+ padvalue: "-webkit-mask",
+ expected: "^^ ^^",
+ },
+ {
+ padvalue: "--var-kebab-case",
+ expected: "^^^ ^^ ^^",
+ },
+ {
+ padvalue: "--varCamelCase",
+ expected: "^^^ ^ ^",
+ },
+ {
+ padvalue: "--varlowercase",
+ expected: "^^^",
+ },
+ {
+ padvalue: "--VARUPPERCASE",
+ expected: "^^^",
+ },
+ {
+ padvalue: "--var_snake_case",
+ expected: "^^^ ^^ ^^",
+ },
+ {
+ padvalue: "var(--name)",
+ expected: "^ ^^^",
+ },
+ {
+ padvalue: "rgb(0, 0, 0)",
+ expected: "^",
+ },
+ {
+ padvalue: "rgb(0 0 0 / 100%)",
+ expected: "^",
+ },
+ {
+ padvalue: "rgb(0 0 0 / 0.1)",
+ expected: "^",
+ }
+ ];
+
+ function createSpecialMask(padvalue, specialIndices) {
+ let mask = " ".repeat(padvalue.length);
+ specialIndices.forEach((index) => {
+ mask = mask.substr(0, index) + "^" + mask.substr(index + 1);
+ });
+ return mask.trim();
+ }
+
+ for (let {padvalue, expected} of tests) {
+ let actual = createSpecialMask(padvalue, matcher._findSpecialCharacterIndices(padvalue));
+ InspectorTest.expectEqual(actual, expected, `Result for ${padvalue} should match expected special indices.`);
+ }
+ }
+ });
+
+ suite.addTestCase({
+ name: "ExecuteQueryAgainstNoValues",
+ description: "Should return no results if no values were added to the controller.",
+ test() {
+ let matcher = new WI.CSSQueryController;
+ let results = matcher.executeQuery("abcde");
+ InspectorTest.expectThat(!results.length, "Should return no results.")
+ }
+ });
+
+ suite.addTestCase({
+ name: "ExecuteWhitespaceQueryOrEmptyQuery",
+ description: "Empty queries and queries containing only whitespace should return no results.",
+ test() {
+ const whitespaceCharacters = " \t\r\n";
+ let matcher = new WI.CSSQueryController(["abcde"]);
+
+ for (let query of whitespaceCharacters) {
+ let results = matcher.executeQuery(query);
+ InspectorTest.expectThat(!results.length, "Whitespace query should return no results.");
+ }
+
+ let results = matcher.executeQuery("");
+ InspectorTest.expectThat(!results.length, "Empty query should return no results.");
+ }
+ });
+
+ suite.addTestCase({
+ name: "ExecuteQueryMatchNone",
+ description: "Should not find a match.",
+ test() {
+ let matcher = new WI.CSSQueryController;
+ let tests = [
+ {query: "abcde", value: "abcd"},
+ {query: "abcd-", value: "abcde"},
+ {query: "abcde", value: "abced"},
+ ];
+
+ for (let {query, value} of tests) {
+ matcher.reset();
+ matcher.addValues([value]);
+ let results = matcher.executeQuery(query);
+ InspectorTest.expectThat(!results.length, `Query "${query}" shouldn't match "${value}".`);
+ }
+ }
+ });
+
+ suite.addTestCase({
+ name: "ExecuteQueryMatchesExpectedCharacters",
+ description: "Should match the expected query characters.",
+ test() {
+ let matcher = new WI.CSSQueryController;
+ let tests = [
+ {
+ query: "abcd",
+ value: "abcde",
+ expected: "abcd"
+ },
+ {
+ query: "a-bcde",
+ value: "abcde-abcde",
+ expected: "a - bcde"
+ },
+ {
+ query: "abcde",
+ value: "AaBbCcDdEe",
+ expected: "A B C D E"
+ },
+ {
+ query: "abcde",
+ value: "AbcdBcdCdDe",
+ expected: "A B C De"
+ },
+ {
+ query: "abcdex",
+ value: "AxBxCxdxexDxyxEF",
+ expected: "A B C d ex"
+ },
+ {
+ query: "bc",
+ value: "abCd",
+ expected: " bC"
+ },
+ {
+ query: "bb",
+ value: "abBc",
+ expected: " bB"
+ }
+ ];
+
+ function createMatchesMask(queryResult) {
+ let value = queryResult.value;
+ let lastIndex = -1;
+ let result = "";
+
+ for (let match of queryResult.matches) {
+ let gap = " ".repeat(match.index - lastIndex - 1);
+ result += gap;
+ result += value[match.index];
+ lastIndex = match.index;
+ }
+
+ return result;
+ }
+
+ for (let {query, value, expected} of tests) {
+ matcher.reset();
+ matcher.addValues([value]);
+
+ let results = matcher.executeQuery(query);
+ InspectorTest.assert(results.length === 1, "Should return exactly one match.");
+ let actual = results.length ? createMatchesMask(results[0]) : null;
+ InspectorTest.expectEqual(actual, expected, `Query "${query}" should match "${expected}" in "${value}".`);
+ }
+ }
+ });
+
+ suite.addTestCase({
+ name: "ExecuteQueryGeneralRankings",
+ description: "Check that query results are ranked by descending relevance.",
+ test() {
+ // Values in order of descending rank.
+ let values = ["AbCdE", "AbcDe", "abcde", "AxbcDe", "AxBxCxDxEx", "AxbxcDe", "xabcde"];
+ let matcher = new WI.CSSQueryController(values);
+ let query = "abcde";
+ let results = matcher.executeQuery(query);
+ let resultValues = results.map((result) => result.value);
+ InspectorTest.expectShallowEqual(resultValues, values, "Results should be ranked by descending relevancy.");
+ }
+ });
+
+ suite.addTestCase({
+ name: "ExecuteQueryPositionRankings",
+ description: "Check that matches close to the beginning of the filename rank higher.",
+ test() {
+ // Values in order of descending rank.
+ let values = ["bcd", "BxCxDx", "AxBxCxDx", "abcd", "xxxAxxxBxxxCxxxD"];
+ let matcher = new WI.CSSQueryController(values);
+ let query = "bcd";
+ let results = matcher.executeQuery(query);
+ let resultValues = results.map((result) => result.value);
+ InspectorTest.expectShallowEqual(resultValues, values, "Results should be ranked by descending relevancy.");
+ }
+ });
+
+ suite.addTestCase({
+ name: "GetMatchingTextRanges",
+ description: "Check that query result TextRanges are correct.",
+ test() {
+ function textRange(start, end) {
+ return new WI.TextRange(0, start, 0, end);
+ }
+
+ let matcher = new WI.CSSQueryController;
+ let tests = [
+ {value: "a", ranges: []},
+ {value: "abcde", ranges: [textRange(0, 5)]},
+ {value: "AxBxCxDe", ranges: [textRange(0, 1), textRange(2, 3), textRange(4, 5), textRange(6, 8)]},
+ ];
+
+ for (let {value, ranges} of tests) {
+ matcher.reset();
+ matcher.addValues([value]);
+
+ let results = matcher.executeQuery("abcde");
+ let resultTextRanges = results.length ? results[0].matchingTextRanges : [];
+ InspectorTest.expectEqual(JSON.stringify(resultTextRanges), JSON.stringify(ranges), "Result TextRanges should match the expected ranges.");
+ }
+ }
+ });
+
+ suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body _onload_="runTest()">
+ <p>Testing WI.CSSQueryController</p>
+</body>
+</html>
Modified: trunk/LayoutTests/inspector/unit-tests/string-utilities-expected.txt (286610 => 286611)
--- trunk/LayoutTests/inspector/unit-tests/string-utilities-expected.txt 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/LayoutTests/inspector/unit-tests/string-utilities-expected.txt 2021-12-07 20:51:52 UTC (rev 286611)
@@ -70,3 +70,21 @@
PASS: The letter 'c' and 'e' are escaped.
PASS: The letter 'c', 'd', and 'e' are escaped.
+-- Running test case: String.prototype.isLowerCase
+PASS: String with single lowercase character should be lowercase.
+PASS: String with multiple lowercase characters should be lowercase.
+PASS: String with single uppercase character should not be lowercase.
+PASS: String with mixed case characters should not be lowercase.
+PASS: Empty string should not be lowercase.
+PASS: String with non-alpha character should not be lowercase.
+PASS: String with numeric character should not be lowercase.
+
+-- Running test case: String.prototype.isUpperCase
+PASS: String with single uppercase character should be uppercase.
+PASS: String with multiple uppercase characters should be uppercase.
+PASS: String with single lowercase character should not be uppercase.
+PASS: String with mixed case characters should not be uppercase.
+PASS: Empty string should not be uppercase.
+PASS: String with non-alpha character should not be uppercase.
+PASS: String with numeric character should not be uppercase.
+
Modified: trunk/LayoutTests/inspector/unit-tests/string-utilities.html (286610 => 286611)
--- trunk/LayoutTests/inspector/unit-tests/string-utilities.html 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/LayoutTests/inspector/unit-tests/string-utilities.html 2021-12-07 20:51:52 UTC (rev 286611)
@@ -125,6 +125,36 @@
}
});
+ suite.addTestCase({
+ name: "String.prototype.isLowerCase",
+ test() {
+ InspectorTest.expectTrue("a".isLowerCase(), "String with single lowercase character should be lowercase.");
+ InspectorTest.expectTrue("abc".isLowerCase(), "String with multiple lowercase characters should be lowercase.");
+ InspectorTest.expectFalse("A".isLowerCase(), "String with single uppercase character should not be lowercase.");
+ InspectorTest.expectFalse("aBc".isLowerCase(), "String with mixed case characters should not be lowercase.");
+ InspectorTest.expectFalse("".isLowerCase(), "Empty string should not be lowercase.");
+ InspectorTest.expectFalse(".".isLowerCase(), "String with non-alpha character should not be lowercase.");
+ InspectorTest.expectFalse("1".isLowerCase(), "String with numeric character should not be lowercase.");
+
+ return true;
+ }
+ });
+
+ suite.addTestCase({
+ name: "String.prototype.isUpperCase",
+ test() {
+ InspectorTest.expectTrue("A".isUpperCase(), "String with single uppercase character should be uppercase.");
+ InspectorTest.expectTrue("ABC".isUpperCase(), "String with multiple uppercase characters should be uppercase.");
+ InspectorTest.expectFalse("a".isUpperCase(), "String with single lowercase character should not be uppercase.");
+ InspectorTest.expectFalse("AbC".isUpperCase(), "String with mixed case characters should not be uppercase.");
+ InspectorTest.expectFalse("".isUpperCase(), "Empty string should not be uppercase.");
+ InspectorTest.expectFalse(".".isUpperCase(), "String with non-alpha character should not be uppercase.");
+ InspectorTest.expectFalse("1".isUpperCase(), "String with numeric character should not be uppercase.");
+
+ return true;
+ }
+ });
+
suite.runTestCasesAndFinish();
}
</script>
Modified: trunk/Source/WebInspectorUI/ChangeLog (286610 => 286611)
--- trunk/Source/WebInspectorUI/ChangeLog 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/ChangeLog 2021-12-07 20:51:52 UTC (rev 286611)
@@ -1,3 +1,121 @@
+2021-12-07 Razvan Caliman <[email protected]>
+
+ Web Inspector: Support fuzzy matching in CSS completions
+ https://bugs.webkit.org/show_bug.cgi?id=230351
+ <rdar://82976292>
+
+ Reviewed by Devin Rousso.
+
+ Use fuzzy matching for identifying CSS completions in the Styles details sidebar.
+
+ There are three main parts to this patch:
+ 1. Introduce `WI.CSSQueryController` with logic to do fuzzy matching on provided values.
+ 2. Change `WI.CompletionSuggestionsView` to support both plain strings and `WI.QueryResult`s.
+ 3. Change `WI.SpreadsheetTextField` so its `value` doesn't always return its `element.textContent`.
+
+ With fuzzy matching, completions are not guaranteed anymore to be prefixed with the query.
+ Therefore, it's no longer viable to rely on `WI.SpreadsheetTextField.value` being a concatenation of `textContent` of nodes within.
+
+ To adress this, there's now `WI.SpreadsheetTextField._pendingValue` which includes the prospective
+ completion regardless of whether the query is a completion prefix or at match at any other position.
+
+ * Localizations/en.lproj/localizedStrings.js:
+ * UserInterface/Base/Setting.js:
+ Add a flag to enable the fuzzy matching feature.
+
+ * UserInterface/Controllers/CSSQueryController.js: Added.
+ (WI.CSSQueryController):
+ (WI.CSSQueryController.prototype.addValues):
+ (WI.CSSQueryController.prototype.reset):
+ (WI.CSSQueryController.prototype.executeQuery):
+ (WI.CSSQueryController.prototype._findSpecialCharacterIndices):
+ Add a speclialized class to hold the logic for fuzzy matching of CSS properties and values.
+ It clones logic from `WI.ResouceQueryController` with adjustments specific to CSS; more to follow as the feature gets refined.
+
+ * UserInterface/Main.html:
+ * UserInterface/Models/CSSCompletions.js:
+ (WI.CSSCompletions):
+ (WI.CSSCompletions.prototype.addValues):
+ (WI.CSSCompletions.prototype.executeQuery):
+ Support both the current prefix matching approach as well as fuzzy matching.
+
+ * UserInterface/Models/CSSKeywordCompletions.js:
+ (WI.CSSKeywordCompletions.forPartialPropertyName):
+ Opt into fuzzy matching when getting completions for property names and values.
+
+ * UserInterface/Models/QueryResult.js:
+ (WI.QueryResult.prototype.get matches):
+ * UserInterface/Test.html:
+
+ * UserInterface/Views/CompletionSuggestionsView.css:
+ (.completion-suggestions-container > .item > .highlighted):
+ Highlight specific characters that matched in result identified by fuzzy matching.
+
+ * UserInterface/Views/CompletionSuggestionsView.js:
+ (WI.CompletionSuggestionsView.prototype.selectNext):
+ (WI.CompletionSuggestionsView.prototype.selectPrevious):
+ (WI.CompletionSuggestionsView.prototype.update):
+ (WI.CompletionSuggestionsView.prototype.getCompletionText):
+ (WI.CompletionSuggestionsView.prototype._createHighlightedCompletionFragment):
+ Change `WI.CompletionSuggestionsView` to hold a list of completions,
+ either strings or `WI.QueryResult`, and return the appropriate completion text
+ instead of returning the `textContent` of the selected element.
+
+ `WI.CompletionSuggestionsView` is used elsewhere in the Console and Sources panel
+ so avoid impacting those consumers until they opt in to fuzzy matching as well.
+
+ * UserInterface/Views/SettingsTabContentView.js:
+
+ * UserInterface/Views/SpreadsheetStyleProperty.js:
+ (WI.SpreadsheetStyleProperty.prototype._handleNameChange):
+ (WI.SpreadsheetStyleProperty.prototype._handleValueChange):
+ (WI.SpreadsheetStyleProperty.prototype._nameCompletionDataProvider):
+ (WI.SpreadsheetStyleProperty.prototype._valueCompletionDataProvider):
+ On change, get the value of the `WI.SpreadsheetTextField` instead of
+ the corresponding element's `textContent`.
+
+ * UserInterface/Views/SpreadsheetTextField.js:
+ Introduced `SpreadsheetTextField._completionPrefix` to hold the query a user is typing.
+
+ Introduced `SpreadsheetTextField._completionText` to hold the completion text of the selected but
+ not yet applied completion. This replaces the previous behavior of concatenating the query and `suggestionHint`
+ because with fuzzy matching the completion isn't guaranteed to be prefixed with the query.
+
+ Introduced `SpreadsheetTextField._pendingValue` to hold the result of applying the selected
+ completion. This replaces the previous behavior of `WI.SpreadsheetTextField._combineEditorElementChildren`.
+
+ (WI.SpreadsheetTextField):
+ (WI.SpreadsheetTextField.prototype.get value):
+ Change `WI.SpreadsheetTextField` so its value is divorced from its element's `textContent`.
+ While editing, `WI.SpreadsheetTextField.value` returns the pending value so that
+ a valid CSS string gets written to the stylesheet.
+
+ (WI.SpreadsheetTextField.prototype.set suggestionHint):
+ If the query is a prefix for the selected completion, keep the existing behavior of appending
+ the suggestion hint substring. Otherwise, hide the suggestion hint element because the
+ concatenation doesn't make sense.
+
+ (WI.SpreadsheetTextField.prototype.stopEditing):
+ (WI.SpreadsheetTextField.prototype.discardCompletion):
+ (WI.SpreadsheetTextField.prototype.completionSuggestionsSelectedCompletion):
+ (WI.SpreadsheetTextField.prototype.completionSuggestionsClickedCompletion):
+ (WI.SpreadsheetTextField.prototype._discardChange):
+ (WI.SpreadsheetTextField.prototype._handleBlur):
+ (WI.SpreadsheetTextField.prototype._handleKeyDown):
+ (WI.SpreadsheetTextField.prototype._handleKeyDownForSuggestionView):
+ (WI.SpreadsheetTextField.prototype._handleInput):
+ (WI.SpreadsheetTextField.prototype._updateCompletions):
+ (WI.SpreadsheetTextField.prototype._applyPendingValue):
+ After applying a completion, the value is replaced with `WI.SpreadsheetTextField._pendingValue`.
+ At this point, `WI.SpreadsheetTextField.value` and the `textContent` of the element converge.
+
+ (WI.SpreadsheetTextField.prototype._updatePendingValueWithCompletionText):
+ When presented with a completion, the query gets substituted with the full completion text in
+ `WI.SpreadsheetTextField._pendingValue` regardless of whether the query is a prefix or not.
+
+ (WI.SpreadsheetTextField.prototype._applyCompletionHint): Deleted.
+ (WI.SpreadsheetTextField.prototype._combineEditorElementChildren): Deleted.
+
2021-12-06 Patrick Angle <[email protected]>
Web Inspector: Support Cascade Layers in the Styles sidebar
Modified: trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -1607,6 +1607,7 @@
localizedStrings["Use Default Appearance"] = "Use Default Appearance";
localizedStrings["Use Mock Capture Devices"] = "Use Mock Capture Devices";
localizedStrings["Use default media styles"] = "Use default media styles";
+localizedStrings["Use fuzzy matching for completion suggestions"] = "Use fuzzy matching for completion suggestions";
localizedStrings["Use the resource cache when loading resources"] = "Use the resource cache when loading resources";
localizedStrings["User Agent"] = "User Agent";
localizedStrings["User Agent Style Sheet"] = "User Agent Style Sheet";
Modified: trunk/Source/WebInspectorUI/UserInterface/Base/Setting.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Base/Setting.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Base/Setting.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -231,6 +231,7 @@
experimentalEnableStylesJumpToVariableDeclaration: new WI.Setting("experimental-styles-jump-to-variable-declaration", false),
experimentalCollapseBlackboxedCallFrames: new WI.Setting("experimental-collapse-blackboxed-call-frames", false),
experimentalAllowInspectingInspector: new WI.Setting("experimental-allow-inspecting-inspector", false),
+ experimentalCSSCompletionFuzzyMatching: new WI.Setting("experimental-css-completion-fuzzy-matching", false),
// Protocol
protocolLogAsText: new WI.Setting("protocol-log-as-text", false),
Modified: trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -773,7 +773,7 @@
{
value()
{
- return String(this) === this.toLowerCase();
+ return /^[a-z]+$/.test(this);
}
});
@@ -781,7 +781,7 @@
{
value()
{
- return String(this) === this.toUpperCase();
+ return /^[A-Z]+$/.test(this);
}
});
Added: trunk/Source/WebInspectorUI/UserInterface/Controllers/CSSQueryController.js (0 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Controllers/CSSQueryController.js (rev 0)
+++ trunk/Source/WebInspectorUI/UserInterface/Controllers/CSSQueryController.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -0,0 +1,115 @@
+/*
+ * 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 met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.CSSQueryController = class CSSQueryController extends WI.QueryController
+{
+ constructor(values)
+ {
+ console.assert(Array.isArray(values), values);
+
+ super();
+
+ this._values = values || [];
+ this._cachedSpecialCharacterIndicesForValueMap = new Map;
+ }
+
+ // Public
+
+ addValues(values)
+ {
+ console.assert(Array.isArray(values), values);
+ if (!values.length)
+ return;
+
+ this._values.pushAll(values);
+ }
+
+ reset()
+ {
+ this._values = [];
+ this._cachedSpecialCharacterIndicesForValueMap.clear();
+ }
+
+ executeQuery(query)
+ {
+ if (!query || !this._values.length)
+ return [];
+
+ query = query.toLowerCase();
+
+ let results = [];
+
+ for (let value of this._values) {
+ if (!this._cachedSpecialCharacterIndicesForValueMap.has(value))
+ this._cachedSpecialCharacterIndicesForValueMap.set(value, this._findSpecialCharacterIndices(value));
+
+ let matches = this.findQueryMatches(query, value.toLowerCase(), this._cachedSpecialCharacterIndicesForValueMap.get(value));
+ if (matches.length)
+ results.push(new WI.QueryResult(value, matches));
+ }
+
+ return results.sort((a, b) => {
+ if (a.rank === b.rank)
+ return a.value.extendedLocaleCompare(b.value);
+ return b.rank - a.rank;
+ });
+ }
+
+ // Private
+
+ _findSpecialCharacterIndices(string)
+ {
+ if (!string.length)
+ return [];
+
+ const separators = "-_";
+
+ // Special characters include the following:
+ // 1. The first character.
+ // 2. Uppercase characters that follow a lowercase letter.
+ // 3. Separators and the first character following the separator.
+ let indices = [0];
+
+ for (let i = 1; i < string.length; ++i) {
+ let character = string[i];
+ let isSpecial = false;
+
+ if (separators.includes(character))
+ isSpecial = true;
+ else {
+ let previousCharacter = string[i - 1];
+ if (separators.includes(previousCharacter))
+ isSpecial = true;
+ else if (character.isUpperCase() && previousCharacter.isLowerCase())
+ isSpecial = true;
+ }
+
+ if (isSpecial)
+ indices.push(i);
+ }
+
+ return indices;
+ }
+};
Modified: trunk/Source/WebInspectorUI/UserInterface/Main.html (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Main.html 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Main.html 2021-12-07 20:51:52 UTC (rev 286611)
@@ -927,6 +927,7 @@
<script src=""
<script src=""
<script src=""
+ <script src=""
<script src=""
<script src=""
<script src=""
Modified: trunk/Source/WebInspectorUI/UserInterface/Models/CSSCompletions.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Models/CSSCompletions.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Models/CSSCompletions.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -58,6 +58,7 @@
this._values.sort();
this._acceptEmptyPrefix = acceptEmptyPrefix;
+ this._queryController = null;
}
// Static
@@ -253,8 +254,17 @@
this._values.pushAll(values);
this._values.sort();
+
+ this._queryController?.addValues(values);
}
+ executeQuery(query)
+ {
+ this._queryController ||= new WI.CSSQueryController(this._values);
+
+ return this._queryController.executeQuery(query);
+ }
+
startsWith(prefix)
{
if (!prefix)
Modified: trunk/Source/WebInspectorUI/UserInterface/Models/CSSKeywordCompletions.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Models/CSSKeywordCompletions.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Models/CSSKeywordCompletions.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -31,7 +31,7 @@
WI.CSSKeywordCompletions = {};
-WI.CSSKeywordCompletions.forPartialPropertyName = function(text, {caretPosition, allowEmptyPrefix} = {})
+WI.CSSKeywordCompletions.forPartialPropertyName = function(text, {caretPosition, allowEmptyPrefix, useFuzzyMatching} = {})
{
caretPosition ??= text.length;
allowEmptyPrefix ??= false;
@@ -42,10 +42,17 @@
if (!text.length && allowEmptyPrefix)
return {prefix: text, completions: WI.CSSCompletions.cssNameCompletions.values};
- return {prefix: text, completions:WI.CSSCompletions.cssNameCompletions.startsWith(text)};
+
+ let completions;
+ if (useFuzzyMatching)
+ completions = WI.CSSCompletions.cssNameCompletions.executeQuery(text);
+ else
+ completions = WI.CSSCompletions.cssNameCompletions.startsWith(text);
+
+ return {prefix: text, completions};
};
-WI.CSSKeywordCompletions.forPartialPropertyValue = function(text, propertyName, {caretPosition, additionalFunctionValueCompletionsProvider} = {})
+WI.CSSKeywordCompletions.forPartialPropertyValue = function(text, propertyName, {caretPosition, additionalFunctionValueCompletionsProvider, useFuzzyMatching} = {})
{
caretPosition ??= text.length;
@@ -86,10 +93,15 @@
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 the current token value is a comma or open parenthesis, treat it as if we are at the start of a new token.
if (currentTokenValue === "(" || currentTokenValue === ",")
currentTokenValue = "";
+ // It's not valid CSS to append completions immediately after a closing parenthesis.
+ let tokenBeforeCaret = tokens[indexOfTokenAtCaret - 1];
+ if (currentTokenValue === ")" || tokenBeforeCaret?.value === ")")
+ return {prefix: "", completions: []};
+
let functionName = null;
let preceedingFunctionDepth = 0;
for (let i = indexOfTokenAtCaret; i >= 0; --i) {
@@ -108,14 +120,20 @@
}
}
+ let valueCompletions;
if (functionName) {
- let completions = WI.CSSKeywordCompletions.forFunction(functionName);
- let contextualValueCompletions = additionalFunctionValueCompletionsProvider?.(functionName) || [];
- completions.addValues(contextualValueCompletions);
- return {prefix: currentTokenValue, completions: completions.startsWith(currentTokenValue)};
- }
+ valueCompletions = WI.CSSKeywordCompletions.forFunction(functionName);
+ valueCompletions.addValues(additionalFunctionValueCompletionsProvider?.(functionName) ?? []);
+ } else
+ valueCompletions = WI.CSSKeywordCompletions.forProperty(propertyName);
- return {prefix: currentTokenValue, completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(currentTokenValue)};
+ let completions;
+ if (useFuzzyMatching)
+ completions = valueCompletions.executeQuery(currentTokenValue);
+ else
+ completions = valueCompletions.startsWith(currentTokenValue);
+
+ return {prefix: currentTokenValue, completions};
};
WI.CSSKeywordCompletions.forProperty = function(propertyName)
Modified: trunk/Source/WebInspectorUI/UserInterface/Models/QueryResult.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Models/QueryResult.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Models/QueryResult.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -36,6 +36,7 @@
// Public
get value() { return this._value; }
+ get matches() { return this._matches; }
get rank()
{
Modified: trunk/Source/WebInspectorUI/UserInterface/Test.html (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Test.html 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Test.html 2021-12-07 20:51:52 UTC (rev 286611)
@@ -253,6 +253,7 @@
<script src=""
<script src=""
<script src=""
+ <script src=""
<script src=""
<script src=""
<script src=""
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.css (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.css 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.css 2021-12-07 20:51:52 UTC (rev 286611)
@@ -70,6 +70,10 @@
color: var(--text-color);
}
+.completion-suggestions-container > .item > .matched {
+ font-weight: bold;
+}
+
.completion-suggestions-container:not(:active) > .item.selected,
.completion-suggestions-container > .item:active {
background-color: var(--selected-background-color);
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -32,6 +32,7 @@
this._delegate = delegate || null;
this._preventBlur = preventBlur || false;
+ this._completions = [];
this._selectedIndex = NaN;
this._moveIntervalIdentifier = null;
@@ -89,9 +90,8 @@
else
++this.selectedIndex;
- var selectedItemElement = this._selectedItemElement;
- if (selectedItemElement && this._delegate && typeof this._delegate.completionSuggestionsSelectedCompletion === "function")
- this._delegate.completionSuggestionsSelectedCompletion(this, selectedItemElement.textContent);
+ if (this._completions[this.selectedIndex])
+ this._delegate?.completionSuggestionsSelectedCompletion?.(this, this.getCompletionText(this._completions[this.selectedIndex]));
}
selectPrevious()
@@ -101,9 +101,8 @@
else
--this.selectedIndex;
- var selectedItemElement = this._selectedItemElement;
- if (selectedItemElement && this._delegate && typeof this._delegate.completionSuggestionsSelectedCompletion === "function")
- this._delegate.completionSuggestionsSelectedCompletion(this, selectedItemElement.textContent);
+ if (this._completions[this.selectedIndex])
+ this._delegate?.completionSuggestionsSelectedCompletion?.(this, this.getCompletionText(this._completions[this.selectedIndex]));
}
isHandlingClickEvent()
@@ -175,22 +174,39 @@
update(completions, selectedIndex)
{
this._containerElement.removeChildren();
+ this._completions = completions;
if (typeof selectedIndex === "number")
this._selectedIndex = selectedIndex;
- for (var i = 0; i < completions.length; ++i) {
+ for (let [index, completion] of completions.entries()) {
var itemElement = document.createElement("div");
itemElement.classList.add("item");
- itemElement.classList.toggle("selected", i === this._selectedIndex);
- itemElement.textContent = completions[i];
+ itemElement.classList.toggle("selected", index === this._selectedIndex);
+
+ if (typeof completion === "string")
+ itemElement.textContent = completion;
+ else if (completion instanceof WI.QueryResult)
+ itemElement.appendChild(this._createMatchedCompletionFragment(completion.value, completion.matchingTextRanges));
+
this._containerElement.appendChild(itemElement);
-
- if (this._delegate && typeof this._delegate.completionSuggestionsViewCustomizeCompletionElement === "function")
- this._delegate.completionSuggestionsViewCustomizeCompletionElement(this, itemElement, completions[i]);
+ this._delegate?.completionSuggestionsViewCustomizeCompletionElement?.(this, itemElement, completion);
}
}
+ getCompletionText(completion)
+ {
+ console.assert(typeof completion === "string" || completion instanceof WI.QueryResult, completion);
+
+ if (typeof completion === "string")
+ return completion;
+
+ if (completion instanceof WI.QueryResult)
+ return completion.value;
+
+ return "";
+ }
+
// Private
get _selectedItemElement()
@@ -203,6 +219,31 @@
return element;
}
+ _createMatchedCompletionFragment(completionText, matchingTextRanges)
+ {
+ let completionFragment = document.createDocumentFragment();
+ let lastIndex = 0;
+ for (let textRange of matchingTextRanges) {
+ console.assert(textRange.startColumn >= 0 && textRange.startColumn < completionText.length, textRange);
+ console.assert(textRange.endColumn > 0 && textRange.endColumn <= completionText.length, textRange);
+ console.assert(textRange.startColumn < textRange.endColumn);
+
+ if (textRange.startColumn > lastIndex)
+ completionFragment.append(completionText.substring(lastIndex, textRange.startColumn));
+
+ let matchedSpan = document.createElement("span");
+ matchedSpan.classList.add("matched");
+ matchedSpan.append(completionText.substring(textRange.startColumn, textRange.endColumn));
+ completionFragment.append(matchedSpan);
+ lastIndex = textRange.endColumn;
+ }
+
+ if (lastIndex < completionText.length)
+ completionFragment.append(completionText.substring(lastIndex, completionText.length));
+
+ return completionFragment;
+ }
+
_mouseDown(event)
{
if (event.button !== 0)
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -397,6 +397,7 @@
let stylesGroup = experimentalSettingsView.addGroup(WI.UIString("Styles:"));
stylesGroup.addSetting(WI.settings.experimentalEnableStylesJumpToEffective, WI.UIString("Show jump to effective property button"));
stylesGroup.addSetting(WI.settings.experimentalEnableStylesJumpToVariableDeclaration, WI.UIString("Show jump to variable declaration button"));
+ stylesGroup.addSetting(WI.settings.experimentalCSSCompletionFuzzyMatching, WI.UIString("Use fuzzy matching for completion suggestions"));
experimentalSettingsView.addSeparator();
}
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -936,12 +936,12 @@
_handleNameChange()
{
- this._property.name = this._nameElement.textContent.trim();
+ this._property.name = this._nameTextField.value;
}
_handleValueChange()
{
- let value = this._valueElement.textContent;
+ let value = this._valueTextField.value;
this._property.rawValue = value.trim();
@@ -975,9 +975,9 @@
}
}
- _nameCompletionDataProvider(text, {caretPosition, allowEmptyPrefix} = {})
+ _nameCompletionDataProvider(text, options = {})
{
- return WI.CSSKeywordCompletions.forPartialPropertyName(text, {caretPosition, allowEmptyPrefix});
+ return WI.CSSKeywordCompletions.forPartialPropertyName(text, options);
}
_handleValueBeforeInput(event)
@@ -999,9 +999,10 @@
this.spreadsheetTextFieldDidCommit(this._valueTextField, {direction: "forward"});
}
- _valueCompletionDataProvider(text, {caretPosition, allowEmptyPrefix} = {})
+ _valueCompletionDataProvider(text, options = {})
{
- return WI.CSSKeywordCompletions.forPartialPropertyValue(text, this._nameElement.textContent.trim(), {caretPosition, additionalFunctionValueCompletionsProvider: this.additionalFunctionValueCompletionsProvider.bind(this)});
+ options.additionalFunctionValueCompletionsProvider = this.additionalFunctionValueCompletionsProvider.bind(this);
+ return WI.CSSKeywordCompletions.forPartialPropertyValue(text, this._nameElement.textContent.trim(), options);
}
_setupJumpToSymbol(element)
Modified: trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js (286610 => 286611)
--- trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js 2021-12-07 20:51:03 UTC (rev 286610)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js 2021-12-07 20:51:52 UTC (rev 286611)
@@ -29,6 +29,7 @@
{
this._delegate = delegate;
this._element = element;
+ this._pendingValue = null;
this._completionProvider = completionProvider || null;
if (this._completionProvider) {
@@ -52,6 +53,7 @@
this._keyDownCaretPosition = -1;
this._valueBeforeEditing = "";
this._completionPrefix = "";
+ this._completionText = "";
this._controlSpaceKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, WI.KeyboardShortcut.Key.Space);
}
@@ -61,9 +63,18 @@
get editing() { return this._editing; }
- get value() { return this._element.textContent; }
- set value(value) { this._element.textContent = value; }
+ get value()
+ {
+ return this._pendingValue ?? this._element.textContent;
+ }
+ set value(value)
+ {
+ this._element.textContent = value;
+
+ this._pendingValue = null;
+ }
+
valueWithoutSuggestion()
{
// The suggestion could appear anywhere within the element, and the text of the element can span multiple nodes.
@@ -94,9 +105,6 @@
}
this._suggestionHintElement.remove();
-
- // Removing the suggestion hint element may leave the contents of `_element` fragmented into multiple text nodes.
- this._combineEditorElementChildren();
}
startEditing()
@@ -130,6 +138,8 @@
this._editing = false;
this._valueBeforeEditing = "";
+ this._pendingValue = null;
+ this._completionText = "";
this._element.classList.remove("editing");
this._element.contentEditable = false;
@@ -143,10 +153,16 @@
this._suggestionsView.hide();
- let hadSuggestionHint = !!this.suggestionHint;
+ let hadCompletionText = this._completionText.length > 0;
+
+ // Resetting the suggestion hint removes any suggestion hint element that is attached.
this.suggestionHint = "";
- if (hadSuggestionHint && this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
- this._delegate.spreadsheetTextFieldDidChange(this);
+ this._completionText = "";
+
+ if (hadCompletionText) {
+ this._pendingValue = this.valueWithoutSuggestion();
+ this._delegate?.spreadsheetTextFieldDidChange?.(this);
+ }
}
detached()
@@ -157,22 +173,26 @@
// CompletionSuggestionsView delegate
- completionSuggestionsSelectedCompletion(suggestionsView, selectedText = "")
+ completionSuggestionsSelectedCompletion(suggestionsView, completionText = "")
{
- this.suggestionHint = selectedText.slice(this._completionPrefix.length);
+ this._completionText = completionText;
- if (this.suggestionHint.length)
- this._reAttachSuggestionHint();
+ if (this._completionText.startsWith(this._completionPrefix))
+ this.suggestionHint = this._completionText.slice(this._completionPrefix.length);
+ else
+ this.suggestionHint = "";
+ this._updatePendingValueWithCompletionText();
+
if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
this._delegate.spreadsheetTextFieldDidChange(this);
}
- completionSuggestionsClickedCompletion(suggestionsView, selectedText)
+ completionSuggestionsClickedCompletion(suggestionsView, completionText)
{
- this.suggestionHint = selectedText.slice(this._completionPrefix.length);
-
- this._applyCompletionHint({moveCaretToEndOfCompletion: true});
+ this._completionText = completionText;
+ this._updatePendingValueWithCompletionText();
+ this._applyPendingValue({moveCaretToEndOfCompletion: true});
this.discardCompletion();
if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
@@ -219,7 +239,7 @@
if (document.activeElement === this._element)
return;
- this._applyCompletionHint();
+ this._applyPendingValue();
this.discardCompletion();
let changed = this._valueBeforeEditing !== this.value;
@@ -252,7 +272,7 @@
let isTabKey = event.key === "Tab";
if (isEnterKey || isTabKey) {
event.stop();
- this._applyCompletionHint();
+ this._applyPendingValue();
let direction = (isTabKey && event.shiftKey) ? "backward" : "forward";
@@ -338,12 +358,12 @@
return true;
}
- if (event.key === "ArrowRight" && this.suggestionHint.length) {
+ if (event.key === "ArrowRight" && this._completionText.length) {
let selection = window.getSelection();
if (selection.isCollapsed) {
event.stop();
- this._applyCompletionHint({moveCaretToEndOfCompletion: true});
+ this._applyPendingValue({moveCaretToEndOfCompletion: true});
// When completing "background", don't hide the completion popover.
// Continue showing the popover with properties such as "background-color" and "background-image".
@@ -358,21 +378,14 @@
if (event.key === "Escape" && this._suggestionsView.visible) {
event.stop();
-
- let willChange = !!this.suggestionHint;
this.discardCompletion();
- if (willChange && this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
- this._delegate.spreadsheetTextFieldDidChange(this);
-
return true;
}
- if (event.key === "ArrowLeft" && (this.suggestionHint || this._suggestionsView.visible)) {
+ if (event.key === "ArrowLeft" && (this._completionText.length || this._suggestionsView.visible)) {
this.discardCompletion();
- if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
- this._delegate.spreadsheetTextFieldDidChange(this);
return true;
}
@@ -405,6 +418,7 @@
if (!this._editing)
return;
+ this._pendingValue = this.valueWithoutSuggestion().trim();
this._preventDiscardingCompletionsOnKeyUp = true;
this._updateCompletions();
@@ -417,8 +431,9 @@
if (!this._completionProvider)
return;
+ let useFuzzyMatching = WI.settings.experimentalCSSCompletionFuzzyMatching.value;
let valueWithoutSuggestion = this.valueWithoutSuggestion();
- let {completions, prefix} = this._completionProvider(valueWithoutSuggestion, {allowEmptyPrefix: forceCompletions, caretPosition: this._getCaretPosition()});
+ let {completions, prefix} = this._completionProvider(valueWithoutSuggestion, {allowEmptyPrefix: forceCompletions, caretPosition: this._getCaretPosition(), useFuzzyMatching});
this._completionPrefix = prefix;
if (!completions.length) {
@@ -427,7 +442,7 @@
}
// No need to show the completion popover with only one item that matches the entered value.
- if (completions.length === 1 && completions[0] === valueWithoutSuggestion) {
+ if (completions.length === 1 && this._suggestionsView.getCompletionText(completions[0]) === valueWithoutSuggestion) {
this.discardCompletion();
return;
}
@@ -440,8 +455,9 @@
this._suggestionsView.update(completions);
- if (completions.length === 1) {
- // No need to show the completion popover that matches the suggestion hint.
+ if (completions.length === 1 && this._suggestionsView.getCompletionText(completions[0]).startsWith(this._completionPrefix)) {
+ // No need to show the completion popover with only one item that begins with the completion prefix.
+ // When using fuzzy matching, the completion prefix may not occur at the beginning of the suggestion.
this._suggestionsView.hide();
} else
this._showSuggestionsView();
@@ -530,23 +546,17 @@
return range;
}
- _applyCompletionHint({moveCaretToEndOfCompletion} = {})
+ _applyPendingValue({moveCaretToEndOfCompletion} = {})
{
- if (!this._completionProvider || !this.suggestionHint)
+ if (!this._pendingValue)
return;
- this._combineEditorElementChildren({newCaretPosition: moveCaretToEndOfCompletion ? this._getCaretPosition() + this.suggestionHint.length : null});
- }
+ let caretPosition = this._getCaretPosition();
+ let newCaretPosition = moveCaretToEndOfCompletion ? caretPosition - this._completionPrefix.length + this._completionText.length : caretPosition;
- _combineEditorElementChildren({newCaretPosition} = {})
- {
- newCaretPosition ??= this._getCaretPosition();
+ // Setting the value collapses the text selection. Get the caret position before doing this.
+ this.value = this._pendingValue;
- // Setting the textContent of the element to its current textContent will take the text from the multiple
- // potential child nodes (potentially a suggestion hint node and some number of existing text nodes) and turn
- // them into a single text node within the element.
- this._element.textContent = this._element.textContent;
-
if (this._element.textContent.length) {
let textChildNode = this._element.firstChild;
window.getSelection().setBaseAndExtent(textChildNode, newCaretPosition, textChildNode, newCaretPosition);
@@ -553,6 +563,14 @@
}
}
+ _updatePendingValueWithCompletionText()
+ {
+ let caretPosition = this._getCaretPosition();
+ let value = this.valueWithoutSuggestion();
+
+ this._pendingValue = value.slice(0, caretPosition - this._completionPrefix.length) + this._completionText + value.slice(caretPosition + 1, value.length);
+ }
+
_reAttachSuggestionHint()
{
console.assert(this.suggestionHint.length, "Suggestion hint should not be empty when attaching the suggestion hint element.");