Chromium Code Reviews| Index: third_party/WebKit/Source/core/editing/markers/DocumentMarkerController.cpp |
| diff --git a/third_party/WebKit/Source/core/editing/markers/DocumentMarkerController.cpp b/third_party/WebKit/Source/core/editing/markers/DocumentMarkerController.cpp |
| index 165d36dd9f0a9028cf965e7882a70b1df643c9bd..3332ecf77bfc251f528ee6e494a378a16d3429b2 100644 |
| --- a/third_party/WebKit/Source/core/editing/markers/DocumentMarkerController.cpp |
| +++ b/third_party/WebKit/Source/core/editing/markers/DocumentMarkerController.cpp |
| @@ -35,6 +35,7 @@ |
| #include "core/editing/iterators/TextIterator.h" |
| #include "core/editing/markers/RenderedDocumentMarker.h" |
| #include "core/frame/FrameView.h" |
| +#include "core/frame/LocalFrame.h" |
| #include "core/layout/LayoutObject.h" |
| #include <algorithm> |
| @@ -71,6 +72,10 @@ DocumentMarker::MarkerTypeIndex MarkerTypeToMarkerIndex( |
| return DocumentMarker::InvisibleSpellcheckMarkerIndex; |
| case DocumentMarker::Composition: |
| return DocumentMarker::CompositionMarkerIndex; |
| + case DocumentMarker::Suggestion: |
| + return DocumentMarker::SuggestionMarkerIndex; |
| + case DocumentMarker::SuggestionBackgroundHighlight: |
| + return DocumentMarker::SuggestionBackgroundHighlightMarkerIndex; |
| } |
| NOTREACHED(); |
| @@ -85,11 +90,14 @@ inline bool DocumentMarkerController::possiblyHasMarkers( |
| } |
| DocumentMarkerController::DocumentMarkerController(const Document& document) |
| - : m_possiblyExistingMarkerTypes(0), m_document(&document) {} |
| + : m_possiblyExistingMarkerTypes(0), |
| + m_document(&document), |
| + m_nextSuggestionMarkerID(0) {} |
| void DocumentMarkerController::clear() { |
| m_markers.clear(); |
| m_possiblyExistingMarkerTypes = 0; |
| + m_nextSuggestionMarkerID = 0; |
| } |
| void DocumentMarkerController::addMarker(const Position& start, |
| @@ -124,19 +132,40 @@ void DocumentMarkerController::addTextMatchMarker(const EphemeralRange& range, |
| // throttling algorithm. crbug.com/6819. |
| } |
| -void DocumentMarkerController::addCompositionMarker(const Position& start, |
| - const Position& end, |
| - Color underlineColor, |
| - bool thick, |
| - Color backgroundColor) { |
| +void DocumentMarkerController::addCompositionMarker( |
| + const Position& start, |
| + const Position& end, |
| + Color underlineColor, |
| + bool thick, |
| + Color backgroundColor, |
| + const std::vector<std::string>& suggestions) { |
|
esprehn
2017/01/31 22:41:35
ditto
rlanday
2017/01/31 23:30:09
Ok
|
| DCHECK(!m_document->needsLayoutTreeUpdate()); |
| - |
| for (TextIterator markedText(start, end); !markedText.atEnd(); |
| - markedText.advance()) |
| + markedText.advance()) { |
| addMarker(markedText.currentContainer(), |
| DocumentMarker(markedText.startOffsetInCurrentContainer(), |
| markedText.endOffsetInCurrentContainer(), |
| + underlineColor, thick, backgroundColor, |
| + suggestions, m_nextSuggestionMarkerID++)); |
| + } |
| +} |
| + |
| +void DocumentMarkerController::addSuggestionBackgroundHighlightMarker( |
| + const Position& start, |
| + const Position& end, |
| + Color underlineColor, |
| + bool thick, |
| + Color backgroundColor) { |
| + DCHECK(!m_document->needsLayoutTreeUpdate()); |
| + |
| + for (TextIterator markedText(start, end); !markedText.atEnd(); |
| + markedText.advance()) { |
| + addMarker(markedText.currentContainer(), |
| + DocumentMarker(DocumentMarker::SuggestionBackgroundHighlight, |
| + markedText.startOffsetInCurrentContainer(), |
| + markedText.endOffsetInCurrentContainer(), |
| underlineColor, thick, backgroundColor)); |
| + } |
| } |
| void DocumentMarkerController::prepareForDestruction() { |
| @@ -155,6 +184,7 @@ void DocumentMarkerController::removeMarkers( |
| int startOffset = markedText.startOffsetInCurrentContainer(); |
| int endOffset = markedText.endOffsetInCurrentContainer(); |
| + |
| removeMarkers(markedText.currentContainer(), startOffset, |
| endOffset - startOffset, markerTypes, |
| shouldRemovePartiallyOverlappingMarker); |
| @@ -178,11 +208,6 @@ static bool startsFurther(const Member<RenderedDocumentMarker>& lhv, |
| return lhv->startOffset() < rhv->startOffset(); |
| } |
| -static bool startsAfter(const Member<RenderedDocumentMarker>& marker, |
| - size_t startOffset) { |
| - return marker->startOffset() < startOffset; |
| -} |
| - |
| static bool compareByStart(const Member<DocumentMarker>& lhv, |
| const Member<DocumentMarker>& rhv) { |
| return lhv->startOffset() < rhv->startOffset(); |
| @@ -249,7 +274,8 @@ void DocumentMarkerController::addMarker(Node* node, |
| list->push_back(newRenderedMarker); |
| } else { |
| if (newMarker.type() != DocumentMarker::TextMatch && |
| - newMarker.type() != DocumentMarker::Composition) { |
| + newMarker.type() != DocumentMarker::Composition && |
| + newMarker.type() != DocumentMarker::Suggestion) { |
| mergeOverlapping(list.get(), newRenderedMarker); |
| } else { |
| MarkerList::iterator pos = std::lower_bound(list->begin(), list->end(), |
| @@ -348,6 +374,7 @@ void DocumentMarkerController::removeMarkers( |
| if (!possiblyHasMarkers(markerTypes)) |
| return; |
| + |
| DCHECK(!(m_markers.isEmpty())); |
| MarkerLists* markers = m_markers.get(node); |
| @@ -469,17 +496,35 @@ DocumentMarkerVector DocumentMarkerController::markers() { |
| DocumentMarkerVector DocumentMarkerController::markersInRange( |
| const EphemeralRange& range, |
| DocumentMarker::MarkerTypes markerTypes) { |
| + return markersInRangeInternal(range, markerTypes, false); |
| +} |
| + |
| +DocumentMarkerVector DocumentMarkerController::markersInRangeInclusive( |
| + const EphemeralRange& range, |
| + DocumentMarker::MarkerTypes markerTypes) { |
| + return markersInRangeInternal(range, markerTypes, true); |
| +} |
| + |
| +DocumentMarkerVector DocumentMarkerController::markersInRangeInternal( |
| + const EphemeralRange& range, |
| + DocumentMarker::MarkerTypes markerTypes, |
| + bool includeSpansTouchingEndpoints) { |
| if (!possiblyHasMarkers(markerTypes)) |
| return DocumentMarkerVector(); |
| DocumentMarkerVector foundMarkers; |
| Node* startContainer = range.startPosition().computeContainerNode(); |
| - DCHECK(startContainer); |
| + if (!startContainer) |
| + return DocumentMarkerVector(); |
| + |
| unsigned startOffset = static_cast<unsigned>( |
| range.startPosition().computeOffsetInContainerNode()); |
| + |
| Node* endContainer = range.endPosition().computeContainerNode(); |
| - DCHECK(endContainer); |
| + if (!endContainer) |
| + return DocumentMarkerVector(); |
| + |
| unsigned endOffset = |
| static_cast<unsigned>(range.endPosition().computeOffsetInContainerNode()); |
| @@ -487,10 +532,18 @@ DocumentMarkerVector DocumentMarkerController::markersInRange( |
| for (DocumentMarker* marker : markersFor(&node)) { |
| if (!markerTypes.contains(marker->type())) |
| continue; |
| - if (node == startContainer && marker->endOffset() <= startOffset) |
| - continue; |
| - if (node == endContainer && marker->startOffset() >= endOffset) |
| - continue; |
| + if (node == startContainer) { |
| + if (marker->endOffset() < startOffset || |
| + (marker->endOffset() == startOffset && |
| + !includeSpansTouchingEndpoints)) |
| + continue; |
| + } |
| + if (node == endContainer) { |
| + if (marker->startOffset() > endOffset || |
| + (marker->startOffset() == endOffset && |
| + !includeSpansTouchingEndpoints)) |
| + continue; |
| + } |
| foundMarkers.push_back(marker); |
| } |
| } |
| @@ -641,6 +694,127 @@ void DocumentMarkerController::removeMarkers( |
| m_possiblyExistingMarkerTypes.remove(markerTypes); |
| } |
| +void DocumentMarkerController::removeSuggestionMarkersByID( |
| + const Vector<int>& idsToRemove) { |
| + for (auto& nodeMarkers : m_markers) { |
| + MarkerLists& markers = *nodeMarkers.value; |
| + Member<MarkerList>& suggestionMarkerList = |
| + markers[DocumentMarker::SuggestionMarkerIndex]; |
| + if (!suggestionMarkerList) |
| + continue; |
| + bool removedMarkers = false; |
| + for (size_t j = suggestionMarkerList->size(); j > 0; --j) { |
| + if (idsToRemove.contains( |
| + suggestionMarkerList->at(j - 1)->suggestionMarkerID())) { |
| + suggestionMarkerList->remove(j - 1); |
| + removedMarkers = true; |
| + } |
| + } |
| + } |
| +} |
| + |
| +void DocumentMarkerController::removeMarkersForWordsAffectedByEditing( |
| + DocumentMarker::MarkerTypes markerTypes, |
| + bool doNotRemoveIfSelectionAtWordBoundary) { |
| + DCHECK(m_document->frame()->selection().isAvailable()); |
| + TRACE_EVENT0( |
| + "blink", |
| + "DocumentMarkerController::removeMarkersForWordsAffectedByEditing"); |
| + |
| + Document* document = m_document->frame()->document(); |
| + DCHECK(document); |
| + |
| + // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| + // needs to be audited. See http://crbug.com/590369 for more details. |
| + document->updateStyleAndLayoutIgnorePendingStylesheets(); |
| + |
| + // We want to remove the markers from a word if an editing command will change |
| + // the word. This can happen in one of several scenarios: |
| + // 1. Insert in the middle of a word. |
| + // 2. Appending non whitespace at the beginning of word. |
| + // 3. Appending non whitespace at the end of word. |
| + // Note that, appending only whitespaces at the beginning or end of word won't |
| + // change the word, so we don't need to remove the markers on that word. Of |
| + // course, if current selection is a range, we potentially will edit two words |
| + // that fall on the boundaries of selection, and remove words between the |
| + // selection boundaries. |
| + VisiblePosition startOfSelection = |
| + m_document->frame()->selection().selection().visibleStart(); |
| + VisiblePosition endOfSelection = |
| + m_document->frame()->selection().selection().visibleEnd(); |
| + if (startOfSelection.isNull()) |
| + return; |
| + // First word is the word that ends after or on the start of selection. |
| + VisiblePosition startOfFirstWord = |
| + startOfWord(startOfSelection, LeftWordIfOnBoundary); |
| + VisiblePosition endOfFirstWord = |
| + endOfWord(startOfSelection, LeftWordIfOnBoundary); |
| + // Last word is the word that begins before or on the end of selection |
| + VisiblePosition startOfLastWord = |
| + startOfWord(endOfSelection, RightWordIfOnBoundary); |
| + VisiblePosition endOfLastWord = |
| + endOfWord(endOfSelection, RightWordIfOnBoundary); |
| + |
| + if (startOfFirstWord.isNull()) { |
| + startOfFirstWord = startOfWord(startOfSelection, RightWordIfOnBoundary); |
| + endOfFirstWord = endOfWord(startOfSelection, RightWordIfOnBoundary); |
| + } |
| + |
| + if (endOfLastWord.isNull()) { |
| + startOfLastWord = startOfWord(endOfSelection, LeftWordIfOnBoundary); |
| + endOfLastWord = endOfWord(endOfSelection, LeftWordIfOnBoundary); |
| + } |
| + |
| + // If doNotRemoveIfSelectionAtWordBoundary is true, and first word ends at the |
| + // start of selection, we choose next word as the first word. |
| + if (doNotRemoveIfSelectionAtWordBoundary && |
| + endOfFirstWord.deepEquivalent() == startOfSelection.deepEquivalent()) { |
| + startOfFirstWord = nextWordPosition(startOfFirstWord); |
| + endOfFirstWord = endOfWord(startOfFirstWord, RightWordIfOnBoundary); |
| + if (startOfFirstWord.deepEquivalent() == endOfSelection.deepEquivalent()) |
| + return; |
| + } |
| + |
| + // If doNotRemoveIfSelectionAtWordBoundary is true, and last word begins at |
| + // the end of selection, we choose previous word as the last word. |
| + if (doNotRemoveIfSelectionAtWordBoundary && |
| + startOfLastWord.deepEquivalent() == endOfSelection.deepEquivalent()) { |
| + startOfLastWord = previousWordPosition(startOfLastWord); |
| + endOfLastWord = endOfWord(startOfLastWord, RightWordIfOnBoundary); |
| + if (endOfLastWord.deepEquivalent() == startOfSelection.deepEquivalent()) |
| + return; |
| + } |
| + |
| + if (startOfFirstWord.isNull() || endOfFirstWord.isNull() || |
| + startOfLastWord.isNull() || endOfLastWord.isNull()) |
| + return; |
| + |
| + const Position& removeMarkerStart = startOfFirstWord.deepEquivalent(); |
| + const Position& removeMarkerEnd = endOfLastWord.deepEquivalent(); |
| + if (removeMarkerStart > removeMarkerEnd) { |
| + // editing/inserting/insert-br-008.html and more reach here. |
| + // TODO(yosin): To avoid |DCHECK(removeMarkerStart <= removeMarkerEnd)| |
| + // in |EphemeralRange| constructor, we have this if-statement. Once we |
| + // fix |startOfWord()| and |endOfWord()|, we should remove this |
| + // if-statement. |
| + return; |
| + } |
| + |
| + // Now we remove markers on everything between startOfFirstWord and |
| + // endOfLastWord. However, if an autocorrection change a single word to |
| + // multiple words, we want to remove correction mark from all the resulted |
| + // words even we only edit one of them. For example, assuming autocorrection |
| + // changes "avantgarde" to "avant garde", we will have CorrectionIndicator |
| + // marker on both words and on the whitespace between them. If we then edit |
| + // garde, we would like to remove the marker from word "avant" and whitespace |
| + // as well. So we need to get the continous range of of marker that contains |
| + // the word in question, and remove marker on that whole range. |
| + const EphemeralRange wordRange(removeMarkerStart, removeMarkerEnd); |
| + document->markers().removeMarkers( |
| + wordRange, markerTypes, |
| + DocumentMarkerController::RemovePartiallyOverlappingMarker); |
| +} |
| + |
| void DocumentMarkerController::removeMarkersFromList( |
| MarkerMap::iterator iterator, |
| DocumentMarker::MarkerTypes markerTypes) { |
| @@ -721,8 +895,13 @@ void DocumentMarkerController::repaintMarkers( |
| } |
| void DocumentMarkerController::shiftMarkers(Node* node, |
| - unsigned startOffset, |
| - int delta) { |
| + int startOffset, |
| + int prevLength, |
| + int newLength) { |
| + DCHECK_GE(startOffset, 0); |
| + DCHECK_GE(prevLength, 0); |
| + DCHECK_GE(newLength, 0); |
| + |
| if (!possiblyHasMarkers(DocumentMarker::AllMarkers())) |
| return; |
| DCHECK(!m_markers.isEmpty()); |
| @@ -738,15 +917,34 @@ void DocumentMarkerController::shiftMarkers(Node* node, |
| Member<MarkerList>& list = (*markers)[markerListIndex]; |
| if (!list) |
| continue; |
| - MarkerList::iterator startPos = |
| - std::lower_bound(list->begin(), list->end(), startOffset, startsAfter); |
| - for (MarkerList::iterator marker = startPos; marker != list->end(); |
| + |
| + // algorithm from https://dom.spec.whatwg.org/#concept-cd-replace |
|
rlanday
2017/01/31 19:50:22
This is fancy logic that lets you replace part of
|
| + for (MarkerList::iterator marker = list->begin(); marker != list->end(); |
| ++marker) { |
| -#if DCHECK_IS_ON() |
| - int startOffset = (*marker)->startOffset(); |
| - DCHECK_GE(startOffset + delta, 0); |
| -#endif |
| - (*marker)->shiftOffsets(delta); |
| + if ((*marker)->startOffset() > (unsigned)startOffset) { |
| + if ((*marker)->startOffset() <= (unsigned)(startOffset + prevLength)) { |
| + // Marker start was in the replaced text. Move to beginning of new |
| + // text |
| + (*marker)->setStartOffset(startOffset); |
| + } else { |
| + // Marker start was after the replaced text. Shift by length |
| + // difference |
| + unsigned oldStartOffset = (*marker)->startOffset(); |
| + (*marker)->setStartOffset(oldStartOffset + newLength - prevLength); |
| + } |
| + } |
| + |
| + if ((*marker)->endOffset() > (unsigned)startOffset) { |
| + if ((*marker)->endOffset() <= (unsigned)(startOffset + prevLength)) { |
| + // Marker end was in the replaced text. Move to beginning of new text |
| + (*marker)->setEndOffset(startOffset); |
| + } else { |
| + // Marker end was after the replaced text. Shift by length difference |
| + unsigned oldEndOffset = (*marker)->endOffset(); |
| + (*marker)->setEndOffset(oldEndOffset + newLength - prevLength); |
| + } |
| + } |
| + |
| didShiftMarker = true; |
| } |
| } |