Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3)

Unified Diff: third_party/WebKit/Source/core/editing/commands/InsertIncrementalTextCommand.cpp

Issue 2530843003: Introduce InsertIncrementalTextCommand to respect existing style for composition (Closed)
Patch Set: Introduce InsertIncrementalTextCommand Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698