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

Side by Side 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "core/editing/commands/InsertIncrementalTextCommand.h"
6
7 #include "core/dom/Document.h"
8 #include "core/dom/Element.h"
9 #include "core/dom/Text.h"
10 #include "core/editing/EditingUtilities.h"
11 #include "core/editing/Editor.h"
12 #include "core/editing/PlainTextRange.h"
13 #include "core/editing/VisibleUnits.h"
14 #include "core/frame/LocalFrame.h"
15 #include "core/html/HTMLSpanElement.h"
16
17 namespace blink {
18
19 InsertIncrementalTextCommand::InsertIncrementalTextCommand(
20 Document& document,
21 const String& text,
22 bool selectInsertedText,
23 RebalanceType rebalanceType)
24 : CompositeEditCommand(document),
25 m_text(text),
26 m_selectInsertedText(selectInsertedText),
27 m_rebalanceType(rebalanceType) {}
28
29 String InsertIncrementalTextCommand::textDataForInputEvent() const {
30 return m_text;
31 }
32
33 Position InsertIncrementalTextCommand::positionInsideTextNode(
34 const Position& p,
35 EditingState* editingState) {
36 Position pos = p;
37 if (isTabHTMLSpanElementTextNode(pos.anchorNode())) {
38 Text* textNode = document().createEditingTextNode("");
39 insertNodeAtTabSpanPosition(textNode, pos, editingState);
40 if (editingState->isAborted())
41 return Position();
42 return Position::firstPositionInNode(textNode);
43 }
44
45 // Prepare for text input by looking at the specified position.
46 // It may be necessary to insert a text node to receive characters.
47 if (!pos.computeContainerNode()->isTextNode()) {
48 Text* textNode = document().createEditingTextNode("");
49 insertNodeAt(textNode, pos, editingState);
50 if (editingState->isAborted())
51 return Position();
52 return Position::firstPositionInNode(textNode);
53 }
54
55 return pos;
56 }
57
58 void InsertIncrementalTextCommand::setEndingSelectionWithoutValidation(
59 const Position& startPosition,
60 const Position& endPosition) {
61 // We could have inserted a part of composed character sequence,
62 // so we are basically treating ending selection as a range to avoid
63 // validation. <http://bugs.webkit.org/show_bug.cgi?id=15781>
64 setEndingSelection(SelectionInDOMTree::Builder()
65 .collapse(startPosition)
66 .extend(endPosition)
67 .setIsDirectional(endingSelection().isDirectional())
68 .build());
69 }
70
71 // This avoids the expense of a full fledged delete operation, and avoids a
72 // layout that typically results from text removal.
73 bool InsertIncrementalTextCommand::performTrivialReplace(
74 const String& text,
75 bool selectInsertedText) {
76 if (!endingSelection().isRange())
77 return false;
78
79 if (text.contains('\t') || text.contains(' ') || text.contains('\n'))
80 return false;
81
82 Position start = endingSelection().start();
83 Position endPosition = replaceSelectedTextInNode(text);
84 if (endPosition.isNull())
85 return false;
86
87 setEndingSelectionWithoutValidation(start, endPosition);
88 if (selectInsertedText)
89 return true;
90 setEndingSelection(SelectionInDOMTree::Builder()
91 .collapse(endingSelection().end())
92 .setIsDirectional(endingSelection().isDirectional())
93 .build());
94 return true;
95 }
96
97 bool InsertIncrementalTextCommand::performOverwrite(const String& text,
98 bool selectInsertedText) {
99 Position start = endingSelection().start();
100 if (start.isNull() || !start.isOffsetInAnchor() ||
101 !start.computeContainerNode()->isTextNode())
102 return false;
103 Text* textNode = toText(start.computeContainerNode());
104 if (!textNode)
105 return false;
106
107 unsigned count = std::min(text.length(),
108 textNode->length() - start.offsetInContainerNode());
109 if (!count)
110 return false;
111
112 replaceTextInNode(textNode, start.offsetInContainerNode(), count, text);
113
114 Position endPosition =
115 Position(textNode, start.offsetInContainerNode() + text.length());
116 setEndingSelectionWithoutValidation(start, endPosition);
117 if (selectInsertedText || endingSelection().isNone())
118 return true;
119 setEndingSelection(SelectionInDOMTree::Builder()
120 .collapse(endingSelection().end())
121 .setIsDirectional(endingSelection().isDirectional())
122 .build());
123 return true;
124 }
125
126 static size_t computeCommonPrefixLength(const String& str1,
127 const String& str2) {
128 const size_t maxCommonPrefixLength = std::min(str1.length(), str2.length());
129 for (size_t index = 0; index < maxCommonPrefixLength; ++index) {
130 if (str1[index] != str2[index])
131 return index;
132 }
133 return maxCommonPrefixLength;
134 }
135
136 static size_t computeCommonSuffixLength(const String& str1,
137 const String& str2) {
138 const size_t length1 = str1.length();
139 const size_t length2 = str2.length();
140 const size_t maxCommonSuffixLength = std::min(length1, length2);
141 for (size_t index = 0; index < maxCommonSuffixLength; ++index) {
142 if (str1[length1 - index - 1] != str2[length2 - index - 1])
143 return index;
144 }
145 return maxCommonSuffixLength;
146 }
147
148 // If current position is at grapheme boundary, return 0; otherwise, return the
149 // distance to its nearest left grapheme boundary.
150 static size_t computeDistanceToLeftGraphemeBoundary(const Position& position) {
151 const Position& adjustedPosition = previousPositionOf(
152 nextPositionOf(position, PositionMoveType::GraphemeCluster),
153 PositionMoveType::GraphemeCluster);
154 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode());
155 DCHECK_GE(position.computeOffsetInContainerNode(),
156 adjustedPosition.computeOffsetInContainerNode());
157 return static_cast<size_t>(position.computeOffsetInContainerNode() -
158 adjustedPosition.computeOffsetInContainerNode());
159 }
160
161 static size_t computeCommonGraphemeClusterPrefixLength(
162 const String& oldText,
163 const String& newText,
164 const Element* rootEditableElement) {
165 const size_t commonPrefixLength = computeCommonPrefixLength(oldText, newText);
166
167 // For grapheme cluster, we should adjust it for grapheme boundary.
168 const EphemeralRange& range =
169 PlainTextRange(0, commonPrefixLength).createRange(*rootEditableElement);
170 if (range.isNull())
171 return 0;
172 const Position& position = range.endPosition();
173 const size_t diff = computeDistanceToLeftGraphemeBoundary(position);
174 DCHECK_GE(commonPrefixLength, diff);
175 return commonPrefixLength - diff;
176 }
177
178 // If current position is at grapheme boundary, return 0; otherwise, return the
179 // distance to its nearest right grapheme boundary.
180 static size_t computeDistanceToRightGraphemeBoundary(const Position& position) {
181 const Position& adjustedPosition = nextPositionOf(
182 previousPositionOf(position, PositionMoveType::GraphemeCluster),
183 PositionMoveType::GraphemeCluster);
184 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode());
185 DCHECK_GE(adjustedPosition.computeOffsetInContainerNode(),
186 position.computeOffsetInContainerNode());
187 return static_cast<size_t>(adjustedPosition.computeOffsetInContainerNode() -
188 position.computeOffsetInContainerNode());
189 }
190
191 static size_t computeCommonGraphemeClusterSuffixLength(
192 const String& oldText,
193 const String& newText,
194 const Element* rootEditableElement) {
195 const size_t commonSuffixLength = computeCommonSuffixLength(oldText, newText);
196
197 // For grapheme cluster, we should adjust it for grapheme boundary.
198 const EphemeralRange& range =
199 PlainTextRange(0, oldText.length() - commonSuffixLength)
200 .createRange(*rootEditableElement);
201 if (range.isNull())
202 return 0;
203 const Position& position = range.endPosition();
204 const size_t diff = computeDistanceToRightGraphemeBoundary(position);
205 DCHECK_GE(commonSuffixLength, diff);
206 return commonSuffixLength - diff;
207 }
208
209 static PlainTextRange getSelectionOffsets(LocalFrame* frame) {
210 EphemeralRange range = firstEphemeralRangeOf(frame->selection().selection());
211 if (range.isNull())
212 return PlainTextRange();
213 ContainerNode* editable =
214 frame->selection().rootEditableElementOrTreeScopeRootNode();
215 DCHECK(editable);
216
217 return PlainTextRange::create(*editable, range);
218 }
219
220 static const VisibleSelection createSelectionForIncrementalInsertion(
221 const size_t start,
222 const size_t end,
223 const bool isDirectional,
224 LocalFrame* frame) {
225 Element* element = frame->selection().selection().rootEditableElement();
226 DCHECK(element);
227
228 const EphemeralRange& startRange =
229 PlainTextRange(0, static_cast<int>(start)).createRange(*element);
230 DCHECK(startRange.isNotNull());
231 const Position& startPosition = startRange.endPosition();
232
233 const EphemeralRange& endRange =
234 PlainTextRange(0, static_cast<int>(end)).createRange(*element);
235 DCHECK(endRange.isNotNull());
236 const Position& endPosition = endRange.endPosition();
237
238 VisibleSelection selection =
239 createVisibleSelection(SelectionInDOMTree::Builder()
240 .setBaseAndExtent(startPosition, endPosition)
241 .build());
242 selection.setIsDirectional(isDirectional);
243
244 return selection;
245 }
246
247 void InsertIncrementalTextCommand::setSelection(const size_t start,
248 const size_t end,
249 LocalFrame* frame) {
250 const VisibleSelection selection = createSelectionForIncrementalInsertion(
251 start, end, endingSelection().isDirectional(), frame);
252 setStartingSelection(selection);
253 setEndingSelectionWithoutValidation(selection.start(), selection.end());
254
255 document().frame()->selection().setSelection(selection);
256 }
257
258 void InsertIncrementalTextCommand::doApply(EditingState* editingState) {
259 DCHECK_EQ(m_text.find('\n'), kNotFound);
260
261 if (!endingSelection().isNonOrphanedCaretOrRange())
262 return;
263
264 LocalFrame* frame = document().frame();
265 DCHECK(frame);
266 const Element* element = endingSelection().rootEditableElement();
267 DCHECK(element);
268
269 const String& newText = m_text;
270 const String oldText = frame->selectedText();
271
272 const size_t newTextLength = newText.length();
273 const size_t commonPrefixLength =
274 computeCommonGraphemeClusterPrefixLength(oldText, newText, element);
275
276 // We should ignore common prefix when finding common suffix.
277 const size_t commonSuffixLength = computeCommonGraphemeClusterSuffixLength(
278 oldText.right(oldText.length() - commonPrefixLength),
279 newText.right(newTextLength - commonPrefixLength), element);
280
281 const String textToInsert =
282 newText.substring(commonPrefixLength, newTextLength - commonPrefixLength -
283 commonSuffixLength);
284
285 const PlainTextRange selectionOffsets = getSelectionOffsets(frame);
286 const size_t selecitonStart = selectionOffsets.start();
287 const size_t selectionEnd = selectionOffsets.end();
288 const size_t insertionStart = selecitonStart + commonPrefixLength;
289 const size_t insertionEnd = selectionEnd - commonSuffixLength;
290 DCHECK_LE(insertionStart, insertionEnd);
291
292 const VisibleSelection selectionForInsertion =
293 createSelectionForIncrementalInsertion(insertionStart, insertionEnd,
294 endingSelection().isDirectional(),
295 frame);
296 const bool changeSelection = selectionForInsertion != endingSelection();
297
298 setStartingSelection(selectionForInsertion);
299 setEndingSelectionWithoutValidation(selectionForInsertion.start(),
300 selectionForInsertion.end());
301
302 // 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.
303 if (endingSelection().isRange()) {
304 if (performTrivialReplace(textToInsert, m_selectInsertedText)) {
305 if (changeSelection)
306 setSelection(selecitonStart, selecitonStart + newTextLength, frame);
307 return;
308 }
309 document().updateStyleAndLayoutIgnorePendingStylesheets();
310 const bool endOfSelectionWasAtStartOfBlock =
311 isStartOfBlock(endingSelection().visibleEnd());
312 deleteSelection(editingState, false, true, false, false);
313 if (editingState->isAborted())
314 return;
315
316 // deleteSelection eventually makes a new endingSelection out of a Position.
317 // If that Position doesn't have a layoutObject (e.g. it is on a <frameset>
318 // in the DOM), the VisibleSelection cannot be canonicalized to anything
319 // other than NoSelection. The rest of this function requires a real
320 // endingSelection, so bail out.
321 if (endingSelection().isNone())
322 return;
323 if (endOfSelectionWasAtStartOfBlock) {
324 if (EditingStyle* typingStyle = frame->selection().typingStyle())
325 typingStyle->removeBlockProperties();
326 }
327 } else if (frame->editor().isOverwriteModeEnabled()) {
328 if (performOverwrite(textToInsert, m_selectInsertedText)) {
329 if (changeSelection)
330 setSelection(selecitonStart, selecitonStart + newTextLength, frame);
331 return;
332 }
333 }
334
335 document().updateStyleAndLayoutIgnorePendingStylesheets();
336
337 Position startPosition(endingSelection().start());
338
339 Position placeholder;
340 // We want to remove preserved newlines and brs that will collapse (and thus
341 // become unnecessary) when content is inserted just before them.
342 // FIXME: We shouldn't really have to do this, but removing placeholders is a
343 // workaround for 9661.
344 // If the caret is just before a placeholder, downstream will normalize the
345 // caret to it.
346 Position downstream(mostForwardCaretPosition(startPosition));
347 if (lineBreakExistsAtPosition(downstream)) {
348 // FIXME: This doesn't handle placeholders at the end of anonymous blocks.
349 VisiblePosition caret = createVisiblePosition(startPosition);
350 if (isEndOfBlock(caret) && isStartOfParagraph(caret))
351 placeholder = downstream;
352 // Don't remove the placeholder yet, otherwise the block we're inserting
353 // into would collapse before we get a chance to insert into it. We check
354 // for a placeholder now, though, because doing so requires the creation of
355 // a VisiblePosition, and if we did that post-insertion it would force a
356 // layout.
357 }
358
359 // Insert the character at the leftmost candidate.
360 startPosition = mostBackwardCaretPosition(startPosition);
361
362 // It is possible for the node that contains startPosition to contain only
363 // unrendered whitespace, and so deleteInsignificantText could remove it.
364 // Save the position before the node in case that happens.
365 DCHECK(startPosition.computeContainerNode()) << startPosition;
366 Position positionBeforeStartNode(
367 Position::inParentBeforeNode(*startPosition.computeContainerNode()));
368 deleteInsignificantText(startPosition,
369 mostForwardCaretPosition(startPosition));
370 if (!startPosition.isConnected())
371 startPosition = positionBeforeStartNode;
372 if (!isVisuallyEquivalentCandidate(startPosition))
373 startPosition = mostForwardCaretPosition(startPosition);
374
375 startPosition =
376 positionAvoidingSpecialElementBoundary(startPosition, editingState);
377 if (editingState->isAborted())
378 return;
379
380 Position endPosition;
381
382 if (textToInsert == "\t" && isRichlyEditablePosition(startPosition)) {
383 endPosition = insertTab(startPosition, editingState);
384 if (editingState->isAborted())
385 return;
386 startPosition =
387 previousPositionOf(endPosition, PositionMoveType::GraphemeCluster);
388 if (placeholder.isNotNull())
389 removePlaceholderAt(placeholder);
390 } else {
391 // Make sure the document is set up to receive textToInsert
392 startPosition = positionInsideTextNode(startPosition, editingState);
393 if (editingState->isAborted())
394 return;
395 DCHECK(startPosition.isOffsetInAnchor()) << startPosition;
396 DCHECK(startPosition.computeContainerNode()) << startPosition;
397 DCHECK(startPosition.computeContainerNode()->isTextNode()) << startPosition;
398 if (placeholder.isNotNull())
399 removePlaceholderAt(placeholder);
400 Text* textNode = toText(startPosition.computeContainerNode());
401 const unsigned offset = startPosition.offsetInContainerNode();
402
403 insertTextIntoNode(textNode, offset, textToInsert);
404 endPosition = Position(textNode, offset + textToInsert.length());
405
406 if (m_rebalanceType == RebalanceLeadingAndTrailingWhitespaces) {
407 // The insertion may require adjusting adjacent whitespace, if it is
408 // present.
409 rebalanceWhitespaceAt(endPosition);
410 // Rebalancing on both sides isn't necessary if we've inserted only
411 // spaces.
412 if (!shouldRebalanceLeadingWhitespaceFor(textToInsert))
413 rebalanceWhitespaceAt(startPosition);
414 } else {
415 DCHECK_EQ(m_rebalanceType, RebalanceAllWhitespaces);
416 if (canRebalance(startPosition) && canRebalance(endPosition)) {
417 rebalanceWhitespaceOnTextSubstring(
418 textNode, startPosition.offsetInContainerNode(),
419 endPosition.offsetInContainerNode());
420 }
421 }
422 }
423
424 setEndingSelectionWithoutValidation(startPosition, endPosition);
425
426 // Handle the case where there is a typing style.
427 if (EditingStyle* typingStyle = frame->selection().typingStyle()) {
428 typingStyle->prepareToApplyAt(endPosition,
429 EditingStyle::PreserveWritingDirection);
430 if (!typingStyle->isEmpty()) {
431 applyStyle(typingStyle, editingState);
432 if (editingState->isAborted())
433 return;
434 }
435 }
436
437 if (changeSelection)
438 setSelection(selecitonStart, selecitonStart + newTextLength, frame);
439
440 if (!m_selectInsertedText) {
441 SelectionInDOMTree::Builder builder;
442 builder.setAffinity(endingSelection().affinity());
443 builder.setIsDirectional(endingSelection().isDirectional());
444 if (endingSelection().end().isNotNull())
445 builder.collapse(endingSelection().end());
446 setEndingSelection(builder.build());
447 }
448 }
449
450 Position InsertIncrementalTextCommand::insertTab(const Position& pos,
451 EditingState* editingState) {
452 document().updateStyleAndLayoutIgnorePendingStylesheets();
453
454 Position insertPos = createVisiblePosition(pos).deepEquivalent();
455 if (insertPos.isNull())
456 return pos;
457
458 Node* node = insertPos.computeContainerNode();
459 unsigned offset = node->isTextNode() ? insertPos.offsetInContainerNode() : 0;
460
461 // keep tabs coalesced in tab span
462 if (isTabHTMLSpanElementTextNode(node)) {
463 Text* textNode = toText(node);
464 insertTextIntoNode(textNode, offset, "\t");
465 return Position(textNode, offset + 1);
466 }
467
468 // create new tab span
469 HTMLSpanElement* spanElement = createTabSpanElement(document());
470
471 // place it
472 if (!node->isTextNode()) {
473 insertNodeAt(spanElement, insertPos, editingState);
474 } else {
475 Text* textNode = toText(node);
476 if (offset >= textNode->length()) {
477 insertNodeAfter(spanElement, textNode, editingState);
478 } else {
479 // split node to make room for the span
480 // NOTE: splitTextNode uses textNode for the
481 // second node in the split, so we need to
482 // insert the span before it.
483 if (offset > 0)
484 splitTextNode(textNode, offset);
485 insertNodeBefore(spanElement, textNode, editingState);
486 }
487 }
488 if (editingState->isAborted())
489 return Position();
490
491 // return the position following the new tab
492 return Position::lastPositionInNode(spanElement);
493 }
494
495 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698