Changeset 285851 in webkit
- Timestamp:
- Nov 15, 2021 10:01:36 PM (8 months ago)
- Location:
- trunk/Source/WebInspectorUI
- Files:
-
- 3 edited
-
ChangeLog (modified) (1 diff)
-
UserInterface/Views/SpreadsheetStyleProperty.js (modified) (2 diffs)
-
UserInterface/Views/SpreadsheetTextField.js (modified) (17 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/Source/WebInspectorUI/ChangeLog
r285839 r285851 1 2021-11-15 Patrick Angle <pangle@apple.com> 2 3 Web Inspector: Styles: Autocomplete should support mid-line completions 4 https://bugs.webkit.org/show_bug.cgi?id=227411 5 6 Reviewed by Devin Rousso. 7 8 Autocompletion for CSS property values was lacking in the ability to perform mid-line completions, including 9 within functions, and a lack of support for multi-line CSS property values. This resolves those pain points by 10 making SpreadsheetTextField multi-line aware and allowing mid-line autocompletion. 11 12 * UserInterface/Views/SpreadsheetStyleProperty.js: 13 (WI.SpreadsheetStyleProperty.prototype._nameCompletionDataProvider): 14 (WI.SpreadsheetStyleProperty.prototype._valueCompletionDataProvider): 15 16 * UserInterface/Views/SpreadsheetTextField.js: 17 (WI.SpreadsheetTextField): 18 (WI.SpreadsheetTextField.prototype.valueWithoutSuggestion): 19 Because suggestions can occur anywhere within the value, we need to iterate through each of the nodes and 20 collect the text of any of them that are not the completion suggestion. 21 22 (WI.SpreadsheetTextField.prototype.set suggestionHint): 23 When removing the suggestion hint element, we should recombine the text nodes we may have split upon insertion 24 to prevent the text content from becoming endlessly fragmented into multiple text nodes unnecessarily. 25 26 (WI.SpreadsheetTextField.prototype.startEditing): 27 Reset the last known caret position when editing starts so that we don't mistake keys that are already down as 28 having moved the cursor to its initial position. 29 30 (WI.SpreadsheetTextField.prototype.completionSuggestionsSelectedCompletion): 31 We should only attempt to reattach the suggestion hint element if we have a suggestion hint to show, otherwise 32 we could end up with a phantom empty suggestion hint that isn't properly placed later. 33 34 (WI.SpreadsheetTextField.prototype.completionSuggestionsClickedCompletion): 35 Updated to use existing completion committing path to reduce duplicated logic. 36 37 (WI.SpreadsheetTextField.prototype._handleMouseDown): 38 When the user clicks inside the text field while suggestions are visible treat that as intent to stop 39 autocompletion similar to the escape key, since the new cursor location could be anywhere in the text field. 40 This behavior also matches other code editors like Xcode, where clicking outside a completion popup will dismiss 41 the completions list. 42 43 (WI.SpreadsheetTextField.prototype._handleKeyDown): 44 Keep track of where the caret was as well as if the the key event was handled by the suggestion view at the time 45 a key is pressed so that we can later compare the position to that of when the key is released to determine if 46 completions should be discarded. 47 48 (WI.SpreadsheetTextField.prototype._handleKeyDownForSuggestionView): 49 We can no longer assume that the current selection's offset will match the value's length, as the offset could 50 be on any line, or the suggestion could be in middle of a line. Because a suggestionHint only contains text when 51 we are in middle of autocompletion, it alone is an indicator that pressing the right arrow key should commit the 52 completion. `document.execCommand` is deprecated, so instead we now use the existing completion committing path 53 to reduce duplicated logic here. 54 55 Note that the left arrow key is still explicitly handled for dismissing autocompletion, as the new mechanism for 56 dismissing autocompletion won't trigger unless the caret has moved, and the caret will not move if you press the 57 left arrow key at the start of the text field. 58 59 (WI.SpreadsheetTextField.prototype._handleKeyUp): 60 When a key is released, we should check to see if the caret has moved as a result of the keystroke that we have 61 not already explicitly handled. If it has moved and was not handled, we dismiss the autocompletion suggestions. 62 63 (WI.SpreadsheetTextField.prototype._handleInput): 64 65 (WI.SpreadsheetTextField.prototype._updateCompletions): 66 Provide the current caret position to the completion provided. 67 68 (WI.SpreadsheetTextField.prototype._showSuggestionsView): 69 In order to correctly align the completion list with the current text content, it must be offset from the caret 70 position excluding the current prefix. 71 72 (WI.SpreadsheetTextField.prototype._getCaretPosition): 73 Added to get the index of the caret in the complete text value, accounting for multi-line values and mid-line 74 suggestions. 75 76 (WI.SpreadsheetTextField.prototype._getCaretRect): 77 78 (WI.SpreadsheetTextField.prototype._rangeAtCaretPosition): 79 Find the range for a caret at the given position. This compliments `_getCaretPosition`, and allows us to count 80 back some number of characters and create a range of which we later get the client rectangle. 81 82 (WI.SpreadsheetTextField.prototype._applyCompletionHint): 83 Add optional support for updating the caret position to be at the end of the newly inserted text. 84 85 (WI.SpreadsheetTextField.prototype._combineEditorElementChildren): 86 Added to handle combining the fragmented text nodes (and possibly a suggestion hint element) back into a single 87 text node while maintaining the current cursor position or optionally moving the cursor to a new location (e.g. 88 the end of a completion). 89 90 (WI.SpreadsheetTextField.prototype._reAttachSuggestionHint): 91 Now that completions are not guaranteed to be at the end of the value, we may need to split the text node to 92 insert the suggestion hint node. 93 1 94 2021-11-15 Fujii Hironori <Hironori.Fujii@sony.com> 2 95 -
trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetStyleProperty.js
r283723 r285851 964 964 } 965 965 966 _nameCompletionDataProvider(text, { allowEmptyPrefix} = {})967 { 968 return WI.CSSKeywordCompletions.forPartialPropertyName(text, { allowEmptyPrefix});966 _nameCompletionDataProvider(text, {caretPosition, allowEmptyPrefix} = {}) 967 { 968 return WI.CSSKeywordCompletions.forPartialPropertyName(text, {caretPosition, allowEmptyPrefix}); 969 969 } 970 970 … … 988 988 } 989 989 990 _valueCompletionDataProvider(text, {allowEmptyPrefix} = {}) 991 { 992 // FIXME: <webkit.org/b/227411> Styles sidebar panel should support midline and multiline completions. 993 return WI.CSSKeywordCompletions.forPartialPropertyValue(text, this._nameElement.textContent.trim(), {additionalFunctionValueCompletionsProvider: this.additionalFunctionValueCompletionsProvider.bind(this)}); 990 _valueCompletionDataProvider(text, {caretPosition, allowEmptyPrefix} = {}) 991 { 992 return WI.CSSKeywordCompletions.forPartialPropertyValue(text, this._nameElement.textContent.trim(), {caretPosition, additionalFunctionValueCompletionsProvider: this.additionalFunctionValueCompletionsProvider.bind(this)}); 994 993 } 995 994 -
trunk/Source/WebInspectorUI/UserInterface/Views/SpreadsheetTextField.js
r252523 r285851 1 1 /* 2 * Copyright (C) 20 17Apple Inc. All rights reserved.2 * Copyright (C) 2021 Apple Inc. All rights reserved. 3 3 * 4 4 * Redistribution and use in source and binary forms, with or without … … 45 45 this._element.addEventListener("blur", this._handleBlur.bind(this)); 46 46 this._element.addEventListener("keydown", this._handleKeyDown.bind(this)); 47 this._element.addEventListener("keyup", this._handleKeyUp.bind(this)); 47 48 this._element.addEventListener("input", this._handleInput.bind(this)); 48 49 49 50 this._editing = false; 51 this._preventDiscardingCompletionsOnKeyUp = false; 52 this._keyDownCaretPosition = -1; 50 53 this._valueBeforeEditing = ""; 51 54 this._completionPrefix = ""; … … 64 67 valueWithoutSuggestion() 65 68 { 66 let value = this._element.textContent; 67 return value.slice(0, value.length - this.suggestionHint.length); 69 // The suggestion could appear anywhere within the element, and the text of the element can span multiple nodes. 70 let valueWithoutSuggestion = ""; 71 for (let childNode of this._element.childNodes) { 72 if (childNode === this._suggestionHintElement) 73 continue; 74 valueWithoutSuggestion += childNode.textContent; 75 } 76 return valueWithoutSuggestion; 68 77 } 69 78 … … 75 84 set suggestionHint(value) 76 85 { 86 if (this._suggestionHintElement.textContent === value) 87 return; 88 77 89 this._suggestionHintElement.textContent = value; 78 90 79 if (value) 91 if (value) { 80 92 this._reAttachSuggestionHint(); 81 else 82 this._suggestionHintElement.remove(); 93 return; 94 } 95 96 this._suggestionHintElement.remove(); 97 98 // Removing the suggestion hint element may leave the contents of `_element` fragmented into multiple text nodes. 99 this._combineEditorElementChildren(); 83 100 } 84 101 … … 93 110 this._editing = true; 94 111 this._valueBeforeEditing = this.value; 112 113 this._keyDownCaretPosition = -1; 95 114 96 115 this._element.classList.add("editing"); … … 143 162 this.suggestionHint = selectedText.slice(this._completionPrefix.length); 144 163 145 this._reAttachSuggestionHint(); 164 if (this.suggestionHint.length) 165 this._reAttachSuggestionHint(); 146 166 147 167 if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function") … … 151 171 completionSuggestionsClickedCompletion(suggestionsView, selectedText) 152 172 { 153 // Consider the following example: 154 // 155 // border: 1px solid ro| 156 // rosybrown 157 // royalblue 158 // 159 // Clicking on "rosybrown" should replace "ro" with "rosybrown". 160 // 161 // prefix: 1px solid ro 162 // completionPrefix: ro 163 // newPrefix: 1px solid 164 // selectedText: rosybrown 165 let prefix = this.valueWithoutSuggestion(); 166 let newPrefix = prefix.slice(0, -this._completionPrefix.length); 167 168 this._element.textContent = newPrefix + selectedText; 169 170 // Place text caret at the end. 171 window.getSelection().setBaseAndExtent(this._element, selectedText.length, this._element, selectedText.length); 172 173 this.suggestionHint = selectedText.slice(this._completionPrefix.length); 174 175 this._applyCompletionHint({moveCaretToEndOfCompletion: true}); 173 176 this.discardCompletion(); 174 177 … … 201 204 _handleMouseDown(event) 202 205 { 203 if (this._editing) 204 event.stopPropagation(); 206 if (!this._editing) 207 return; 208 209 event.stopPropagation(); 210 this.discardCompletion(); 205 211 } 206 212 … … 227 233 return; 228 234 235 this._preventDiscardingCompletionsOnKeyUp = false; 236 this._keyDownCaretPosition = this._getCaretPosition(); 237 229 238 if (this._suggestionsView) { 230 239 let consumed = this._handleKeyDownForSuggestionView(event); 240 this._preventDiscardingCompletionsOnKeyUp = consumed; 231 241 if (consumed) 232 242 return; … … 293 303 this._suggestionsView.hide(); 294 304 else { 305 this._preventDiscardingCompletionsOnKeyUp = true; 295 306 const forceCompletions = true; 296 307 this._updateCompletions(forceCompletions); … … 328 339 } 329 340 330 if (event.key === "ArrowRight" && this.suggestionHint ) {341 if (event.key === "ArrowRight" && this.suggestionHint.length) { 331 342 let selection = window.getSelection(); 332 343 333 if (selection.isCollapsed && (selection.focusOffset === this.valueWithoutSuggestion().length || selection.focusNode === this._suggestionHintElement)) {344 if (selection.isCollapsed) { 334 345 event.stop(); 335 document.execCommand("insertText", false, this.suggestionHint);346 this._applyCompletionHint({moveCaretToEndOfCompletion: true}); 336 347 337 348 // When completing "background", don't hide the completion popover. … … 363 374 if (this._delegate && typeof this._delegate.spreadsheetTextFieldDidChange === "function") 364 375 this._delegate.spreadsheetTextFieldDidChange(this); 376 return true; 365 377 } 366 378 … … 368 380 } 369 381 382 _handleKeyUp() 383 { 384 if (!this._editing || !this._suggestionsView) 385 return; 386 387 // Certain actions, like Ctrl+Space will handle updating or discarding completions as necessary. 388 if (this._preventDiscardingCompletionsOnKeyUp) 389 return; 390 391 // Some key events, like the arrow keys and Ctrl+A (move to line start), will move the caret without committing 392 // any input to the text field. In those situations we should discard completion if they are available. It is 393 // also possible that we receive a KeyUp event for a key that was not pressed inside this text field, in which 394 // case the _keyDownCaretPosition will still be -1. This can occur when the user types a `:` to begin editing 395 // the value for a property, and the KeyUp events for each of those keys will be handled here, even though the 396 // corresponding KeyDown events was never handled by this text field. 397 if (this._keyDownCaretPosition === -1 || this._keyDownCaretPosition === this._getCaretPosition()) 398 return; 399 400 this.discardCompletion(); 401 } 402 370 403 _handleInput(event) 371 404 { … … 373 406 return; 374 407 408 this._preventDiscardingCompletionsOnKeyUp = true; 375 409 this._updateCompletions(); 376 410 … … 385 419 386 420 let valueWithoutSuggestion = this.valueWithoutSuggestion(); 387 let {completions, prefix} = this._completionProvider(valueWithoutSuggestion, {allowEmptyPrefix: forceCompletions });421 let {completions, prefix} = this._completionProvider(valueWithoutSuggestion, {allowEmptyPrefix: forceCompletions, caretPosition: this._getCaretPosition()}); 388 422 this._completionPrefix = prefix; 389 423 … … 423 457 _showSuggestionsView() 424 458 { 425 let prefix = this.valueWithoutSuggestion(); 426 let startOffset = prefix.length - this._completionPrefix.length; 427 let caretRect = this._getCaretRect(startOffset); 459 // Adjust the used caret position to correctly align autocompletion results with existing text. The suggestions 460 // should appear aligned as below: 461 // 462 // border: 1px solid ro| 463 // rosybrown 464 // royalblue 465 let adjustedCaretPosition = this._getCaretPosition() - this._completionPrefix.length; 466 let caretRect = this._getCaretRect(adjustedCaretPosition); 428 467 429 468 // Hide completion popover when the anchor element is removed from the DOM. … … 436 475 } 437 476 438 _getCaret Rect(startOffset)477 _getCaretPosition() 439 478 { 440 479 let selection = window.getSelection(); 441 480 if (!selection.rangeCount) 481 return 0; 482 483 // The window's selection range will only contain the current line's positioning in multiline text, so a new 484 // range must be created between the end of the current range and the beginning of the first line of text in 485 // order to get an accurate character position for the caret. 486 let lineRange = selection.getRangeAt(0); 487 let multilineRange = document.createRange(); 488 multilineRange.setStart(this._element, 0); 489 multilineRange.setEnd(lineRange.endContainer, lineRange.endOffset); 490 return multilineRange.toString().length; 491 } 492 493 _getCaretRect(caretPosition) 494 { 442 495 let isHidden = (clientRect) => { 443 496 return clientRect.x === 0 && clientRect.y === 0; 444 497 }; 445 498 446 if (selection.rangeCount) { 447 let range = selection.getRangeAt(0).cloneRange(); 448 range.setStart(range.startContainer, startOffset); 449 let clientRect = range.getBoundingClientRect(); 450 451 if (!isHidden(clientRect)) { 452 // This happens after deleting value. However, when focusing 453 // on an empty value clientRect is visible. 454 return WI.Rect.rectFromClientRect(clientRect); 499 let caretRange = this._rangeAtCaretPosition(caretPosition); 500 let caretClientRect = caretRange.getBoundingClientRect(); 501 if (!isHidden(caretClientRect)) 502 return WI.Rect.rectFromClientRect(caretClientRect); 503 504 let elementClientRect = this._element.getBoundingClientRect(); 505 if (isHidden(elementClientRect)) 506 return null; 507 508 const leftPadding = parseInt(getComputedStyle(this._element).paddingLeft) || 0; 509 return new WI.Rect(elementClientRect.left + leftPadding, elementClientRect.top, elementClientRect.width, elementClientRect.height); 510 } 511 512 _rangeAtCaretPosition(caretPosition) { 513 for (let node of this._element.childNodes) { 514 let textContent = node.textContent; 515 if (caretPosition <= textContent.length) { 516 let range = document.createRange(); 517 range.setStart(node, caretPosition); 518 range.setEnd(node, caretPosition); 519 return range; 455 520 } 456 } 457 458 let clientRect = this._element.getBoundingClientRect(); 459 if (isHidden(clientRect)) 460 return null; 461 462 const leftPadding = parseInt(getComputedStyle(this._element).paddingLeft) || 0; 463 return new WI.Rect(clientRect.left + leftPadding, clientRect.top, clientRect.width, clientRect.height); 464 } 465 466 _applyCompletionHint() 521 522 caretPosition -= textContent.length; 523 } 524 525 // If there are no nodes, or the caret position is greater than the total text content, provide the range of a 526 // caret at the end of the element. 527 let range = document.createRange(); 528 range.selectNodeContents(this._element); 529 range.collapse(); 530 return range; 531 } 532 533 _applyCompletionHint({moveCaretToEndOfCompletion} = {}) 467 534 { 468 535 if (!this._completionProvider || !this.suggestionHint) 469 536 return; 470 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. 471 548 this._element.textContent = this._element.textContent; 549 550 if (this._element.textContent.length) { 551 let textChildNode = this._element.firstChild; 552 window.getSelection().setBaseAndExtent(textChildNode, newCaretPosition, textChildNode, newCaretPosition); 553 } 472 554 } 473 555 474 556 _reAttachSuggestionHint() 475 557 { 558 console.assert(this.suggestionHint.length, "Suggestion hint should not be empty when attaching the suggestion hint element."); 559 476 560 if (this._suggestionHintElement.parentElement === this._element) 477 561 return; 478 562 479 this._element.append(this._suggestionHintElement); 563 let selection = window.getSelection(); 564 if (!this._element.textContent.length || !selection.rangeCount) { 565 this._element.append(this._suggestionHintElement); 566 return; 567 } 568 569 let range = selection.getRangeAt(0); 570 571 console.assert(range.endContainer instanceof Text, range.endContainer); 572 if (!(range.endContainer instanceof Text)) 573 return; 574 575 this._element.insertBefore(this._suggestionHintElement, range.endContainer.splitText(range.endOffset)); 480 576 } 481 577 };
Note: See TracChangeset
for help on using the changeset viewer.