Changeset 286611 in webkit


Ignore:
Timestamp:
Dec 7, 2021 12:51:52 PM (7 months ago)
Author:
Razvan Caliman
Message:

Web Inspector: Support fuzzy matching in CSS completions
https://bugs.webkit.org/show_bug.cgi?id=230351
<rdar://82976292>

Reviewed by Devin Rousso.

Source/WebInspectorUI:

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.QueryResults.
  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.

LayoutTests:

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.
Location:
trunk
Files:
3 added
19 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r286609 r286611  
     12021-12-07  Razvan Caliman  <rcaliman@apple.com>
     2
     3        Web Inspector: Support fuzzy matching in CSS completions
     4        https://bugs.webkit.org/show_bug.cgi?id=230351
     5        <rdar://82976292>
     6
     7        Reviewed by Devin Rousso.
     8
     9        Add test for `WI.CSSQueryController` to check fuzzy matching logic for CSS completions.
     10        Follows prior example from `LayoutTests/inspector/unit-tests/resource-query-controller.html`
     11        since `WI.ResouceQueryController` served as the model class that was adapted for CSS.
     12
     13        * inspector/unit-tests/css-query-controller-expected.txt: Added.
     14        * inspector/unit-tests/css-query-controller.html: Added.
     15
    1162021-12-07  Chris Dumez  <cdumez@apple.com>
    217
  • trunk/LayoutTests/inspector/unit-tests/css-keyword-completions-expected.txt

    r279502 r286611  
    7272PASS: All expected completions were present.
    7373
     74-- Running test case: WI.CSSKeywordCompletions.forPartialPropertyValue.NoWhitespaceAfterFunction
     75PASS: Expected result prefix to be ""
     76PASS: Expected exactly 0 completion results.
     77PASS: All expected completions were present.
     78
     79-- Running test case: WI.CSSKeywordCompletions.forPartialPropertyValue.WhitespaceAfterFunction
     80PASS: Expected result prefix to be "a"
     81PASS: All expected completions were present.
     82
  • trunk/LayoutTests/inspector/unit-tests/css-keyword-completions.html

    r279502 r286611  
    6767    });
    6868
    69     function addTestForPartialPropertyValue({name, description, propertyName, text, caretPosition, expectedPrefix, expectedCompletions, additionalFunctionValueCompletionsProvider}) {
     69    function addTestForPartialPropertyValue({name, description, propertyName, text, caretPosition, expectedPrefix, expectedCompletions, expectedCompletionCount, additionalFunctionValueCompletionsProvider}) {
    7070        suite.addTestCase({
    7171            name,
     
    7575                expectedPrefix ??= text;
    7676                expectedCompletions ??= [];
     77                expectedCompletionCount ??= -1;
    7778                additionalFunctionValueCompletionsProvider ??= () => {};
    7879
    7980                let completionResults = WI.CSSKeywordCompletions.forPartialPropertyValue(text, propertyName, {caretPosition, additionalFunctionValueCompletionsProvider});
    8081                InspectorTest.expectEqual(completionResults.prefix, expectedPrefix, `Expected result prefix to be "${expectedPrefix}"`);
     82
     83                if (expectedCompletionCount >= 0)
     84                    InspectorTest.expectEqual(completionResults.completions.length, expectedCompletionCount, `Expected exactly ${expectedCompletionCount} completion results.`);
    8185
    8286                // 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.
     
    230234    });
    231235
     236    // `hsl()a|`
     237    addTestForPartialPropertyValue({
     238        name: "WI.CSSKeywordCompletions.forPartialPropertyValue.NoWhitespaceAfterFunction",
     239        description: "Test that color completions are not offered immediately after a closing parenthesis",
     240        propertyName: "background",
     241        text: "hsl()a",
     242        caretPosition: 6,
     243        expectedPrefix: "",
     244        expectedCompletionCount: 0
     245    });
     246
     247    // `hsl() a|`
     248    addTestForPartialPropertyValue({
     249        name: "WI.CSSKeywordCompletions.forPartialPropertyValue.WhitespaceAfterFunction",
     250        description: "Test that color completions are offered when a closing parenthesis is followed by whitespace",
     251        propertyName: "background",
     252        text: "hsl() a",
     253        caretPosition: 7,
     254        expectedPrefix: "a",
     255        expectedCompletions: ["aliceblue", "antiquewhite"],
     256    });
     257
    232258    suite.runTestCasesAndFinish();
    233259}
  • trunk/LayoutTests/inspector/unit-tests/string-utilities-expected.txt

    r237644 r286611  
    7171PASS: The letter 'c', 'd', and 'e' are escaped.
    7272
     73-- Running test case: String.prototype.isLowerCase
     74PASS: String with single lowercase character should be lowercase.
     75PASS: String with multiple lowercase characters should be lowercase.
     76PASS: String with single uppercase character should not be lowercase.
     77PASS: String with mixed case characters should not be lowercase.
     78PASS: Empty string should not be lowercase.
     79PASS: String with non-alpha character should not be lowercase.
     80PASS: String with numeric character should not be lowercase.
     81
     82-- Running test case: String.prototype.isUpperCase
     83PASS: String with single uppercase character should be uppercase.
     84PASS: String with multiple uppercase characters should be uppercase.
     85PASS: String with single lowercase character should not be uppercase.
     86PASS: String with mixed case characters should not be uppercase.
     87PASS: Empty string should not be uppercase.
     88PASS: String with non-alpha character should not be uppercase.
     89PASS: String with numeric character should not be uppercase.
     90
  • trunk/LayoutTests/inspector/unit-tests/string-utilities.html

    r237644 r286611  
    126126    });
    127127
     128    suite.addTestCase({
     129        name: "String.prototype.isLowerCase",
     130        test() {
     131            InspectorTest.expectTrue("a".isLowerCase(), "String with single lowercase character should be lowercase.");
     132            InspectorTest.expectTrue("abc".isLowerCase(), "String with multiple lowercase characters should be lowercase.");
     133            InspectorTest.expectFalse("A".isLowerCase(), "String with single uppercase character should not be lowercase.");
     134            InspectorTest.expectFalse("aBc".isLowerCase(), "String with mixed case characters should not be lowercase.");
     135            InspectorTest.expectFalse("".isLowerCase(), "Empty string should not be lowercase.");
     136            InspectorTest.expectFalse(".".isLowerCase(), "String with non-alpha character should not be lowercase.");
     137            InspectorTest.expectFalse("1".isLowerCase(), "String with numeric character should not be lowercase.");
     138
     139            return true;
     140        }
     141    });
     142
     143    suite.addTestCase({
     144        name: "String.prototype.isUpperCase",
     145        test() {
     146            InspectorTest.expectTrue("A".isUpperCase(), "String with single uppercase character should be uppercase.");
     147            InspectorTest.expectTrue("ABC".isUpperCase(), "String with multiple uppercase characters should be uppercase.");
     148            InspectorTest.expectFalse("a".isUpperCase(), "String with single lowercase character should not be uppercase.");
     149            InspectorTest.expectFalse("AbC".isUpperCase(), "String with mixed case characters should not be uppercase.");
     150            InspectorTest.expectFalse("".isUpperCase(), "Empty string should not be uppercase.");
     151            InspectorTest.expectFalse(".".isUpperCase(), "String with non-alpha character should not be uppercase.");
     152            InspectorTest.expectFalse("1".isUpperCase(), "String with numeric character should not be uppercase.");
     153
     154            return true;
     155        }
     156    });
     157
    128158    suite.runTestCasesAndFinish();
    129159}
  • trunk/Source/WebInspectorUI/ChangeLog

    r286558 r286611  
     12021-12-07  Razvan Caliman  <rcaliman@apple.com>
     2
     3        Web Inspector: Support fuzzy matching in CSS completions
     4        https://bugs.webkit.org/show_bug.cgi?id=230351
     5        <rdar://82976292>
     6
     7        Reviewed by Devin Rousso.
     8
     9        Use fuzzy matching for identifying CSS completions in the Styles details sidebar.
     10
     11        There are three main parts to this patch:
     12        1. Introduce `WI.CSSQueryController` with logic to do fuzzy matching on provided values.
     13        2. Change `WI.CompletionSuggestionsView` to support both plain strings and `WI.QueryResult`s.
     14        3. Change `WI.SpreadsheetTextField` so its `value` doesn't always return its `element.textContent`.
     15
     16        With fuzzy matching, completions are not guaranteed anymore to be prefixed with the query.
     17        Therefore, it's no longer viable to rely on `WI.SpreadsheetTextField.value` being a concatenation of `textContent` of nodes within.
     18
     19        To adress this, there's now `WI.SpreadsheetTextField._pendingValue` which includes the prospective
     20        completion regardless of whether the query is a completion prefix or at match at any other position.
     21
     22        * Localizations/en.lproj/localizedStrings.js:
     23        * UserInterface/Base/Setting.js:
     24        Add a flag to enable the fuzzy matching feature.
     25
     26        * UserInterface/Controllers/CSSQueryController.js: Added.
     27        (WI.CSSQueryController):
     28        (WI.CSSQueryController.prototype.addValues):
     29        (WI.CSSQueryController.prototype.reset):
     30        (WI.CSSQueryController.prototype.executeQuery):
     31        (WI.CSSQueryController.prototype._findSpecialCharacterIndices):
     32        Add a speclialized class to hold the logic for fuzzy matching of CSS properties and values.
     33        It clones logic from `WI.ResouceQueryController` with adjustments specific to CSS; more to follow as the feature gets refined.
     34
     35        * UserInterface/Main.html:
     36        * UserInterface/Models/CSSCompletions.js:
     37        (WI.CSSCompletions):
     38        (WI.CSSCompletions.prototype.addValues):
     39        (WI.CSSCompletions.prototype.executeQuery):
     40        Support both the current prefix matching approach as well as fuzzy matching.
     41
     42        * UserInterface/Models/CSSKeywordCompletions.js:
     43        (WI.CSSKeywordCompletions.forPartialPropertyName):
     44        Opt into fuzzy matching when getting completions for property names and values.
     45
     46        * UserInterface/Models/QueryResult.js:
     47        (WI.QueryResult.prototype.get matches):
     48        * UserInterface/Test.html:
     49
     50        * UserInterface/Views/CompletionSuggestionsView.css:
     51        (.completion-suggestions-container > .item > .highlighted):
     52        Highlight specific characters that matched in result identified by fuzzy matching.
     53
     54        * UserInterface/Views/CompletionSuggestionsView.js:
     55        (WI.CompletionSuggestionsView.prototype.selectNext):
     56        (WI.CompletionSuggestionsView.prototype.selectPrevious):
     57        (WI.CompletionSuggestionsView.prototype.update):
     58        (WI.CompletionSuggestionsView.prototype.getCompletionText):
     59        (WI.CompletionSuggestionsView.prototype._createHighlightedCompletionFragment):
     60        Change `WI.CompletionSuggestionsView` to hold a list of completions,
     61        either strings or `WI.QueryResult`, and return the appropriate completion text
     62        instead of returning the `textContent` of the selected element.
     63
     64        `WI.CompletionSuggestionsView` is used elsewhere in the Console and Sources panel
     65        so avoid impacting those consumers until they opt in to fuzzy matching as well.
     66
     67        * UserInterface/Views/SettingsTabContentView.js:
     68
     69        * UserInterface/Views/SpreadsheetStyleProperty.js:
     70        (WI.SpreadsheetStyleProperty.prototype._handleNameChange):
     71        (WI.SpreadsheetStyleProperty.prototype._handleValueChange):
     72        (WI.SpreadsheetStyleProperty.prototype._nameCompletionDataProvider):
     73        (WI.SpreadsheetStyleProperty.prototype._valueCompletionDataProvider):
     74        On change, get the value of the `WI.SpreadsheetTextField` instead of
     75        the corresponding element's `textContent`.
     76
     77        * UserInterface/Views/SpreadsheetTextField.js:
     78        Introduced `SpreadsheetTextField._completionPrefix` to hold the query a user is typing.
     79
     80        Introduced `SpreadsheetTextField._completionText` to hold the completion text of the selected but
     81        not yet applied completion. This replaces the previous behavior of concatenating the query and `suggestionHint`
     82        because with fuzzy matching the completion isn't guaranteed to be prefixed with the query.
     83
     84        Introduced `SpreadsheetTextField._pendingValue` to hold the result of applying the selected
     85        completion. This replaces the previous behavior of `WI.SpreadsheetTextField._combineEditorElementChildren`.
     86
     87        (WI.SpreadsheetTextField):
     88        (WI.SpreadsheetTextField.prototype.get value):
     89        Change `WI.SpreadsheetTextField` so its value is divorced from its element's `textContent`.
     90        While editing, `WI.SpreadsheetTextField.value` returns the pending value so that
     91        a valid CSS string gets written to the stylesheet.
     92
     93        (WI.SpreadsheetTextField.prototype.set suggestionHint):
     94        If the query is a prefix for the selected completion, keep the existing behavior of appending
     95        the suggestion hint substring. Otherwise, hide the suggestion hint element because the
     96        concatenation doesn't make sense.
     97
     98        (WI.SpreadsheetTextField.prototype.stopEditing):
     99        (WI.SpreadsheetTextField.prototype.discardCompletion):
     100        (WI.SpreadsheetTextField.prototype.completionSuggestionsSelectedCompletion):
     101        (WI.SpreadsheetTextField.prototype.completionSuggestionsClickedCompletion):
     102        (WI.SpreadsheetTextField.prototype._discardChange):
     103        (WI.SpreadsheetTextField.prototype._handleBlur):
     104        (WI.SpreadsheetTextField.prototype._handleKeyDown):
     105        (WI.SpreadsheetTextField.prototype._handleKeyDownForSuggestionView):
     106        (WI.SpreadsheetTextField.prototype._handleInput):
     107        (WI.SpreadsheetTextField.prototype._updateCompletions):
     108        (WI.SpreadsheetTextField.prototype._applyPendingValue):
     109        After applying a completion, the value is replaced with `WI.SpreadsheetTextField._pendingValue`.
     110        At this point, `WI.SpreadsheetTextField.value` and the `textContent` of the element converge.
     111
     112        (WI.SpreadsheetTextField.prototype._updatePendingValueWithCompletionText):
     113        When presented with a completion, the query gets substituted with the full completion text in
     114        `WI.SpreadsheetTextField._pendingValue` regardless of whether the query is a prefix or not.
     115
     116        (WI.SpreadsheetTextField.prototype._applyCompletionHint): Deleted.
     117        (WI.SpreadsheetTextField.prototype._combineEditorElementChildren): Deleted.
     118
    11192021-12-06  Patrick Angle  <pangle@apple.com>
    2120
  • trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js

    r285974 r286611  
    16081608localizedStrings["Use Mock Capture Devices"] = "Use Mock Capture Devices";
    16091609localizedStrings["Use default media styles"] = "Use default media styles";
     1610localizedStrings["Use fuzzy matching for completion suggestions"] = "Use fuzzy matching for completion suggestions";
    16101611localizedStrings["Use the resource cache when loading resources"] = "Use the resource cache when loading resources";
    16111612localizedStrings["User Agent"] = "User Agent";
  • trunk/Source/WebInspectorUI/UserInterface/Base/Setting.js

    r283212 r286611  
    232232    experimentalCollapseBlackboxedCallFrames: new WI.Setting("experimental-collapse-blackboxed-call-frames", false),
    233233    experimentalAllowInspectingInspector: new WI.Setting("experimental-allow-inspecting-inspector", false),
     234    experimentalCSSCompletionFuzzyMatching: new WI.Setting("experimental-css-completion-fuzzy-matching", false),
    234235
    235236    // Protocol
  • trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js

    r276975 r286611  
    774774    value()
    775775    {
    776         return String(this) === this.toLowerCase();
     776        return /^[a-z]+$/.test(this);
    777777    }
    778778});
     
    782782    value()
    783783    {
    784         return String(this) === this.toUpperCase();
     784        return /^[A-Z]+$/.test(this);
    785785    }
    786786});
  • trunk/Source/WebInspectorUI/UserInterface/Main.html

    r286329 r286611  
    928928    <script src="Controllers/NetworkManager.js"></script>
    929929    <script src="Controllers/OverlayManager.js"></script>
     930    <script src="Controllers/CSSQueryController.js"></script>
    930931    <script src="Controllers/ResourceQueryController.js"></script>
    931932    <script src="Controllers/RuntimeManager.js"></script>
  • trunk/Source/WebInspectorUI/UserInterface/Models/CSSCompletions.js

    r286458 r286611  
    5959
    6060        this._acceptEmptyPrefix = acceptEmptyPrefix;
     61        this._queryController = null;
    6162    }
    6263
     
    254255        this._values.pushAll(values);
    255256        this._values.sort();
     257
     258        this._queryController?.addValues(values);
     259    }
     260
     261    executeQuery(query)
     262    {
     263        this._queryController ||= new WI.CSSQueryController(this._values);
     264
     265        return this._queryController.executeQuery(query);
    256266    }
    257267
  • trunk/Source/WebInspectorUI/UserInterface/Models/CSSKeywordCompletions.js

    r285615 r286611  
    3232WI.CSSKeywordCompletions = {};
    3333
    34 WI.CSSKeywordCompletions.forPartialPropertyName = function(text, {caretPosition, allowEmptyPrefix} = {})
     34WI.CSSKeywordCompletions.forPartialPropertyName = function(text, {caretPosition, allowEmptyPrefix, useFuzzyMatching} = {})
    3535{
    3636    caretPosition ??= text.length;
     
    4343    if (!text.length && allowEmptyPrefix)
    4444        return {prefix: text, completions: WI.CSSCompletions.cssNameCompletions.values};
    45     return {prefix: text, completions:WI.CSSCompletions.cssNameCompletions.startsWith(text)};
     45
     46    let completions;
     47    if (useFuzzyMatching)
     48        completions = WI.CSSCompletions.cssNameCompletions.executeQuery(text);
     49    else
     50        completions = WI.CSSCompletions.cssNameCompletions.startsWith(text);
     51
     52    return {prefix: text, completions};
    4653};
    4754
    48 WI.CSSKeywordCompletions.forPartialPropertyValue = function(text, propertyName, {caretPosition, additionalFunctionValueCompletionsProvider} = {})
     55WI.CSSKeywordCompletions.forPartialPropertyValue = function(text, propertyName, {caretPosition, additionalFunctionValueCompletionsProvider, useFuzzyMatching} = {})
    4956{
    5057    caretPosition ??= text.length;
     
    8794        return {prefix: "", completions: []};
    8895
    89     // If the current token value is a comma or opening parenthesis, treat it as if we are at the start of a new token.
     96    // If the current token value is a comma or open parenthesis, treat it as if we are at the start of a new token.
    9097    if (currentTokenValue === "(" || currentTokenValue === ",")
    9198        currentTokenValue = "";
     99
     100    // It's not valid CSS to append completions immediately after a closing parenthesis.
     101    let tokenBeforeCaret = tokens[indexOfTokenAtCaret - 1];
     102    if (currentTokenValue === ")" || tokenBeforeCaret?.value === ")")
     103        return {prefix: "", completions: []};
    92104
    93105    let functionName = null;
     
    109121    }
    110122
     123    let valueCompletions;
    111124    if (functionName) {
    112         let completions = WI.CSSKeywordCompletions.forFunction(functionName);
    113         let contextualValueCompletions = additionalFunctionValueCompletionsProvider?.(functionName) || [];
    114         completions.addValues(contextualValueCompletions);
    115         return {prefix: currentTokenValue, completions: completions.startsWith(currentTokenValue)};
    116     }
    117 
    118     return {prefix: currentTokenValue, completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(currentTokenValue)};
     125        valueCompletions = WI.CSSKeywordCompletions.forFunction(functionName);
     126        valueCompletions.addValues(additionalFunctionValueCompletionsProvider?.(functionName) ?? []);
     127    } else
     128        valueCompletions = WI.CSSKeywordCompletions.forProperty(propertyName);
     129
     130    let completions;
     131    if (useFuzzyMatching)
     132        completions = valueCompletions.executeQuery(currentTokenValue);
     133    else
     134        completions = valueCompletions.startsWith(currentTokenValue);
     135
     136    return {prefix: currentTokenValue, completions};
    119137};
    120138
  • trunk/Source/WebInspectorUI/UserInterface/Models/QueryResult.js

    r285711 r286611  
    3737
    3838    get value() { return this._value; }
     39    get matches() { return this._matches; }
    3940
    4041    get rank()
  • trunk/Source/WebInspectorUI/UserInterface/Test.html

    r285711 r286611  
    254254    <script src="Controllers/BrowserManager.js"></script>
    255255    <script src="Controllers/CSSManager.js"></script>
     256    <script src="Controllers/CSSQueryController.js"></script>
    256257    <script src="Controllers/CanvasManager.js"></script>
    257258    <script src="Controllers/ConsoleManager.js"></script>
  • trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.css

    r239760 r286611  
    7171}
    7272
     73.completion-suggestions-container > .item > .matched {
     74    font-weight: bold;
     75}
     76
    7377.completion-suggestions-container:not(:active) > .item.selected,
    7478.completion-suggestions-container > .item:active {
  • trunk/Source/WebInspectorUI/UserInterface/Views/CompletionSuggestionsView.js

    r249863 r286611  
    3333        this._preventBlur = preventBlur || false;
    3434
     35        this._completions = [];
    3536        this._selectedIndex = NaN;
    3637        this._moveIntervalIdentifier = null;
     
    9091            ++this.selectedIndex;
    9192
    92         var selectedItemElement = this._selectedItemElement;
    93         if (selectedItemElement && this._delegate && typeof this._delegate.completionSuggestionsSelectedCompletion === "function")
    94             this._delegate.completionSuggestionsSelectedCompletion(this, selectedItemElement.textContent);
     93        if (this._completions[this.selectedIndex])
     94            this._delegate?.completionSuggestionsSelectedCompletion?.(this, this.getCompletionText(this._completions[this.selectedIndex]));
    9595    }
    9696
     
    102102            --this.selectedIndex;
    103103
    104         var selectedItemElement = this._selectedItemElement;
    105         if (selectedItemElement && this._delegate && typeof this._delegate.completionSuggestionsSelectedCompletion === "function")
    106             this._delegate.completionSuggestionsSelectedCompletion(this, selectedItemElement.textContent);
     104        if (this._completions[this.selectedIndex])
     105            this._delegate?.completionSuggestionsSelectedCompletion?.(this, this.getCompletionText(this._completions[this.selectedIndex]));
    107106    }
    108107
     
    176175    {
    177176        this._containerElement.removeChildren();
     177        this._completions = completions;
    178178
    179179        if (typeof selectedIndex === "number")
    180180            this._selectedIndex = selectedIndex;
    181181
    182         for (var i = 0; i < completions.length; ++i) {
     182        for (let [index, completion] of completions.entries()) {
    183183            var itemElement = document.createElement("div");
    184184            itemElement.classList.add("item");
    185             itemElement.classList.toggle("selected", i === this._selectedIndex);
    186             itemElement.textContent = completions[i];
     185            itemElement.classList.toggle("selected", index === this._selectedIndex);
     186
     187            if (typeof completion === "string")
     188                itemElement.textContent = completion;
     189            else if (completion instanceof WI.QueryResult)
     190                itemElement.appendChild(this._createMatchedCompletionFragment(completion.value, completion.matchingTextRanges));
     191
    187192            this._containerElement.appendChild(itemElement);
    188 
    189             if (this._delegate && typeof this._delegate.completionSuggestionsViewCustomizeCompletionElement === "function")
    190                 this._delegate.completionSuggestionsViewCustomizeCompletionElement(this, itemElement, completions[i]);
     193            this._delegate?.completionSuggestionsViewCustomizeCompletionElement?.(this, itemElement, completion);
    191194        }
     195    }
     196
     197    getCompletionText(completion)
     198    {
     199        console.assert(typeof completion === "string" || completion instanceof WI.QueryResult, completion);
     200
     201        if (typeof completion === "string")
     202            return completion;
     203
     204        if (completion instanceof WI.QueryResult)
     205            return completion.value;
     206
     207        return "";
    192208    }
    193209
     
    204220    }
    205221
     222    _createMatchedCompletionFragment(completionText, matchingTextRanges)
     223    {
     224        let completionFragment = document.createDocumentFragment();
     225        let lastIndex = 0;
     226        for (let textRange of matchingTextRanges) {
     227            console.assert(textRange.startColumn >= 0 && textRange.startColumn < completionText.length, textRange);
     228            console.assert(textRange.endColumn > 0 && textRange.endColumn <= completionText.length, textRange);
     229            console.assert(textRange.startColumn < textRange.endColumn);
     230
     231            if (textRange.startColumn > lastIndex)
     232                completionFragment.append(completionText.substring(lastIndex, textRange.startColumn));
     233
     234            let matchedSpan = document.createElement("span");
     235            matchedSpan.classList.add("matched");
     236            matchedSpan.append(completionText.substring(textRange.startColumn, textRange.endColumn));
     237            completionFragment.append(matchedSpan);
     238            lastIndex = textRange.endColumn;
     239        }
     240
     241        if (lastIndex < completionText.length)
     242            completionFragment.append(completionText.substring(lastIndex, completionText.length));
     243
     244        return completionFragment;
     245    }
     246
    206247    _mouseDown(event)
    207248    {
  • trunk/Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js

    r283212 r286611  
    398398            stylesGroup.addSetting(WI.settings.experimentalEnableStylesJumpToEffective, WI.UIString("Show jump to effective property button"));
    399399            stylesGroup.addSetting(WI.settings.experimentalEnableStylesJumpToVariableDeclaration, WI.UIString("Show jump to variable declaration button"));
     400            stylesGroup.addSetting(WI.settings.experimentalCSSCompletionFuzzyMatching, WI.UIString("Use fuzzy matching for completion suggestions"));
    400401
    401402            experimentalSettingsView.addSeparator();
  • trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js

    r285983 r286611  
    937937    _handleNameChange()
    938938    {
    939         this._property.name = this._nameElement.textContent.trim();
     939        this._property.name = this._nameTextField.value;
    940940    }
    941941
    942942    _handleValueChange()
    943943    {
    944         let value = this._valueElement.textContent;
     944        let value = this._valueTextField.value;
    945945
    946946        this._property.rawValue = value.trim();
     
    976976    }
    977977
    978     _nameCompletionDataProvider(text, {caretPosition, allowEmptyPrefix} = {})
    979     {
    980         return WI.CSSKeywordCompletions.forPartialPropertyName(text, {caretPosition, allowEmptyPrefix});
     978    _nameCompletionDataProvider(text, options = {})
     979    {
     980        return WI.CSSKeywordCompletions.forPartialPropertyName(text, options);
    981981    }
    982982
     
    10001000    }
    10011001
    1002     _valueCompletionDataProvider(text, {caretPosition, allowEmptyPrefix} = {})
    1003     {
    1004         return WI.CSSKeywordCompletions.forPartialPropertyValue(text, this._nameElement.textContent.trim(), {caretPosition, additionalFunctionValueCompletionsProvider: this.additionalFunctionValueCompletionsProvider.bind(this)});
     1002    _valueCompletionDataProvider(text, options = {})
     1003    {
     1004        options.additionalFunctionValueCompletionsProvider = this.additionalFunctionValueCompletionsProvider.bind(this);
     1005        return WI.CSSKeywordCompletions.forPartialPropertyValue(text, this._nameElement.textContent.trim(), options);
    10051006    }
    10061007
  • trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js

    r285851 r286611  
    3030        this._delegate = delegate;
    3131        this._element = element;
     32        this._pendingValue = null;
    3233
    3334        this._completionProvider = completionProvider || null;
     
    5354        this._valueBeforeEditing = "";
    5455        this._completionPrefix = "";
     56        this._completionText = "";
    5557        this._controlSpaceKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, WI.KeyboardShortcut.Key.Space);
    5658    }
     
    6264    get editing() { return this._editing; }
    6365
    64     get value() { return this._element.textContent; }
    65     set value(value) { this._element.textContent = value; }
     66    get value()
     67    {
     68        return this._pendingValue ?? this._element.textContent;
     69    }
     70
     71    set value(value)
     72    {
     73        this._element.textContent = value;
     74
     75        this._pendingValue = null;
     76    }
    6677
    6778    valueWithoutSuggestion()
     
    95106
    96107        this._suggestionHintElement.remove();
    97 
    98         // Removing the suggestion hint element may leave the contents of `_element` fragmented into multiple text nodes.
    99         this._combineEditorElementChildren();
    100108    }
    101109
     
    131139        this._editing = false;
    132140        this._valueBeforeEditing = "";
     141        this._pendingValue = null;
     142        this._completionText = "";
    133143        this._element.classList.remove("editing");
    134144        this._element.contentEditable = false;
     
    144154        this._suggestionsView.hide();
    145155
    146         let hadSuggestionHint = !!this.suggestionHint;
     156        let hadCompletionText = this._completionText.length > 0;
     157
     158        // Resetting the suggestion hint removes any suggestion hint element that is attached.
    147159        this.suggestionHint = "";
    148         if (hadSuggestionHint && this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
    149             this._delegate.spreadsheetTextFieldDidChange(this);
     160        this._completionText = "";
     161
     162        if (hadCompletionText) {
     163            this._pendingValue = this.valueWithoutSuggestion();
     164            this._delegate?.spreadsheetTextFieldDidChange?.(this);
     165        }
    150166    }
    151167
     
    158174    // CompletionSuggestionsView delegate
    159175
    160     completionSuggestionsSelectedCompletion(suggestionsView, selectedText = "")
    161     {
    162         this.suggestionHint = selectedText.slice(this._completionPrefix.length);
    163 
    164         if (this.suggestionHint.length)
    165             this._reAttachSuggestionHint();
     176    completionSuggestionsSelectedCompletion(suggestionsView, completionText = "")
     177    {
     178        this._completionText = completionText;
     179
     180        if (this._completionText.startsWith(this._completionPrefix))
     181            this.suggestionHint = this._completionText.slice(this._completionPrefix.length);
     182        else
     183            this.suggestionHint = "";
     184
     185        this._updatePendingValueWithCompletionText();
    166186
    167187        if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
     
    169189    }
    170190
    171     completionSuggestionsClickedCompletion(suggestionsView, selectedText)
    172     {
    173         this.suggestionHint = selectedText.slice(this._completionPrefix.length);
    174 
    175         this._applyCompletionHint({moveCaretToEndOfCompletion: true});
     191    completionSuggestionsClickedCompletion(suggestionsView, completionText)
     192    {
     193        this._completionText = completionText;
     194        this._updatePendingValueWithCompletionText();
     195        this._applyPendingValue({moveCaretToEndOfCompletion: true});
    176196        this.discardCompletion();
    177197
     
    220240            return;
    221241
    222         this._applyCompletionHint();
     242        this._applyPendingValue();
    223243        this.discardCompletion();
    224244
     
    253273        if (isEnterKey || isTabKey) {
    254274            event.stop();
    255             this._applyCompletionHint();
     275            this._applyPendingValue();
    256276
    257277            let direction = (isTabKey && event.shiftKey) ? "backward" : "forward";
     
    339359        }
    340360
    341         if (event.key === "ArrowRight" && this.suggestionHint.length) {
     361        if (event.key === "ArrowRight" && this._completionText.length) {
    342362            let selection = window.getSelection();
    343363
    344364            if (selection.isCollapsed) {
    345365                event.stop();
    346                 this._applyCompletionHint({moveCaretToEndOfCompletion: true});
     366                this._applyPendingValue({moveCaretToEndOfCompletion: true});
    347367
    348368                // When completing "background", don't hide the completion popover.
     
    359379        if (event.key === "Escape" && this._suggestionsView.visible) {
    360380            event.stop();
    361 
    362             let willChange = !!this.suggestionHint;
    363381            this.discardCompletion();
    364382
    365             if (willChange && this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
    366                 this._delegate.spreadsheetTextFieldDidChange(this);
    367 
    368383            return true;
    369384        }
    370385
    371         if (event.key === "ArrowLeft" && (this.suggestionHint || this._suggestionsView.visible)) {
     386        if (event.key === "ArrowLeft" && (this._completionText.length || this._suggestionsView.visible)) {
    372387            this.discardCompletion();
    373388
    374             if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function")
    375                 this._delegate.spreadsheetTextFieldDidChange(this);
    376389            return true;
    377390        }
     
    406419            return;
    407420
     421        this._pendingValue = this.valueWithoutSuggestion().trim();
    408422        this._preventDiscardingCompletionsOnKeyUp = true;
    409423        this._updateCompletions();
     
    418432            return;
    419433
     434        let useFuzzyMatching = WI.settings.experimentalCSSCompletionFuzzyMatching.value;
    420435        let valueWithoutSuggestion = this.valueWithoutSuggestion();
    421         let {completions, prefix} = this._completionProvider(valueWithoutSuggestion, {allowEmptyPrefix: forceCompletions, caretPosition: this._getCaretPosition()});
     436        let {completions, prefix} = this._completionProvider(valueWithoutSuggestion, {allowEmptyPrefix: forceCompletions, caretPosition: this._getCaretPosition(), useFuzzyMatching});
    422437        this._completionPrefix = prefix;
    423438
     
    428443
    429444        // No need to show the completion popover with only one item that matches the entered value.
    430         if (completions.length === 1 && completions[0] === valueWithoutSuggestion) {
     445        if (completions.length === 1 && this._suggestionsView.getCompletionText(completions[0]) === valueWithoutSuggestion) {
    431446            this.discardCompletion();
    432447            return;
     
    441456        this._suggestionsView.update(completions);
    442457
    443         if (completions.length === 1) {
    444             // No need to show the completion popover that matches the suggestion hint.
     458        if (completions.length === 1 && this._suggestionsView.getCompletionText(completions[0]).startsWith(this._completionPrefix)) {
     459            // No need to show the completion popover with only one item that begins with the completion prefix.
     460            // When using fuzzy matching, the completion prefix may not occur at the beginning of the suggestion.
    445461            this._suggestionsView.hide();
    446462        } else
     
    531547    }
    532548
    533     _applyCompletionHint({moveCaretToEndOfCompletion} = {})
    534     {
    535         if (!this._completionProvider || !this.suggestionHint)
    536             return;
    537 
    538         this._combineEditorElementChildren({newCaretPosition: moveCaretToEndOfCompletion ? this._getCaretPosition() + this.suggestionHint.length : null});
    539     }
    540 
    541     _combineEditorElementChildren({newCaretPosition} = {})
    542     {
    543         newCaretPosition ??= this._getCaretPosition();
    544 
    545         // Setting the textContent of the element to its current textContent will take the text from the multiple
    546         // potential child nodes (potentially a suggestion hint node and some number of existing text nodes) and turn
    547         // them into a single text node within the element.
    548         this._element.textContent = this._element.textContent;
     549    _applyPendingValue({moveCaretToEndOfCompletion} = {})
     550    {
     551        if (!this._pendingValue)
     552            return;
     553
     554        let caretPosition = this._getCaretPosition();
     555        let newCaretPosition = moveCaretToEndOfCompletion ? caretPosition - this._completionPrefix.length + this._completionText.length : caretPosition;
     556
     557        // Setting the value collapses the text selection. Get the caret position before doing this.
     558        this.value = this._pendingValue;
    549559
    550560        if (this._element.textContent.length) {
     
    554564    }
    555565
     566    _updatePendingValueWithCompletionText()
     567    {
     568        let caretPosition = this._getCaretPosition();
     569        let value = this.valueWithoutSuggestion();
     570
     571        this._pendingValue = value.slice(0, caretPosition - this._completionPrefix.length) + this._completionText + value.slice(caretPosition + 1, value.length);
     572    }
     573
    556574    _reAttachSuggestionHint()
    557575    {
Note: See TracChangeset for help on using the changeset viewer.