Chromium Code Reviews| Index: third_party/WebKit/Source/core/editing/commands/InsertIncrementalTextCommand.cpp |
| diff --git a/third_party/WebKit/Source/core/editing/commands/InsertIncrementalTextCommand.cpp b/third_party/WebKit/Source/core/editing/commands/InsertIncrementalTextCommand.cpp |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..c092ac5b1abf8cbf02b01d7ce39e402f1eb64d59 |
| --- /dev/null |
| +++ b/third_party/WebKit/Source/core/editing/commands/InsertIncrementalTextCommand.cpp |
| @@ -0,0 +1,495 @@ |
| +// Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +#include "core/editing/commands/InsertIncrementalTextCommand.h" |
| + |
| +#include "core/dom/Document.h" |
| +#include "core/dom/Element.h" |
| +#include "core/dom/Text.h" |
| +#include "core/editing/EditingUtilities.h" |
| +#include "core/editing/Editor.h" |
| +#include "core/editing/PlainTextRange.h" |
| +#include "core/editing/VisibleUnits.h" |
| +#include "core/frame/LocalFrame.h" |
| +#include "core/html/HTMLSpanElement.h" |
| + |
| +namespace blink { |
| + |
| +InsertIncrementalTextCommand::InsertIncrementalTextCommand( |
| + Document& document, |
| + const String& text, |
| + bool selectInsertedText, |
| + RebalanceType rebalanceType) |
| + : CompositeEditCommand(document), |
| + m_text(text), |
| + m_selectInsertedText(selectInsertedText), |
| + m_rebalanceType(rebalanceType) {} |
| + |
| +String InsertIncrementalTextCommand::textDataForInputEvent() const { |
| + return m_text; |
| +} |
| + |
| +Position InsertIncrementalTextCommand::positionInsideTextNode( |
| + const Position& p, |
| + EditingState* editingState) { |
| + Position pos = p; |
| + if (isTabHTMLSpanElementTextNode(pos.anchorNode())) { |
| + Text* textNode = document().createEditingTextNode(""); |
| + insertNodeAtTabSpanPosition(textNode, pos, editingState); |
| + if (editingState->isAborted()) |
| + return Position(); |
| + return Position::firstPositionInNode(textNode); |
| + } |
| + |
| + // Prepare for text input by looking at the specified position. |
| + // It may be necessary to insert a text node to receive characters. |
| + if (!pos.computeContainerNode()->isTextNode()) { |
| + Text* textNode = document().createEditingTextNode(""); |
| + insertNodeAt(textNode, pos, editingState); |
| + if (editingState->isAborted()) |
| + return Position(); |
| + return Position::firstPositionInNode(textNode); |
| + } |
| + |
| + return pos; |
| +} |
| + |
| +void InsertIncrementalTextCommand::setEndingSelectionWithoutValidation( |
| + const Position& startPosition, |
| + const Position& endPosition) { |
| + // We could have inserted a part of composed character sequence, |
| + // so we are basically treating ending selection as a range to avoid |
| + // validation. <http://bugs.webkit.org/show_bug.cgi?id=15781> |
| + setEndingSelection(SelectionInDOMTree::Builder() |
| + .collapse(startPosition) |
| + .extend(endPosition) |
| + .setIsDirectional(endingSelection().isDirectional()) |
| + .build()); |
| +} |
| + |
| +// This avoids the expense of a full fledged delete operation, and avoids a |
| +// layout that typically results from text removal. |
| +bool InsertIncrementalTextCommand::performTrivialReplace( |
| + const String& text, |
| + bool selectInsertedText) { |
| + if (!endingSelection().isRange()) |
| + return false; |
| + |
| + if (text.contains('\t') || text.contains(' ') || text.contains('\n')) |
| + return false; |
| + |
| + Position start = endingSelection().start(); |
| + Position endPosition = replaceSelectedTextInNode(text); |
| + if (endPosition.isNull()) |
| + return false; |
| + |
| + setEndingSelectionWithoutValidation(start, endPosition); |
| + if (selectInsertedText) |
| + return true; |
| + setEndingSelection(SelectionInDOMTree::Builder() |
| + .collapse(endingSelection().end()) |
| + .setIsDirectional(endingSelection().isDirectional()) |
| + .build()); |
| + return true; |
| +} |
| + |
| +bool InsertIncrementalTextCommand::performOverwrite(const String& text, |
| + bool selectInsertedText) { |
| + Position start = endingSelection().start(); |
| + if (start.isNull() || !start.isOffsetInAnchor() || |
| + !start.computeContainerNode()->isTextNode()) |
| + return false; |
| + Text* textNode = toText(start.computeContainerNode()); |
| + if (!textNode) |
| + return false; |
| + |
| + unsigned count = std::min(text.length(), |
| + textNode->length() - start.offsetInContainerNode()); |
| + if (!count) |
| + return false; |
| + |
| + replaceTextInNode(textNode, start.offsetInContainerNode(), count, text); |
| + |
| + Position endPosition = |
| + Position(textNode, start.offsetInContainerNode() + text.length()); |
| + setEndingSelectionWithoutValidation(start, endPosition); |
| + if (selectInsertedText || endingSelection().isNone()) |
| + return true; |
| + setEndingSelection(SelectionInDOMTree::Builder() |
| + .collapse(endingSelection().end()) |
| + .setIsDirectional(endingSelection().isDirectional()) |
| + .build()); |
| + return true; |
| +} |
| + |
| +static size_t computeCommonPrefixLength(const String& str1, |
| + const String& str2) { |
| + const size_t maxCommonPrefixLength = std::min(str1.length(), str2.length()); |
| + for (size_t index = 0; index < maxCommonPrefixLength; ++index) { |
| + if (str1[index] != str2[index]) |
| + return index; |
| + } |
| + return maxCommonPrefixLength; |
| +} |
| + |
| +static size_t computeCommonSuffixLength(const String& str1, |
| + const String& str2) { |
| + const size_t length1 = str1.length(); |
| + const size_t length2 = str2.length(); |
| + const size_t maxCommonSuffixLength = std::min(length1, length2); |
| + for (size_t index = 0; index < maxCommonSuffixLength; ++index) { |
| + if (str1[length1 - index - 1] != str2[length2 - index - 1]) |
| + return index; |
| + } |
| + return maxCommonSuffixLength; |
| +} |
| + |
| +// If current position is at grapheme boundary, return 0; otherwise, return the |
| +// distance to its nearest left grapheme boundary. |
| +static size_t computeDistanceToLeftGraphemeBoundary(const Position& position) { |
| + const Position& adjustedPosition = previousPositionOf( |
| + nextPositionOf(position, PositionMoveType::GraphemeCluster), |
| + PositionMoveType::GraphemeCluster); |
| + DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode()); |
| + DCHECK_GE(position.computeOffsetInContainerNode(), |
| + adjustedPosition.computeOffsetInContainerNode()); |
| + return static_cast<size_t>(position.computeOffsetInContainerNode() - |
| + adjustedPosition.computeOffsetInContainerNode()); |
| +} |
| + |
| +static size_t computeCommonGraphemeClusterPrefixLength( |
| + const String& oldText, |
| + const String& newText, |
| + const Element* rootEditableElement) { |
| + const size_t commonPrefixLength = computeCommonPrefixLength(oldText, newText); |
| + |
| + // For grapheme cluster, we should adjust it for grapheme boundary. |
| + const EphemeralRange& range = |
| + PlainTextRange(0, commonPrefixLength).createRange(*rootEditableElement); |
| + if (range.isNull()) |
| + return 0; |
| + const Position& position = range.endPosition(); |
| + const size_t diff = computeDistanceToLeftGraphemeBoundary(position); |
| + DCHECK_GE(commonPrefixLength, diff); |
| + return commonPrefixLength - diff; |
| +} |
| + |
| +// If current position is at grapheme boundary, return 0; otherwise, return the |
| +// distance to its nearest right grapheme boundary. |
| +static size_t computeDistanceToRightGraphemeBoundary(const Position& position) { |
| + const Position& adjustedPosition = nextPositionOf( |
| + previousPositionOf(position, PositionMoveType::GraphemeCluster), |
| + PositionMoveType::GraphemeCluster); |
| + DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode()); |
| + DCHECK_GE(adjustedPosition.computeOffsetInContainerNode(), |
| + position.computeOffsetInContainerNode()); |
| + return static_cast<size_t>(adjustedPosition.computeOffsetInContainerNode() - |
| + position.computeOffsetInContainerNode()); |
| +} |
| + |
| +static size_t computeCommonGraphemeClusterSuffixLength( |
| + const String& oldText, |
| + const String& newText, |
| + const Element* rootEditableElement) { |
| + const size_t commonSuffixLength = computeCommonSuffixLength(oldText, newText); |
| + |
| + // For grapheme cluster, we should adjust it for grapheme boundary. |
| + const EphemeralRange& range = |
| + PlainTextRange(0, oldText.length() - commonSuffixLength) |
| + .createRange(*rootEditableElement); |
| + if (range.isNull()) |
| + return 0; |
| + const Position& position = range.endPosition(); |
| + const size_t diff = computeDistanceToRightGraphemeBoundary(position); |
| + DCHECK_GE(commonSuffixLength, diff); |
| + return commonSuffixLength - diff; |
| +} |
| + |
| +static PlainTextRange getSelectionOffsets(LocalFrame* frame) { |
| + EphemeralRange range = firstEphemeralRangeOf(frame->selection().selection()); |
| + if (range.isNull()) |
| + return PlainTextRange(); |
| + ContainerNode* editable = |
| + frame->selection().rootEditableElementOrTreeScopeRootNode(); |
| + DCHECK(editable); |
| + |
| + return PlainTextRange::create(*editable, range); |
| +} |
| + |
| +static const VisibleSelection createSelectionForIncrementalInsertion( |
| + const size_t start, |
| + const size_t end, |
| + const bool isDirectional, |
| + LocalFrame* frame) { |
| + Element* element = frame->selection().selection().rootEditableElement(); |
| + DCHECK(element); |
| + |
| + const EphemeralRange& startRange = |
| + PlainTextRange(0, static_cast<int>(start)).createRange(*element); |
| + DCHECK(startRange.isNotNull()); |
| + const Position& startPosition = startRange.endPosition(); |
| + |
| + const EphemeralRange& endRange = |
| + PlainTextRange(0, static_cast<int>(end)).createRange(*element); |
| + DCHECK(endRange.isNotNull()); |
| + const Position& endPosition = endRange.endPosition(); |
| + |
| + VisibleSelection selection = |
| + createVisibleSelection(SelectionInDOMTree::Builder() |
| + .setBaseAndExtent(startPosition, endPosition) |
| + .build()); |
| + selection.setIsDirectional(isDirectional); |
| + |
| + return selection; |
| +} |
| + |
| +void InsertIncrementalTextCommand::setSelection(const size_t start, |
| + const size_t end, |
| + LocalFrame* frame) { |
| + const VisibleSelection selection = createSelectionForIncrementalInsertion( |
| + start, end, endingSelection().isDirectional(), frame); |
| + setStartingSelection(selection); |
| + setEndingSelectionWithoutValidation(selection.start(), selection.end()); |
| + |
| + document().frame()->selection().setSelection(selection); |
| +} |
| + |
| +void InsertIncrementalTextCommand::doApply(EditingState* editingState) { |
| + DCHECK_EQ(m_text.find('\n'), kNotFound); |
| + |
| + if (!endingSelection().isNonOrphanedCaretOrRange()) |
| + return; |
| + |
| + LocalFrame* frame = document().frame(); |
| + DCHECK(frame); |
| + const Element* element = endingSelection().rootEditableElement(); |
| + DCHECK(element); |
| + |
| + const String& newText = m_text; |
| + const String oldText = frame->selectedText(); |
| + |
| + const size_t newTextLength = newText.length(); |
| + const size_t commonPrefixLength = |
| + computeCommonGraphemeClusterPrefixLength(oldText, newText, element); |
| + |
| + // We should ignore common prefix when finding common suffix. |
| + const size_t commonSuffixLength = computeCommonGraphemeClusterSuffixLength( |
| + oldText.right(oldText.length() - commonPrefixLength), |
| + newText.right(newTextLength - commonPrefixLength), element); |
| + |
| + const String textToInsert = |
| + newText.substring(commonPrefixLength, newTextLength - commonPrefixLength - |
| + commonSuffixLength); |
| + |
| + const PlainTextRange selectionOffsets = getSelectionOffsets(frame); |
| + const size_t selecitonStart = selectionOffsets.start(); |
| + const size_t selectionEnd = selectionOffsets.end(); |
| + const size_t insertionStart = selecitonStart + commonPrefixLength; |
| + const size_t insertionEnd = selectionEnd - commonSuffixLength; |
| + DCHECK_LE(insertionStart, insertionEnd); |
| + |
| + const VisibleSelection selectionForInsertion = |
| + createSelectionForIncrementalInsertion(insertionStart, insertionEnd, |
| + endingSelection().isDirectional(), |
| + frame); |
| + const bool changeSelection = selectionForInsertion != endingSelection(); |
| + |
| + setStartingSelection(selectionForInsertion); |
| + setEndingSelectionWithoutValidation(selectionForInsertion.start(), |
| + selectionForInsertion.end()); |
| + |
| + // Delete the current selection. |
|
chongz
2016/12/02 20:34:44
I'm not sure if I fully understand the difficultie
yabinh
2016/12/05 08:09:39
Done.
|
| + if (endingSelection().isRange()) { |
| + if (performTrivialReplace(textToInsert, m_selectInsertedText)) { |
| + if (changeSelection) |
| + setSelection(selecitonStart, selecitonStart + newTextLength, frame); |
| + return; |
| + } |
| + document().updateStyleAndLayoutIgnorePendingStylesheets(); |
| + const bool endOfSelectionWasAtStartOfBlock = |
| + isStartOfBlock(endingSelection().visibleEnd()); |
| + deleteSelection(editingState, false, true, false, false); |
| + if (editingState->isAborted()) |
| + return; |
| + |
| + // deleteSelection eventually makes a new endingSelection out of a Position. |
| + // If that Position doesn't have a layoutObject (e.g. it is on a <frameset> |
| + // in the DOM), the VisibleSelection cannot be canonicalized to anything |
| + // other than NoSelection. The rest of this function requires a real |
| + // endingSelection, so bail out. |
| + if (endingSelection().isNone()) |
| + return; |
| + if (endOfSelectionWasAtStartOfBlock) { |
| + if (EditingStyle* typingStyle = frame->selection().typingStyle()) |
| + typingStyle->removeBlockProperties(); |
| + } |
| + } else if (frame->editor().isOverwriteModeEnabled()) { |
| + if (performOverwrite(textToInsert, m_selectInsertedText)) { |
| + if (changeSelection) |
| + setSelection(selecitonStart, selecitonStart + newTextLength, frame); |
| + return; |
| + } |
| + } |
| + |
| + document().updateStyleAndLayoutIgnorePendingStylesheets(); |
| + |
| + Position startPosition(endingSelection().start()); |
| + |
| + Position placeholder; |
| + // We want to remove preserved newlines and brs that will collapse (and thus |
| + // become unnecessary) when content is inserted just before them. |
| + // FIXME: We shouldn't really have to do this, but removing placeholders is a |
| + // workaround for 9661. |
| + // If the caret is just before a placeholder, downstream will normalize the |
| + // caret to it. |
| + Position downstream(mostForwardCaretPosition(startPosition)); |
| + if (lineBreakExistsAtPosition(downstream)) { |
| + // FIXME: This doesn't handle placeholders at the end of anonymous blocks. |
| + VisiblePosition caret = createVisiblePosition(startPosition); |
| + if (isEndOfBlock(caret) && isStartOfParagraph(caret)) |
| + placeholder = downstream; |
| + // Don't remove the placeholder yet, otherwise the block we're inserting |
| + // into would collapse before we get a chance to insert into it. We check |
| + // for a placeholder now, though, because doing so requires the creation of |
| + // a VisiblePosition, and if we did that post-insertion it would force a |
| + // layout. |
| + } |
| + |
| + // Insert the character at the leftmost candidate. |
| + startPosition = mostBackwardCaretPosition(startPosition); |
| + |
| + // It is possible for the node that contains startPosition to contain only |
| + // unrendered whitespace, and so deleteInsignificantText could remove it. |
| + // Save the position before the node in case that happens. |
| + DCHECK(startPosition.computeContainerNode()) << startPosition; |
| + Position positionBeforeStartNode( |
| + Position::inParentBeforeNode(*startPosition.computeContainerNode())); |
| + deleteInsignificantText(startPosition, |
| + mostForwardCaretPosition(startPosition)); |
| + if (!startPosition.isConnected()) |
| + startPosition = positionBeforeStartNode; |
| + if (!isVisuallyEquivalentCandidate(startPosition)) |
| + startPosition = mostForwardCaretPosition(startPosition); |
| + |
| + startPosition = |
| + positionAvoidingSpecialElementBoundary(startPosition, editingState); |
| + if (editingState->isAborted()) |
| + return; |
| + |
| + Position endPosition; |
| + |
| + if (textToInsert == "\t" && isRichlyEditablePosition(startPosition)) { |
| + endPosition = insertTab(startPosition, editingState); |
| + if (editingState->isAborted()) |
| + return; |
| + startPosition = |
| + previousPositionOf(endPosition, PositionMoveType::GraphemeCluster); |
| + if (placeholder.isNotNull()) |
| + removePlaceholderAt(placeholder); |
| + } else { |
| + // Make sure the document is set up to receive textToInsert |
| + startPosition = positionInsideTextNode(startPosition, editingState); |
| + if (editingState->isAborted()) |
| + return; |
| + DCHECK(startPosition.isOffsetInAnchor()) << startPosition; |
| + DCHECK(startPosition.computeContainerNode()) << startPosition; |
| + DCHECK(startPosition.computeContainerNode()->isTextNode()) << startPosition; |
| + if (placeholder.isNotNull()) |
| + removePlaceholderAt(placeholder); |
| + Text* textNode = toText(startPosition.computeContainerNode()); |
| + const unsigned offset = startPosition.offsetInContainerNode(); |
| + |
| + insertTextIntoNode(textNode, offset, textToInsert); |
| + endPosition = Position(textNode, offset + textToInsert.length()); |
| + |
| + if (m_rebalanceType == RebalanceLeadingAndTrailingWhitespaces) { |
| + // The insertion may require adjusting adjacent whitespace, if it is |
| + // present. |
| + rebalanceWhitespaceAt(endPosition); |
| + // Rebalancing on both sides isn't necessary if we've inserted only |
| + // spaces. |
| + if (!shouldRebalanceLeadingWhitespaceFor(textToInsert)) |
| + rebalanceWhitespaceAt(startPosition); |
| + } else { |
| + DCHECK_EQ(m_rebalanceType, RebalanceAllWhitespaces); |
| + if (canRebalance(startPosition) && canRebalance(endPosition)) { |
| + rebalanceWhitespaceOnTextSubstring( |
| + textNode, startPosition.offsetInContainerNode(), |
| + endPosition.offsetInContainerNode()); |
| + } |
| + } |
| + } |
| + |
| + setEndingSelectionWithoutValidation(startPosition, endPosition); |
| + |
| + // Handle the case where there is a typing style. |
| + if (EditingStyle* typingStyle = frame->selection().typingStyle()) { |
| + typingStyle->prepareToApplyAt(endPosition, |
| + EditingStyle::PreserveWritingDirection); |
| + if (!typingStyle->isEmpty()) { |
| + applyStyle(typingStyle, editingState); |
| + if (editingState->isAborted()) |
| + return; |
| + } |
| + } |
| + |
| + if (changeSelection) |
| + setSelection(selecitonStart, selecitonStart + newTextLength, frame); |
| + |
| + if (!m_selectInsertedText) { |
| + SelectionInDOMTree::Builder builder; |
| + builder.setAffinity(endingSelection().affinity()); |
| + builder.setIsDirectional(endingSelection().isDirectional()); |
| + if (endingSelection().end().isNotNull()) |
| + builder.collapse(endingSelection().end()); |
| + setEndingSelection(builder.build()); |
| + } |
| +} |
| + |
| +Position InsertIncrementalTextCommand::insertTab(const Position& pos, |
| + EditingState* editingState) { |
| + document().updateStyleAndLayoutIgnorePendingStylesheets(); |
| + |
| + Position insertPos = createVisiblePosition(pos).deepEquivalent(); |
| + if (insertPos.isNull()) |
| + return pos; |
| + |
| + Node* node = insertPos.computeContainerNode(); |
| + unsigned offset = node->isTextNode() ? insertPos.offsetInContainerNode() : 0; |
| + |
| + // keep tabs coalesced in tab span |
| + if (isTabHTMLSpanElementTextNode(node)) { |
| + Text* textNode = toText(node); |
| + insertTextIntoNode(textNode, offset, "\t"); |
| + return Position(textNode, offset + 1); |
| + } |
| + |
| + // create new tab span |
| + HTMLSpanElement* spanElement = createTabSpanElement(document()); |
| + |
| + // place it |
| + if (!node->isTextNode()) { |
| + insertNodeAt(spanElement, insertPos, editingState); |
| + } else { |
| + Text* textNode = toText(node); |
| + if (offset >= textNode->length()) { |
| + insertNodeAfter(spanElement, textNode, editingState); |
| + } else { |
| + // split node to make room for the span |
| + // NOTE: splitTextNode uses textNode for the |
| + // second node in the split, so we need to |
| + // insert the span before it. |
| + if (offset > 0) |
| + splitTextNode(textNode, offset); |
| + insertNodeBefore(spanElement, textNode, editingState); |
| + } |
| + } |
| + if (editingState->isAborted()) |
| + return Position(); |
| + |
| + // return the position following the new tab |
| + return Position::lastPositionInNode(spanElement); |
| +} |
| + |
| +} // namespace blink |